chore: merge upstream v12.5.1 + keep local fixes
Deploy Install Scripts / deploy (push) Has been cancelled
Windows / build (push) Has been cancelled
Deploy Install Scripts / deploy (push) Has been cancelled
Windows / build (push) Has been cancelled
Upstream changes (v12.4.7 → v12.5.1): v12.4.8 — timeline tool: coerce stringified numeric anchor in MCP tool (#2176) v12.4.9 — 7 critical fixes (PR #2219): - build/bundle drift fix - strip privacy tags before summarization - preserve relevance order in semantic search - restore Windows spawn + Windows CI - Codex transcript ingestion + Windows queue self-deadlock fix - SDK boundary isolation (closes 6 issues) - standalone batch fixes v12.5.0 — observation pipeline cleanup: - removed per-message retry counter that silently dropped data - parser collapsed to binary {valid:true,...} | {valid:false} - schema migrations v31 + v32 drop dead pending_messages columns: retry_count, failed_at_epoch, completed_at_epoch, worker_pid - status enum reduced to 'pending' | 'processing' - GeneratorExitHandler drain-in-flight loop deleted - PendingMessageStore: 226 → 165 lines (markFailed etc removed) - net -181 lines of source v12.5.1 — install fix for Node 25+ (tree-sitter native build skipped via trustedDependencies allowlist) (#2278) Local fixes preserved (resolved manual conflicts in SessionStore.ts and sessions/create.ts where upstream removed surrounding context): - env-sanitizer PATH extension for claude CLI lookup (auto-merged with upstream's expanded ENV_PRESERVE for AWS Bedrock / Vertex auth vars) - SessionStore + sessions/create stale session reset (mac sleep / 4h wall-clock) Built artifacts rebuilt; both fixes verified present in worker-service.cjs. Worker restarted to v12.5.1 (PID 94088). Schema v31+v32 auto-migration ran on startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "12.4.7",
|
||||
"version": "12.5.1",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "12.4.7",
|
||||
"version": "12.5.1",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "12.4.7",
|
||||
"version": "12.5.1",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Keep the build context small for evals/swebench/Dockerfile.agent.
|
||||
# The Dockerfile needs `plugin/` and `evals/swebench/` — do NOT exclude them.
|
||||
node_modules/
|
||||
.git/
|
||||
logs/
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
# Normalize all text files to LF on commit and checkout.
|
||||
# This prevents CRLF shebang lines in bundled scripts from breaking
|
||||
# the MCP server on macOS/Linux when built on Windows. Fixes #1342.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Compiled plugin scripts must always be LF — CRLF in the shebang
|
||||
# causes "env: node\r: No such file or directory" on non-Windows hosts.
|
||||
plugin/scripts/*.cjs eol=lf
|
||||
plugin/scripts/*.js eol=lf
|
||||
|
||||
# Explicitly mark binary assets so git never modifies them.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -36,15 +36,6 @@ jobs:
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ on:
|
||||
jobs:
|
||||
convert:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on labeled event if the label is 'feature-request', or always run on workflow_dispatch
|
||||
if: |
|
||||
(github.event_name == 'issues' && github.event.label.name == 'feature-request') ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'plugin/scripts/**'
|
||||
- 'package.json'
|
||||
- 'bunfig.toml'
|
||||
- '.github/workflows/windows.yml'
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install Bun (worker runtime)
|
||||
run: |
|
||||
irm bun.sh/install.ps1 | iex
|
||||
shell: pwsh
|
||||
|
||||
- run: npm install --no-audit --no-fund
|
||||
|
||||
# Build only — the build-and-sync script also runs marketplace sync + worker
|
||||
# restart from a hardcoded ~/.claude/plugins path that doesn't exist on CI.
|
||||
- run: npm run build
|
||||
+2
-7
@@ -21,14 +21,11 @@ bun.lock
|
||||
private/
|
||||
Auto Run Docs/
|
||||
|
||||
# Generated UI files (built from viewer-template.html)
|
||||
src/ui/viewer.html
|
||||
|
||||
# Local MCP server config (for development only)
|
||||
.mcp.json
|
||||
.cursor/
|
||||
|
||||
# Ignore WebStorm project files (for dinosaur IDE users)
|
||||
.idea/
|
||||
|
||||
.claude-octopus/
|
||||
@@ -37,16 +34,14 @@ src/ui/viewer.html
|
||||
.claude/scheduled_tasks.lock
|
||||
.octo/
|
||||
|
||||
# Installer marker — dropped by the claude-mem CLI at install time
|
||||
plugin/.cli-installed
|
||||
|
||||
# Local contribution analysis (not part of upstream)
|
||||
plugin/scripts/claude-mem
|
||||
|
||||
CONTRIB_NOTES.md
|
||||
|
||||
# Docker container runtime data (basic claude-mem container)
|
||||
.docker-claude-mem-data/
|
||||
|
||||
# SWE-bench eval outputs
|
||||
evals/swebench/runs/
|
||||
claude-opus-4-7+claude-mem.*.json
|
||||
logs/run_evaluation/
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Source code (dist/ and plugin/ are the shipped artifacts)
|
||||
src/
|
||||
scripts/
|
||||
tests/
|
||||
@@ -7,14 +6,12 @@ datasets/
|
||||
private/
|
||||
antipattern-czar/
|
||||
|
||||
# Heavy binaries installed at runtime via smart-install.js
|
||||
plugin/node_modules/
|
||||
plugin/scripts/claude-mem
|
||||
plugin/bun.lock
|
||||
plugin/data/
|
||||
plugin/data.backup/
|
||||
|
||||
# Development files
|
||||
*.ts
|
||||
!*.d.ts
|
||||
tsconfig*.json
|
||||
@@ -24,7 +21,6 @@ tsconfig*.json
|
||||
jest.config*
|
||||
vitest.config*
|
||||
|
||||
# Git and CI
|
||||
.git/
|
||||
.github/
|
||||
.gitignore
|
||||
@@ -33,14 +29,12 @@ vitest.config*
|
||||
.mcp.json
|
||||
.plan/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
Thumbs.db
|
||||
|
||||
# Misc
|
||||
Auto Run Docs/
|
||||
~*/
|
||||
http*/
|
||||
|
||||
@@ -4,6 +4,71 @@ 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/).
|
||||
|
||||
## [12.5.1] - 2026-05-03
|
||||
|
||||
## Fixed
|
||||
|
||||
- **Install failure on Node 25+** — `bun install` no longer fails when trying to compile the unused `tree-sitter` runtime against Node 25's V8 headers (which require C++20). Added `trustedDependencies: ["tree-sitter-cli"]` to the plugin manifest so bun runs only the CLI's prebuilt-binary download script and skips all other lifecycle scripts — including the failing native compile and the unused `.node` bindings of all 24+ grammar packages. claude-mem only ever shells out to the prebuilt `tree-sitter-cli` Rust binary; the runtime native module was never imported. (#2278)
|
||||
|
||||
## Internal
|
||||
|
||||
- Sync the OpenClaw plugin manifest version (10.4.1 → 12.5.1) so it tracks with the rest of the package going forward; the version-bump skill already lists it but past releases skipped it.
|
||||
|
||||
## [12.5.0] - 2026-05-02
|
||||
|
||||
## Highlights
|
||||
|
||||
**Observation pipeline cleanup — kill the per-message retry counter.** The AI's parseable response is the only success signal; any other response (unparseable, empty, transport error) is a no-op. No more silent data loss after 3 retries.
|
||||
|
||||
## What changed
|
||||
|
||||
- **Parser:** collapsed to binary `{ valid: true, observations, summary } | { valid: false }`. No more `kind`/`skipped` enum dispatch in callers.
|
||||
- **ResponseProcessor:** two branches only — parseable → store + clear pending → broadcast; not parseable → reset claimed-but-unprocessed messages to pending. Removed per-message FIFO popping and the summarize-special-case best-effort confirm.
|
||||
- **PendingMessageStore:** 226 → 165 lines. Removed `markFailed` (the retry counter that silently dropped data after 3 attempts), `transitionMessagesTo`, `confirmProcessed`, `clearFailedOlderThan`, plus four other dead methods.
|
||||
- **Provider cleanup:** removed `processingMessageIds` tracking from Claude, Gemini, OpenRouter providers. The session-scoped clear handles the success path; no per-message in-flight tracking needed.
|
||||
- **GeneratorExitHandler:** drain-in-flight loop deleted; hard-stop / restart-guard paths now just clear pending for the session.
|
||||
- **Schema migration v31 + v32:** dropped four dead columns from `pending_messages` — `retry_count`, `failed_at_epoch`, `completed_at_epoch`, `worker_pid`. Status enum reduced to `'pending' | 'processing'` (the unreachable `'processed'` and `'failed'` are gone).
|
||||
|
||||
## Bug fixes / polish
|
||||
|
||||
- **`SessionQueueProcessor`:** removed two arbitrary 1-second recovery sleeps after error in `claimNextMessage`/`waitForMessage`; let the iterator end cleanly so `GeneratorExitHandler` can restart it.
|
||||
- **`Server.ts` + `SettingsRoutes.ts`:** unified four magic-number `setTimeout` exit-flush patterns (100ms × 2, 1000ms × 2) into one `flushResponseThen` helper using `res.on('finish', ...)`.
|
||||
- **PR review feedback (21+ threads):** install.ts argument fixes, settings cache TTL, Dockerfile login-banner sourcing, docs port-model + Node version updates, regex whitespace fix, Date.UTC for year-mismatch test, sync-marketplace port range guard, banner inflate fail-open, version-bump arg validation.
|
||||
|
||||
## Net diff
|
||||
|
||||
`-181 lines` (worker-service.cjs unaffected; total source lines down).
|
||||
|
||||
## Migration
|
||||
|
||||
Existing databases auto-migrate on worker startup (schema v31 + v32 drop the dead columns). No user action needed.
|
||||
|
||||
## [12.4.9] - 2026-04-30
|
||||
|
||||
Patches in 7 critical fixes from PR #2219 (integration/critical-fixes-april):
|
||||
|
||||
- #2211 build/bundle drift — remove stale macOS binary + regen artifacts (closes #2158, #2200, #2154)
|
||||
- #2204 strip privacy tags before summarization (closes #2149)
|
||||
- #2205 preserve relevance order in semantic search (closes #2153)
|
||||
- #2208 restore Windows spawn (PR #751 re-apply) + Windows CI
|
||||
- #2209 Codex transcript ingestion + queue self-deadlock on Windows (closes #2192)
|
||||
- #2206 isolate SDK boundary — close 6 issues at 3 call sites
|
||||
- #2210 standalone batch — npm peer deps, marketplace self-heal, cache prune
|
||||
|
||||
## [12.4.8] - 2026-04-28
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **timeline tool:** Coerce stringified numeric anchors (e.g. `"123"`) into the observation-ID dispatch path so they no longer fall through to ISO-timestamp parsing and return wrong-epoch windows. The HTTP layer always sends anchor as a string, so this fixes anchor lookups via MCP and HTTP across the board. (#2176)
|
||||
|
||||
## Tests
|
||||
|
||||
- Added a 7-case regression suite covering JS-number anchors, stringified-number anchors (incl. whitespace-padded), session-ID anchors (`S<n>`), ISO-timestamp anchors, garbage anchors, and explicit numeric-not-found behavior. The suite runs against a real in-memory SQLite `SessionStore` to exercise the full dispatch path.
|
||||
|
||||
## Refactors
|
||||
|
||||
- Extracted `parseNumericAnchor` helper in `SearchManager` to centralize anchor coercion across timeline handlers.
|
||||
|
||||
## [12.4.7] - 2026-04-26
|
||||
|
||||
## Cynical deletion + review fixes
|
||||
|
||||
@@ -21,9 +21,9 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
|
||||
|
||||
**5 Lifecycle Hooks**: SessionStart → UserPromptSubmit → PostToolUse → Summary → SessionEnd
|
||||
|
||||
**Hooks** (`src/hooks/*.ts`) - TypeScript → ESM, built to `plugin/scripts/*-hook.js`
|
||||
**Hooks** (`src/hooks/*.ts`) - TypeScript hook logic compiled into the unified worker dispatcher (`plugin/scripts/worker-service.cjs`). Lifecycle hook entries in `plugin/hooks/hooks.json` invoke the worker via `bun-runner.js`. The Setup-phase `version-check.js` is the only standalone hook script.
|
||||
|
||||
**Worker Service** (`src/services/worker-service.ts`) - Express API on port 37777, Bun-managed, handles AI processing asynchronously
|
||||
**Worker Service** (`src/services/worker-service.ts`) - Express API on the per-user worker port (default `37700 + (uid % 100)`, configurable via `CLAUDE_MEM_WORKER_PORT`), Bun-managed, handles AI processing asynchronously
|
||||
|
||||
**Database** (`src/services/sqlite/`) - SQLite3 at `~/.claude-mem/claude-mem.db`
|
||||
|
||||
@@ -35,7 +35,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
|
||||
|
||||
**Chroma** (`src/services/sync/ChromaSync.ts`) - Vector embeddings for semantic search
|
||||
|
||||
**Viewer UI** (`src/ui/viewer/`) - React interface at http://localhost:37777, built to `plugin/ui/viewer.html`
|
||||
**Viewer UI** (`src/ui/viewer/`) - React interface served by the worker on its configured port (default `http://127.0.0.1:<worker-port>`), built to `plugin/ui/viewer.html`
|
||||
|
||||
## Privacy Tags
|
||||
- `<private>content</private>` - User-level privacy control (manual, prevents storage)
|
||||
@@ -70,7 +70,7 @@ Claude-mem supports running multiple isolated profiles on the same machine (e.g.
|
||||
|
||||
- **All paths and ports derive from these two env vars.** Hooks, npx-cli (`install`/`uninstall`/`start`/`search`), the OpenCode plugin, the OpenClaw installer, and the timeline-report skill all honor them. The settings file itself lives at `$CLAUDE_MEM_DATA_DIR/settings.json`.
|
||||
|
||||
- **Closes #2101.** See `src/shared/SettingsDefaultsManager.ts` for the canonical port/data-dir defaults and `plugin/skills/timeline-report/SKILL.md` for the shell snippet that resolves the port for arbitrary skills.
|
||||
- See `src/shared/SettingsDefaultsManager.ts` for the canonical port/data-dir defaults and `plugin/skills/timeline-report/SKILL.md` for the shell snippet that resolves the port for arbitrary skills.
|
||||
|
||||
## File Locations
|
||||
|
||||
@@ -110,14 +110,14 @@ Claude-mem is designed with a clean separation between open-source core function
|
||||
|
||||
**Open-Source Core** (this repository):
|
||||
|
||||
- All worker API endpoints on localhost:37777 remain fully open and accessible
|
||||
- All local worker HTTP API endpoints (per-user port — see Architecture above) remain fully open and accessible
|
||||
- Pro features are headless - no proprietary UI elements in this codebase
|
||||
- Pro integration points are minimal: settings for license keys, tunnel provisioning logic
|
||||
- The architecture ensures Pro features extend rather than replace core functionality
|
||||
|
||||
**Pro Features** (coming soon, external):
|
||||
|
||||
- Enhanced UI (Memory Stream) connects to the same localhost:37777 endpoints as the open viewer
|
||||
- Enhanced UI (Memory Stream) connects to the same local worker endpoints as the open viewer
|
||||
- Additional features like advanced filtering, timeline scrubbing, and search tools
|
||||
- Access gated by license validation, not by modifying core endpoints
|
||||
- Users without Pro licenses continue using the full open-source viewer UI without limitation
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
FROM ubuntu:24.04
|
||||
ARG NODE_VERSION=20
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TERM=xterm-256color
|
||||
ENV COLORTERM=truecolor
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
bash \
|
||||
git \
|
||||
build-essential \
|
||||
python3 \
|
||||
unzip \
|
||||
jq \
|
||||
sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN node -v && npm -v
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN cat > /root/.bashrc <<'EOF'
|
||||
export PS1='\[\033[1;36m\]cmem-test\[\033[0m\]:\[\033[1;33m\]\w\[\033[0m\]\$ '
|
||||
|
||||
cat <<'BANNER'
|
||||
=====================================================================
|
||||
claude-mem installer test sandbox (clean Linux, no Bun, no uv)
|
||||
=====================================================================
|
||||
|
||||
Try the new installer interactively:
|
||||
|
||||
node dist/npx-cli/index.js install --no-auto-start
|
||||
|
||||
Or just the runtime setup module via repair:
|
||||
|
||||
node dist/npx-cli/index.js repair
|
||||
|
||||
After install, verify the Setup hook is fast:
|
||||
|
||||
time node ~/.claude/plugins/cache/thedotmack/claude-mem/$(jq -r .version package.json)/scripts/version-check.js
|
||||
|
||||
Container HOME=/root is isolated — nothing here touches your real
|
||||
~/.claude or ~/.claude-mem. Exit with Ctrl-D.
|
||||
|
||||
=====================================================================
|
||||
BANNER
|
||||
EOF
|
||||
|
||||
# bash --login reads ~/.bash_profile (or ~/.profile), not ~/.bashrc, so
|
||||
# without this the banner above never runs in the container's CMD shell.
|
||||
RUN printf '%s\n' '[[ -f ~/.bashrc ]] && . ~/.bashrc' > /root/.bash_profile
|
||||
|
||||
CMD ["bash", "--login"]
|
||||
@@ -1,7 +1,2 @@
|
||||
[test]
|
||||
# Force each test file into its own worker process.
|
||||
# Prevents mock.module() calls (which are permanent within a worker)
|
||||
# from leaking across test files in parallel runs.
|
||||
# Note: smol=true increases test startup time by spawning one Bun process per file.
|
||||
# See: https://github.com/thedotmack/claude-mem/issues/1299
|
||||
smol = true
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# Ignore backup files created by sed
|
||||
*.bak
|
||||
|
||||
|
||||
@@ -1,23 +1,3 @@
|
||||
# Basic claude-mem container for ad-hoc testing.
|
||||
#
|
||||
# Base layout mirrors anthropics/claude-code .devcontainer
|
||||
# (https://github.com/anthropics/claude-code/blob/main/.devcontainer/Dockerfile):
|
||||
# FROM node:20, non-root `node` user, global npm install of @anthropic-ai/claude-code.
|
||||
# We skip the firewall/zsh/fzf/delta/git-hist noise since this image is for
|
||||
# exercising claude-mem, not as a full dev environment.
|
||||
#
|
||||
# On top of that base we install:
|
||||
# - Bun (claude-mem worker service runtime)
|
||||
# - uv (provides Python for Chroma per CLAUDE.md)
|
||||
# - The locally-built plugin/ tree at /opt/claude-mem
|
||||
#
|
||||
# Usage:
|
||||
# docker build -f docker/claude-mem/Dockerfile -t claude-mem:basic .
|
||||
# docker run --rm -it \
|
||||
# -v $(mktemp -d):/home/node/.claude-mem \
|
||||
# -e CLAUDE_MEM_CREDENTIALS_FILE=/auth/.credentials.json \
|
||||
# -v /path/to/extracted/creds.json:/auth/.credentials.json:ro \
|
||||
# claude-mem:basic
|
||||
|
||||
FROM node:20
|
||||
|
||||
@@ -36,56 +16,37 @@ RUN apt-get update \
|
||||
sqlite3 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Bun — system-wide so the unprivileged `node` user can execute it.
|
||||
# Pin via --build-arg BUN_VERSION=X.Y.Z; default is the version verified at PR time.
|
||||
ARG BUN_VERSION=1.3.12
|
||||
ENV BUN_INSTALL="/usr/local/bun"
|
||||
RUN curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" \
|
||||
&& chmod -R a+rX /usr/local/bun
|
||||
ENV PATH="/usr/local/bun/bin:${PATH}"
|
||||
|
||||
# uv — system-wide, for Chroma's Python runtime. Pin via --build-arg UV_VERSION=X.Y.Z.
|
||||
# Versioned installer URL per https://docs.astral.sh/uv/getting-started/installation/.
|
||||
ARG UV_VERSION=0.11.7
|
||||
ENV UV_INSTALL_DIR="/usr/local/bin"
|
||||
# `&&` binds tighter than `||` in bash, so the previous form let `curl|sh` fail
|
||||
# silently via the trailing `|| true`. Group the chmod so tolerated failure is
|
||||
# scoped to perms-fixing only.
|
||||
RUN set -eux \
|
||||
&& curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" | sh \
|
||||
&& { chmod a+rX /usr/local/bin/uv /usr/local/bin/uvx 2>/dev/null || true; }
|
||||
|
||||
# Match the upstream devcontainer's npm-global prefix so `npm install -g`
|
||||
# targets a dir the `node` user owns.
|
||||
RUN mkdir -p /usr/local/share/npm-global \
|
||||
&& chown -R node:node /usr/local/share/npm-global
|
||||
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
||||
ENV PATH="/usr/local/share/npm-global/bin:${PATH}"
|
||||
|
||||
# Claude Code CLI. Override at build-time with --build-arg CLAUDE_CODE_VERSION=X.Y.Z
|
||||
# to pin; default tracks latest.
|
||||
ARG CLAUDE_CODE_VERSION=latest
|
||||
USER node
|
||||
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
|
||||
|
||||
# Locally-built claude-mem plugin. COPY runs as root by default and layers are
|
||||
# cached, so put this after the npm install so iterating on the plugin doesn't
|
||||
# invalidate the CLI install layer.
|
||||
USER root
|
||||
COPY plugin/ /opt/claude-mem/
|
||||
RUN chown -R node:node /opt/claude-mem
|
||||
|
||||
# Persistent mount points for ad-hoc testing — mount a host dir at either of
|
||||
# these to inspect the claude-mem DB after a session.
|
||||
RUN mkdir -p /home/node/.claude /home/node/.claude-mem \
|
||||
&& chown -R node:node /home/node/.claude /home/node/.claude-mem
|
||||
|
||||
USER node
|
||||
WORKDIR /home/node
|
||||
|
||||
# Helper: copies OAuth creds out of the read-only mount into $HOME/.claude/
|
||||
# before exec'ing whatever you asked for. Saves the "cp + chmod" dance every
|
||||
# time you drop in.
|
||||
COPY --chown=node:node docker/claude-mem/entrypoint.sh /usr/local/bin/claude-mem-entrypoint
|
||||
RUN chmod +x /usr/local/bin/claude-mem-entrypoint
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the basic claude-mem Docker image from the current worktree.
|
||||
#
|
||||
# Usage:
|
||||
# docker/claude-mem/build.sh # builds claude-mem:basic
|
||||
# TAG=my-tag docker/claude-mem/build.sh # override the tag
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Entrypoint for the basic claude-mem container. Seeds OAuth creds if a
|
||||
# credentials file is mounted, then exec's whatever was passed (default: bash).
|
||||
#
|
||||
# Env vars:
|
||||
# CLAUDE_MEM_CREDENTIALS_FILE Path to a mounted OAuth credentials JSON file
|
||||
# (e.g. /auth/.credentials.json). Copied into
|
||||
# $HOME/.claude/.credentials.json at startup.
|
||||
# ANTHROPIC_API_KEY Standard API-key auth; set when OAuth isn't used.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -21,8 +13,6 @@ if [[ -n "${CLAUDE_MEM_CREDENTIALS_FILE:-}" ]]; then
|
||||
chmod 600 "$HOME/.claude/.credentials.json"
|
||||
fi
|
||||
|
||||
# Helpful one-liner for interactive users: run `claude` with the plugin dir
|
||||
# preconfigured. Don't force it — `exec "$@"` lets you override freely.
|
||||
export PATH="/usr/local/bun/bin:/usr/local/share/npm-global/bin:$PATH"
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drop into an interactive claude-mem container with OAuth creds + persistent
|
||||
# memory volume. For ad-hoc testing / poking around.
|
||||
#
|
||||
# Usage:
|
||||
# docker/claude-mem/run.sh
|
||||
# docker/claude-mem/run.sh claude --plugin-dir /opt/claude-mem --print "hi"
|
||||
#
|
||||
# On exit, the mounted .claude-mem/ dir on the host survives so you can inspect
|
||||
# the DB: `sqlite3 <HOST_MEM_DIR>/claude-mem.db 'select count(*) from observations'`.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -18,17 +9,12 @@ HOST_MEM_DIR="${HOST_MEM_DIR:-$REPO_ROOT/.docker-claude-mem-data}"
|
||||
mkdir -p "$HOST_MEM_DIR"
|
||||
echo "[run] host .claude-mem dir: $HOST_MEM_DIR" >&2
|
||||
|
||||
# Auth. Prefer OAuth (extracted from macOS Keychain / Linux creds file);
|
||||
# fall back to ANTHROPIC_API_KEY env.
|
||||
CREDS_FILE=""
|
||||
CREDS_MOUNT_ARGS=()
|
||||
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
|
||||
CREDS_FILE="$(mktemp -t claude-mem-creds.XXXXXX.json)"
|
||||
trap 'rm -f "$CREDS_FILE"' EXIT
|
||||
|
||||
# Try macOS Keychain first (primary storage on Darwin), then fall back to
|
||||
# the on-disk credentials file — some macOS setups (older CLI versions,
|
||||
# users who migrated machines) still have the file-only form.
|
||||
creds_obtained=0
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if security find-generic-password -s 'Claude Code-credentials' -w > "$CREDS_FILE" 2>/dev/null \
|
||||
@@ -55,14 +41,9 @@ else
|
||||
CREDS_MOUNT_ARGS=(-e ANTHROPIC_API_KEY)
|
||||
fi
|
||||
|
||||
# Pick -it only when a TTY is attached (keeps non-interactive callers working).
|
||||
# Initialize empty; expansion below safely omits args when the array is unset/empty.
|
||||
TTY_ARGS=()
|
||||
[[ -t 0 && -t 1 ]] && TTY_ARGS=(-it)
|
||||
|
||||
# NOT `exec` — we want the EXIT trap above to run and remove $CREDS_FILE
|
||||
# after the container exits. Running docker as a child keeps the shell
|
||||
# alive long enough for the trap to fire.
|
||||
docker run --rm ${TTY_ARGS[@]+"${TTY_ARGS[@]}"} \
|
||||
"${CREDS_MOUNT_ARGS[@]}" \
|
||||
-v "$HOST_MEM_DIR:/home/node/.claude-mem" \
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
| +-- handlers/ (context, session-init, observation, |
|
||||
| summarize, session-complete) |
|
||||
+-----------------------------------------------------------+
|
||||
| Worker Daemon (Express, port 37777) |
|
||||
| Worker Daemon (Express, per-user port 37700+(uid%100)) |
|
||||
| +-- SessionManager (session lifecycle) |
|
||||
| +-- SDKAgent (Claude Agent SDK) |
|
||||
| +-- SearchManager (search orchestration) |
|
||||
@@ -32,13 +32,15 @@
|
||||
|
||||
| Event | Handler | What it does | Timeout |
|
||||
|-------|---------|-------------|---------|
|
||||
| Setup | setup.sh | Install system dependencies | 300s |
|
||||
| SessionStart | smart-install.js + context | Install deps + start worker + inject context | 60s |
|
||||
| Setup | version-check.js | Sub-100ms version-marker check; prompts `npx claude-mem repair` on mismatch | 60s |
|
||||
| SessionStart | worker start + context | Start worker service and inject context | 60s |
|
||||
| UserPromptSubmit | session-init | Register session + start SDK agent + semantic injection | 60s |
|
||||
| PostToolUse | observation | Capture tool usage -> enqueue in worker | 120s |
|
||||
| Summary | summarize | Request session summary from SDK agent | 120s |
|
||||
| SessionEnd | session-complete | End session + drain pending messages | 30s |
|
||||
|
||||
On first install, `npx claude-mem install` sets up Bun and uv globally, runs `bun install` in the plugin cache, and writes an `.install-version` marker — all behind a visible clack spinner. The Setup hook then runs `version-check.js` on every Claude Code startup; if the plugin was upgraded externally (e.g. `claude plugin update`), it writes a hint to stderr asking the user to run `npx claude-mem repair`. The hook always exits 0 (non-blocking).
|
||||
|
||||
## Data Flow
|
||||
|
||||
```text
|
||||
@@ -62,27 +64,29 @@ Stop -> summarize -> /api/sessions/summarize
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### CLAIM-CONFIRM (PendingMessageStore)
|
||||
### Pending Queue (PendingMessageStore)
|
||||
|
||||
```text
|
||||
enqueue() -> INSERT status='pending'
|
||||
claimNextMessage() -> UPDATE status='processing' (atomic)
|
||||
confirmProcessed() -> DELETE (success)
|
||||
markFailed() -> UPDATE status='failed' (retry < 3)
|
||||
|
||||
Self-healing: messages in 'processing' for >60s reset to 'pending'
|
||||
enqueue() -> INSERT row with `pending` status
|
||||
clearPendingForSession() -> DELETE all pending rows for session
|
||||
(called whenever the parser returns
|
||||
a parseable response, regardless of
|
||||
whether observations were extracted)
|
||||
```
|
||||
|
||||
### Circuit-Breaker (SessionRoutes)
|
||||
Parser is binary: `{ valid: true, observations, summary }` or `{ valid: false }`.
|
||||
Unparseable responses leave the queue untouched and the session iterator continues.
|
||||
|
||||
### Generator restart loop (SessionRoutes)
|
||||
|
||||
```text
|
||||
Generator crash -> retry 1 (1s) -> retry 2 (2s) -> retry 3 (4s)
|
||||
-> consecutiveRestarts > 3 -> CIRCUIT-BREAKER
|
||||
-> markAllSessionMessagesAbandoned(sessionDbId)
|
||||
-> Stop. No infinite loop.
|
||||
-> consecutiveRestarts > 3 -> stop and let the iterator end
|
||||
```
|
||||
|
||||
Counter resets to 0 when generator completes work naturally.
|
||||
Counter resets to 0 when generator completes work naturally. Pending
|
||||
messages remain in the queue across restarts and are cleared by the
|
||||
parser path on the next valid response.
|
||||
|
||||
### Graceful Degradation (hook-command.ts)
|
||||
|
||||
@@ -117,7 +121,7 @@ The conversion between them is handled by SessionStore and is critical for FK co
|
||||
| observations | memory_session_id, type, title, narrative, content_hash | Tool usage observations |
|
||||
| session_summaries | memory_session_id, request, learned, completed | Session summaries |
|
||||
| user_prompts | content_session_id, prompt_text | User prompt history |
|
||||
| pending_messages | session_db_id, status, message_type | CLAIM-CONFIRM queue |
|
||||
| pending_messages | session_db_id, message_type | Per-session pending queue |
|
||||
| observation_feedback | observation_id, signal_type | Usage tracking |
|
||||
|
||||
### ChromaDB (chroma.sqlite3)
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* Claude Agent SDK V2 Examples
|
||||
*
|
||||
* The V2 API provides a session-based interface with separate send()/receive(),
|
||||
* ideal for multi-turn conversations. Run with: npx tsx v2-examples.ts
|
||||
*/
|
||||
|
||||
import {
|
||||
unstable_v2_createSession,
|
||||
@@ -32,7 +26,6 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Basic session with send/receive pattern
|
||||
async function basicSession() {
|
||||
console.log('=== Basic Session ===\n');
|
||||
|
||||
@@ -47,13 +40,11 @@ async function basicSession() {
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-turn conversation - V2's key advantage
|
||||
async function multiTurn() {
|
||||
console.log('=== Multi-Turn Conversation ===\n');
|
||||
|
||||
await using session = unstable_v2_createSession({ model: 'sonnet' });
|
||||
|
||||
// Turn 1
|
||||
await session.send('What is 5 + 3? Just the number.');
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
@@ -62,7 +53,6 @@ async function multiTurn() {
|
||||
}
|
||||
}
|
||||
|
||||
// Turn 2 - Claude remembers context
|
||||
await session.send('Multiply that by 2. Just the number.');
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
@@ -72,7 +62,6 @@ async function multiTurn() {
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot convenience function
|
||||
async function oneShot() {
|
||||
console.log('=== One-Shot Prompt ===\n');
|
||||
|
||||
@@ -84,13 +73,11 @@ async function oneShot() {
|
||||
}
|
||||
}
|
||||
|
||||
// Session resume - persist context across sessions
|
||||
async function sessionResume() {
|
||||
console.log('=== Session Resume ===\n');
|
||||
|
||||
let sessionId: string | undefined;
|
||||
|
||||
// First session - establish a memory
|
||||
{
|
||||
await using session = unstable_v2_createSession({ model: 'sonnet' });
|
||||
console.log('[Session 1] Telling Claude my favorite color...');
|
||||
@@ -110,7 +97,6 @@ async function sessionResume() {
|
||||
|
||||
console.log('--- Session closed. Time passes... ---\n');
|
||||
|
||||
// Resume and verify Claude remembers
|
||||
{
|
||||
await using session = unstable_v2_resumeSession(sessionId!, { model: 'sonnet' });
|
||||
console.log('[Session 2] Resuming and asking Claude...');
|
||||
|
||||
@@ -332,7 +332,6 @@ Block edits to sensitive files:
|
||||
* For troubleshooting steps and debugging techniques, see [Debugging](/en/hooks#debugging) in the hooks reference
|
||||
documentation.
|
||||
|
||||
|
||||
---
|
||||
|
||||
> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://code.claude.com/docs/llms.txt
|
||||
@@ -82,7 +82,6 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="#بداية-سريعة">بداية سريعة</a> •
|
||||
<a href="#كيف-يعمل">كيف يعمل</a> •
|
||||
|
||||
@@ -190,13 +190,16 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Setup": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/version-check.js",
|
||||
"timeout": 60
|
||||
}]
|
||||
}],
|
||||
"SessionStart": [{
|
||||
"matcher": "startup|clear|compact",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js",
|
||||
"timeout": 300
|
||||
}, {
|
||||
"type": "command",
|
||||
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs start",
|
||||
"timeout": 60
|
||||
@@ -246,9 +249,10 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
**Timing**: When user opens Claude Code or resumes session
|
||||
|
||||
**Hooks Triggered** (in order):
|
||||
1. `smart-install.js` - Ensures dependencies are installed
|
||||
2. `worker-service.cjs start` - Starts the worker service
|
||||
3. `context-hook.js` - Fetches and silently injects prior session context
|
||||
1. `worker-service.cjs start` - Starts the worker service
|
||||
2. `context-hook.js` - Fetches and silently injects prior session context
|
||||
|
||||
(Runtime setup is handled out-of-band by `npx claude-mem install` / `npx claude-mem repair`. The Setup phase runs a sub-100ms `version-check.js` that prompts the user to repair if the `.install-version` marker is stale.)
|
||||
|
||||
<Note>
|
||||
As of Claude Code 2.1.0 (ultrathink update), SessionStart hooks no longer display user-visible messages. Context is silently injected via `hookSpecificOutput.additionalContext`.
|
||||
|
||||
@@ -7,30 +7,29 @@ description: "System components and data flow in Claude-Mem"
|
||||
|
||||
## System Components
|
||||
|
||||
Claude-Mem operates as a Claude Code plugin with five core components:
|
||||
Claude-Mem operates as a Claude Code plugin with the following core components:
|
||||
|
||||
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
|
||||
1. **Plugin Hooks** - Lifecycle events (Setup version-check + 5 lifecycle hooks: SessionStart, UserPromptSubmit, PreToolUse for `Read`, PostToolUse, Stop)
|
||||
2. **Worker Service** - Express HTTP API on a per-user port; processes observations via the Claude Agent SDK (or Gemini / OpenRouter)
|
||||
3. **Database Layer** - SQLite + FTS5 (and optional Chroma for semantic search)
|
||||
4. **Search Tools** - HTTP API + the `mem-search` skill / MCP server for progressive disclosure search
|
||||
5. **Viewer UI** - React-based real-time memory stream served by the worker
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|------------------------|-------------------------------------------|
|
||||
| **Language** | TypeScript (ES2022, ESNext modules) |
|
||||
| **Runtime** | Node.js 18+ |
|
||||
| **Runtime** | Node.js 20+ and Bun ≥ 1.0 |
|
||||
| **Database** | SQLite 3 with bun:sqlite driver |
|
||||
| **Vector Store** | ChromaDB (optional, for semantic search) |
|
||||
| **HTTP Server** | Express.js 4.18 |
|
||||
| **Vector Store** | Chroma (optional, for semantic search) |
|
||||
| **HTTP Server** | Express.js 5 |
|
||||
| **Real-time** | Server-Sent Events (SSE) |
|
||||
| **UI Framework** | React + TypeScript |
|
||||
| **AI SDK** | @anthropic-ai/claude-agent-sdk |
|
||||
| **AI SDK** | @anthropic-ai/claude-agent-sdk (or Gemini / OpenRouter) |
|
||||
| **Build Tool** | esbuild (bundles TypeScript) |
|
||||
| **Process Manager** | Bun |
|
||||
| **Testing** | Node.js built-in test runner |
|
||||
| **Testing** | `bun test` |
|
||||
|
||||
## Data Flow
|
||||
|
||||
@@ -63,13 +62,13 @@ Uses 3-layer progressive disclosure: search → timeline → get_observations
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 0. Smart Install Pre-Hook Fires │
|
||||
│ Checks dependencies (cached), only runs on version changes │
|
||||
│ Not a lifecycle hook - runs before context-hook starts │
|
||||
│ 0. Setup Hook Fires (version-check.js) │
|
||||
│ Sub-100ms read of .install-version; on mismatch prints │
|
||||
│ "run: npx claude-mem repair" to stderr. Always exits 0. │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. Session Starts → Context Hook Fires │
|
||||
│ 1. Session Starts → Worker-start, then Context Hook │
|
||||
│ Starts Bun worker if needed, injects context from previous │
|
||||
│ sessions (configurable observation count) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
@@ -106,99 +105,60 @@ Uses 3-layer progressive disclosure: search → timeline → get_observations
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── 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
|
||||
│ │ └── hook-response.ts # Hook response utilities
|
||||
│ │
|
||||
│ ├── hooks/ # TypeScript hook implementations (built via esbuild)
|
||||
│ ├── sdk/ # Claude Agent SDK integration
|
||||
│ │ ├── prompts.ts # XML prompt builders
|
||||
│ │ ├── parser.ts # XML response parser
|
||||
│ │ └── worker.ts # Main SDK agent loop
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── worker-service.ts # Express HTTP + SSE service
|
||||
│ │ └── sqlite/ # Database layer
|
||||
│ │ ├── SessionStore.ts # CRUD operations
|
||||
│ │ ├── SessionSearch.ts # FTS5 search service
|
||||
│ │ ├── migrations.ts
|
||||
│ │ └── types.ts
|
||||
│ │
|
||||
│ ├── ui/ # Viewer UI
|
||||
│ │ └── viewer/ # React + TypeScript web interface
|
||||
│ │ ├── components/ # UI components
|
||||
│ │ ├── hooks/ # React hooks
|
||||
│ │ ├── utils/ # Utilities
|
||||
│ │ └── assets/ # Fonts, logos
|
||||
│ │
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ │ ├── config.ts
|
||||
│ │ ├── paths.ts
|
||||
│ │ └── storage.ts
|
||||
│ │
|
||||
│ └── utils/
|
||||
│ ├── logger.ts
|
||||
│ ├── platform.ts
|
||||
│ └── port-allocator.ts
|
||||
│ │ ├── worker-service.ts # Express HTTP + SSE service (worker entry point)
|
||||
│ │ ├── sync/ChromaSync.ts # Optional Chroma vector index
|
||||
│ │ └── sqlite/ # SQLite + FTS5 storage layer
|
||||
│ ├── ui/viewer/ # React + TypeScript web viewer
|
||||
│ ├── shared/ # Shared utilities (paths, settings defaults)
|
||||
│ └── utils/ # Logging, platform, tag-stripping helpers
|
||||
│
|
||||
├── scripts/ # Build and utility scripts
|
||||
│ └── smart-install.js # Cached dependency checker (pre-hook)
|
||||
├── scripts/ # Build + utility scripts
|
||||
│
|
||||
├── plugin/ # Plugin distribution
|
||||
│ ├── .claude-plugin/
|
||||
│ │ └── plugin.json
|
||||
│ ├── hooks/
|
||||
│ │ └── hooks.json
|
||||
├── plugin/ # Plugin distribution (synced to marketplace)
|
||||
│ ├── .claude-plugin/plugin.json
|
||||
│ ├── hooks/hooks.json # Hook registration (Setup + 5 lifecycle hooks)
|
||||
│ ├── scripts/ # Built executables
|
||||
│ │ ├── context-hook.js
|
||||
│ │ ├── user-message-hook.js
|
||||
│ │ ├── new-hook.js
|
||||
│ │ ├── save-hook.js
|
||||
│ │ ├── summary-hook.js
|
||||
│ │ ├── cleanup-hook.js
|
||||
│ │ └── worker-service.cjs # Background worker + HTTP API
|
||||
│ │
|
||||
│ ├── skills/ # Agent skills (v5.4.0+)
|
||||
│ │ ├── mem-search/ # Search skill with progressive disclosure (v5.5.0)
|
||||
│ │ │ ├── SKILL.md # Skill frontmatter (~250 tokens)
|
||||
│ │ │ ├── operations/ # 12 detailed operation docs
|
||||
│ │ │ └── principles/ # 2 principle guides
|
||||
│ │ ├── troubleshoot/ # Troubleshooting skill
|
||||
│ │ │ ├── SKILL.md
|
||||
│ │ │ └── operations/ # 6 operation docs
|
||||
│ │ └── version-bump/ # Version management skill (deprecated)
|
||||
│ │
|
||||
│ └── ui/ # Built viewer UI
|
||||
│ └── viewer.html # Self-contained bundle
|
||||
│ │ ├── version-check.js # Setup-phase marker check (sub-100ms)
|
||||
│ │ ├── bun-runner.js # Resolves Bun and runs worker-service.cjs
|
||||
│ │ ├── worker-service.cjs # Worker daemon + lifecycle hook dispatcher
|
||||
│ │ ├── worker-cli.js # CLI shim
|
||||
│ │ ├── worker-wrapper.cjs # Process wrapper
|
||||
│ │ ├── mcp-server.cjs # MCP search server
|
||||
│ │ ├── statusline-counts.js
|
||||
│ │ └── context-generator.cjs
|
||||
│ ├── skills/ # Agent skills (mem-search, make-plan, do, etc.)
|
||||
│ └── ui/viewer.html # Self-contained React bundle
|
||||
│
|
||||
├── tests/ # Test suite
|
||||
├── docs/ # Documentation
|
||||
└── ecosystem.config.cjs # Process configuration (deprecated)
|
||||
├── tests/ # Test suite (`bun test`)
|
||||
├── docs/ # Mintlify documentation
|
||||
└── openclaw/ # OpenClaw integration plugin
|
||||
```
|
||||
|
||||
## Component Details
|
||||
|
||||
### 1. Plugin Hooks (6 Hooks)
|
||||
- **context-hook.js** - SessionStart: Starts Bun worker, injects context
|
||||
- **user-message-hook.js** - UserMessage: Debugging hook
|
||||
- **new-hook.js** - UserPromptSubmit: Creates session, saves prompt
|
||||
- **save-hook.js** - PostToolUse: Captures tool executions
|
||||
- **summary-hook.js** - Stop: Generates session summary
|
||||
- **cleanup-hook.js** - SessionEnd: Marks session complete
|
||||
### 1. Plugin Hooks
|
||||
|
||||
**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.
|
||||
The plugin registers a Setup-phase `version-check.js` plus five lifecycle hooks. Each lifecycle event invokes `bun-runner.js` to spawn `worker-service.cjs` with a `hook claude-code <event>` argument; the worker process is the single dispatcher for all hook logic. Events:
|
||||
|
||||
- **Setup** → `version-check.js` (sub-100ms marker check; never installs anything)
|
||||
- **SessionStart** → start worker, then `hook claude-code context` (context injection)
|
||||
- **UserPromptSubmit** → `hook claude-code session-init`
|
||||
- **PreToolUse** (matcher `Read`) → `hook claude-code file-context`
|
||||
- **PostToolUse** (matcher `*`) → `hook claude-code observation`
|
||||
- **Stop** → `hook claude-code summarize` (summary generation)
|
||||
|
||||
The actual runtime install (Bun, uv, `bun install`) is performed by `npx claude-mem install` / `npx claude-mem repair` with a visible installer spinner; the Setup hook itself only reads the `.install-version` marker.
|
||||
|
||||
See [Plugin Hooks](/architecture/hooks) for detailed hook documentation.
|
||||
|
||||
### 2. Worker Service
|
||||
Express.js HTTP server on port 37777 (configurable) with:
|
||||
- 10 search HTTP API endpoints (v5.4.0+)
|
||||
- 8 viewer UI HTTP/SSE endpoints
|
||||
- Async observation processing via Claude Agent SDK
|
||||
Express.js HTTP server on a per-user port (default `37700 + (uid % 100)`, override via `CLAUDE_MEM_WORKER_PORT`) with:
|
||||
- Search HTTP API endpoints
|
||||
- Viewer UI HTTP/SSE endpoints
|
||||
- Async observation processing via the Claude Agent SDK (or Gemini / OpenRouter)
|
||||
- Real-time updates via Server-Sent Events
|
||||
- Auto-managed by Bun
|
||||
|
||||
@@ -230,7 +190,7 @@ Skill-based search with progressive disclosure providing 10 search operations:
|
||||
See [Search Architecture](/architecture/search-architecture) for technical details and examples.
|
||||
|
||||
### 5. Viewer UI
|
||||
React + TypeScript web interface at http://localhost:37777 featuring:
|
||||
React + TypeScript web interface served by the worker on its configured port (default `http://127.0.0.1:<worker-port>`) featuring:
|
||||
- Real-time memory stream via Server-Sent Events
|
||||
- Infinite scroll pagination with automatic deduplication
|
||||
- Project filtering and settings persistence
|
||||
|
||||
@@ -12,10 +12,10 @@ The worker service is a long-running HTTP API built with Express.js and managed
|
||||
- **Technology**: Express.js HTTP server
|
||||
- **Runtime**: Bun (auto-installed if missing)
|
||||
- **Process Manager**: Native Bun process management via ProcessManager
|
||||
- **Port**: Fixed port 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
|
||||
- **Port**: Per-user default `37700 + (uid % 100)` (override with `CLAUDE_MEM_WORKER_PORT`). The active port is stored in `~/.claude-mem/settings.json` and reported by `GET /api/health`.
|
||||
- **Location**: `src/services/worker-service.ts`
|
||||
- **Built Output**: `plugin/scripts/worker-service.cjs`
|
||||
- **Model**: Configurable via `CLAUDE_MEM_MODEL` environment variable (default: sonnet)
|
||||
- **Model**: Configurable via `CLAUDE_MEM_MODEL` (default: `claude-haiku-4-5-20251001`)
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
@@ -51,10 +51,12 @@ GET /health
|
||||
{
|
||||
"status": "ok",
|
||||
"uptime": 12345,
|
||||
"port": 37777
|
||||
"port": 37742
|
||||
}
|
||||
```
|
||||
|
||||
The `port` value is the actual worker port for the current user — per-user default `37700 + (uid % 100)`, or whatever `CLAUDE_MEM_WORKER_PORT` is set to. The example above is illustrative; your value will differ.
|
||||
|
||||
#### 3. Server-Sent Events Stream
|
||||
```
|
||||
GET /stream
|
||||
@@ -612,7 +614,7 @@ The worker service auto-starts when the SessionStart hook fires. Manual start is
|
||||
|
||||
### Bun Requirement
|
||||
|
||||
Bun is required to run the worker service. If Bun is not installed, the smart-install script will automatically install it on first run:
|
||||
Bun is required to run the worker service. If Bun is not installed, `npx claude-mem install` (and `npx claude-mem repair`) installs it globally during setup, with a visible clack spinner:
|
||||
|
||||
- **Windows**: `powershell -c "irm bun.sh/install.ps1 | iex"`
|
||||
- **macOS/Linux**: `curl -fsSL https://bun.sh/install | bash`
|
||||
@@ -640,26 +642,26 @@ The worker service routes observations to the Claude Agent SDK for AI-powered pr
|
||||
|
||||
### Model Configuration
|
||||
|
||||
Set the AI model used for processing via environment variable:
|
||||
Set the Claude model used for compression via environment variable or `~/.claude-mem/settings.json`:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_MODEL=sonnet
|
||||
export CLAUDE_MEM_MODEL=claude-haiku-4-5-20251001
|
||||
```
|
||||
|
||||
Available shorthand models (forward to latest version):
|
||||
- `haiku` - Fast, cost-efficient
|
||||
- `sonnet` - Balanced (default)
|
||||
- `opus` - Most capable
|
||||
Allowed values:
|
||||
- `claude-haiku-4-5-20251001` - default, fast and cheap (best for compression)
|
||||
- `claude-sonnet-4-6` - balanced quality and cost
|
||||
- `claude-opus-4-7` - highest quality, most expensive
|
||||
|
||||
## Port Allocation
|
||||
|
||||
The worker uses a fixed port (37777 by default) for consistent communication:
|
||||
The worker uses a per-user default port so different OS users on the same machine never collide:
|
||||
|
||||
- **Default**: Port 37777
|
||||
- **Override**: Set `CLAUDE_MEM_WORKER_PORT` environment variable
|
||||
- **Port File**: `${CLAUDE_PLUGIN_ROOT}/data/worker.port` tracks current port
|
||||
- **Default**: `37700 + (uid % 100)` (set in `src/shared/SettingsDefaultsManager.ts`)
|
||||
- **Override**: Set `CLAUDE_MEM_WORKER_PORT` (env or `~/.claude-mem/settings.json`)
|
||||
- **Discovery**: `GET /api/health` returns the active port; `~/.claude-mem/settings.json` stores the configured value
|
||||
|
||||
If port 37777 is in use, the worker will fail to start. Set a custom port via environment variable.
|
||||
If the chosen port is occupied, the worker fails to start — pin a different port via `CLAUDE_MEM_WORKER_PORT` and restart.
|
||||
|
||||
## Data Storage
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ Claude-Mem offers a beta channel for users who want to try experimental features
|
||||
|
||||
## Version Channel Switching
|
||||
|
||||
You can switch between stable and beta versions directly from the web viewer UI at http://localhost:37777.
|
||||
You can switch between stable and beta versions directly from the web viewer UI (the worker prints its URL on startup; default `http://127.0.0.1:<worker-port>`).
|
||||
|
||||
### How to Access
|
||||
|
||||
1. Open the Claude-Mem viewer at http://localhost:37777
|
||||
1. Open the Claude-Mem viewer (the worker prints its URL on startup)
|
||||
2. Click the **Settings** gear icon in the top-right
|
||||
3. Find the **Version Channel** section
|
||||
4. Click **Try Beta (Endless Mode)** to switch to beta, or **Switch to Stable** to return
|
||||
|
||||
@@ -13,12 +13,13 @@ Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created
|
||||
|
||||
| Setting | Default | Description |
|
||||
|-------------------------------|---------------------------------|---------------------------------------|
|
||||
| `CLAUDE_MEM_MODEL` | `sonnet` | AI model for processing observations (when using Claude) |
|
||||
| `CLAUDE_MEM_MODEL` | `claude-haiku-4-5-20251001` | Claude model used to compress observations (when using the Claude provider) |
|
||||
| `CLAUDE_MEM_PROVIDER` | `claude` | AI provider: `claude`, `gemini`, or `openrouter` |
|
||||
| `CLAUDE_MEM_MODE` | `code` | Active mode profile (e.g., `code--es`, `email-investigation`) |
|
||||
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject |
|
||||
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
|
||||
| `CLAUDE_MEM_WORKER_PORT` | `37700 + (uid % 100)` | Worker service port (per-user default; override for fixed port) |
|
||||
| `CLAUDE_MEM_WORKER_HOST` | `127.0.0.1` | Worker service host address |
|
||||
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem` | Data root — every other path (database, chroma, logs, settings.json, worker.pid) derives from this |
|
||||
| `CLAUDE_MEM_SKIP_TOOLS` | `ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion` | Comma-separated tools to exclude from observations |
|
||||
|
||||
### Gemini Provider Settings
|
||||
@@ -54,23 +55,19 @@ See [OpenRouter Provider](usage/openrouter-provider) for detailed configuration,
|
||||
|
||||
## Model Configuration
|
||||
|
||||
Configure which AI model processes your observations.
|
||||
Configure which Claude model compresses your observations (only applies when `CLAUDE_MEM_PROVIDER=claude`).
|
||||
|
||||
### Available Models
|
||||
|
||||
Shorthand model names automatically forward to the latest version:
|
||||
| Value | Notes |
|
||||
|-------|-------|
|
||||
| `claude-haiku-4-5-20251001` | Default — fast and cheap, ideal for compression |
|
||||
| `claude-sonnet-4-6` | Balanced quality and cost |
|
||||
| `claude-opus-4-7` | Highest quality, most expensive |
|
||||
|
||||
- `haiku` - Fast, cost-efficient
|
||||
- `sonnet` - Balanced (default)
|
||||
- `opus` - Most capable
|
||||
### Picking via the Installer
|
||||
|
||||
### Using the Interactive Script
|
||||
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
This script manages settings in `~/.claude-mem/settings.json`.
|
||||
`npx claude-mem install` prompts for the Claude model (when the Claude provider is selected) and persists the choice to `~/.claude-mem/settings.json`.
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
@@ -78,7 +75,7 @@ Edit `~/.claude-mem/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"CLAUDE_MEM_MODEL": "sonnet"
|
||||
"CLAUDE_MEM_MODEL": "claude-haiku-4-5-20251001"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -119,8 +116,8 @@ The data directory location depends on the environment:
|
||||
```
|
||||
~/.claude-mem/
|
||||
├── claude-mem.db # SQLite database
|
||||
├── .install-version # Cached version for smart installer
|
||||
├── worker.port # Current worker port file
|
||||
├── .install-version # Version marker written by `npx claude-mem install`/`repair`
|
||||
├── settings.json # Worker port + provider/model settings
|
||||
└── logs/
|
||||
├── worker-out.log # Worker stdout logs
|
||||
└── worker-error.log # Worker stderr logs
|
||||
@@ -136,7 +133,7 @@ ${CLAUDE_PLUGIN_ROOT}/
|
||||
├── hooks/
|
||||
│ └── hooks.json # Hook configuration
|
||||
├── scripts/ # Built executables
|
||||
│ ├── smart-install.js # Smart installer script
|
||||
│ ├── version-check.js # Sub-100ms Setup-hook version marker check
|
||||
│ ├── context-hook.js # Context injection hook
|
||||
│ ├── new-hook.js # Session creation hook
|
||||
│ ├── save-hook.js # Observation capture hook
|
||||
@@ -151,44 +148,16 @@ ${CLAUDE_PLUGIN_ROOT}/
|
||||
|
||||
### Hooks Configuration
|
||||
|
||||
Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
Hooks are registered in `plugin/hooks/hooks.json`. The current shape uses a single dispatcher (`worker-service.cjs hook claude-code <event>`) launched through `bun-runner.js`, plus a fast Setup-phase `version-check.js`. The events wired up are:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Claude-mem memory system hooks",
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"UserPromptSubmit": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
- `Setup` → `version-check.js` (sub-100ms `.install-version` check)
|
||||
- `SessionStart` → start the worker, then `hook claude-code context` (context injection)
|
||||
- `UserPromptSubmit` → `hook claude-code session-init`
|
||||
- `PreToolUse` (matcher `Read`) → `hook claude-code file-context`
|
||||
- `PostToolUse` (matcher `*`) → `hook claude-code observation`
|
||||
- `Stop` → `hook claude-code summarize`
|
||||
|
||||
The exact `hooks.json` entries are written by the installer; do not hand-edit them in the marketplace copy unless you know what you're doing.
|
||||
|
||||
### Search Configuration
|
||||
|
||||
@@ -198,7 +167,7 @@ Claude-Mem provides MCP search tools for querying your project history.
|
||||
|
||||
Search operations are provided via:
|
||||
- **MCP Server**: 3 tools (search, timeline, get_observations) with progressive disclosure
|
||||
- **HTTP API**: 10 endpoints on worker service port 37777
|
||||
- **HTTP API**: 10 endpoints on the worker service port (per-user, default `37700 + (uid % 100)`; see `~/.claude-mem/settings.json`)
|
||||
- **Auto-Invocation**: Claude recognizes natural language queries about past work
|
||||
|
||||
## Version Channel
|
||||
@@ -207,7 +176,7 @@ Claude-Mem supports switching between stable and beta versions via the web viewe
|
||||
|
||||
### Accessing Version Channel
|
||||
|
||||
1. Open the viewer at http://localhost:37777
|
||||
1. Open the viewer at the worker URL (default `http://127.0.0.1:<worker-port>`; the active port is the value of `CLAUDE_MEM_WORKER_PORT` in `~/.claude-mem/settings.json`)
|
||||
2. Click the Settings gear icon
|
||||
3. Find the **Version Channel** section
|
||||
|
||||
@@ -243,7 +212,7 @@ Claude-Mem injects past observations into each new session, giving Claude awaren
|
||||
|
||||
### Context Settings Modal
|
||||
|
||||
Access the settings modal from the web viewer at http://localhost:37777:
|
||||
Access the settings modal from the web viewer (the worker prints its URL on startup; default is `http://127.0.0.1:<worker-port>`):
|
||||
|
||||
1. Click the **gear icon** in the header
|
||||
2. Adjust settings in the right panel
|
||||
@@ -315,7 +284,7 @@ Token economics help you understand the value of cached observations vs. re-read
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| **Model** | sonnet | AI model for generating observations |
|
||||
| **Worker Port** | 37777 | Port for background worker service |
|
||||
| **Worker Port** | `37700 + (uid % 100)` | Port for background worker service (override with `CLAUDE_MEM_WORKER_PORT`) |
|
||||
| **MCP search server** | true | Enable Model Context Protocol search tools |
|
||||
| **Include last summary** | false | Add previous session's summary to context |
|
||||
| **Include last message** | false | Add previous session's final message |
|
||||
@@ -340,7 +309,7 @@ Settings are stored in `~/.claude-mem/settings.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The Context Settings Modal (at http://localhost:37777) is the recommended way to configure these settings, as it provides live preview of changes.
|
||||
**Note**: The Context Settings Modal (in the web viewer) is the recommended way to configure these settings, as it provides live preview of changes.
|
||||
|
||||
## Customization
|
||||
|
||||
@@ -411,22 +380,16 @@ Changes take effect on the next tool execution (no worker restart needed).
|
||||
|
||||
### Hook Timeouts
|
||||
|
||||
Modify timeouts in `plugin/hooks/hooks.json`:
|
||||
Hook timeouts are written into `plugin/hooks/hooks.json` by the installer. The current defaults match the shape of the workload at each lifecycle stage:
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout": 120 // Default: 120 seconds
|
||||
}
|
||||
```
|
||||
|
||||
Recommended values:
|
||||
- SessionStart: 120s (needs time for smart install check and context retrieval)
|
||||
- Setup (`version-check.js`): 300s ceiling but normally < 100ms — only reads `.install-version`
|
||||
- SessionStart (worker-start + context): 60s
|
||||
- UserPromptSubmit: 60s
|
||||
- PostToolUse: 120s (can process many observations)
|
||||
- Stop: 60s
|
||||
- SessionEnd: 60s
|
||||
- PreToolUse (file-context, Read matcher): 60s
|
||||
- PostToolUse (observation): 120s
|
||||
- Stop (summary): 120s
|
||||
|
||||
**Note**: With smart install caching (v5.0.3+), SessionStart is typically very fast (10ms) unless dependencies need installation.
|
||||
The Setup hook never installs anything — runtime install (Bun, uv, `bun install`) happens in `npx claude-mem install` / `npx claude-mem repair` outside the session lifecycle.
|
||||
|
||||
### Worker Memory Limit
|
||||
|
||||
@@ -472,16 +435,15 @@ npm run worker:logs
|
||||
|
||||
### Invalid Model Name
|
||||
|
||||
If you specify an invalid model name, the worker will fall back to `sonnet` and log a warning.
|
||||
If you specify an invalid Claude model name, the worker logs a warning and uses the default. Valid Claude models for `CLAUDE_MEM_MODEL`:
|
||||
|
||||
Valid shorthand models (forward to latest version):
|
||||
- haiku
|
||||
- sonnet
|
||||
- opus
|
||||
- `claude-haiku-4-5-20251001` (default)
|
||||
- `claude-sonnet-4-6`
|
||||
- `claude-opus-4-7`
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 37777 is already in use:
|
||||
The default worker port is `37700 + (uid % 100)`, so different OS users on the same machine get different ports automatically. If you still hit a collision (e.g. running multiple profiles as the same UID), set a fixed port:
|
||||
|
||||
1. Set custom port:
|
||||
```bash
|
||||
@@ -495,7 +457,7 @@ If port 37777 is already in use:
|
||||
|
||||
3. Verify new port:
|
||||
```bash
|
||||
cat ~/.claude-mem/worker.port
|
||||
curl -s http://127.0.0.1:$CLAUDE_MEM_WORKER_PORT/api/health | jq .port
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
@@ -39,7 +39,7 @@ The build process uses esbuild to compile TypeScript:
|
||||
|
||||
**Build Output**:
|
||||
- Hook executables: `*-hook.js` (ESM format)
|
||||
- Smart installer: `smart-install.js` (ESM format)
|
||||
- Setup version-check: `version-check.js` (ESM format, sub-100ms)
|
||||
- Worker service: `worker-service.cjs` (CJS format)
|
||||
- MCP server: `mcp-server.cjs` (CJS format)
|
||||
- Viewer UI: `viewer.html` (self-contained HTML bundle)
|
||||
@@ -320,7 +320,7 @@ npm test
|
||||
export function buildObservationPrompt(observation: Observation): string {
|
||||
return `
|
||||
<observation>
|
||||
<!-- Add new XML structure -->
|
||||
|
||||
</observation>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ Claude Code's hook system provides exactly what we need:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Lifecycle Events" icon="clock">
|
||||
SessionStart, UserPromptSubmit, PostToolUse, Stop
|
||||
SessionStart, UserPromptSubmit, PreToolUse (Read), PostToolUse, Stop, SessionEnd
|
||||
</Card>
|
||||
|
||||
<Card title="Non-Blocking" icon="forward">
|
||||
@@ -74,34 +74,31 @@ Claude Code's hook system provides exactly what we need:
|
||||
|
||||
## The Hook Scripts
|
||||
|
||||
Claude-Mem uses lifecycle hook scripts across 5 lifecycle events. SessionStart runs 3 hooks in sequence: smart-install, worker-service start, and context-hook.
|
||||
Claude-Mem uses lifecycle hook scripts across 5 lifecycle events. Runtime setup is handled out-of-band by `npx claude-mem install` (and `npx claude-mem repair`); the Setup hook only runs a sub-100ms `version-check.js` to flag stale installs. SessionStart runs 2 hooks in sequence: worker-service start, then context-hook.
|
||||
|
||||
### Pre-Hook: Smart Install (Before SessionStart)
|
||||
### Setup Hook: Version Check
|
||||
|
||||
**Purpose:** Intelligently manage dependencies and start worker service
|
||||
**Purpose:** Detect stale installs caused by external plugin upgrades and prompt the user to repair.
|
||||
|
||||
**Note:** This is NOT a lifecycle hook - it's a pre-hook script executed via command chaining before context-hook runs.
|
||||
**Note:** Runtime installation (Bun, uv, `bun install` in the plugin cache) is performed by `npx claude-mem install` and `npx claude-mem repair` — the Setup hook itself never installs anything.
|
||||
|
||||
**When:** Claude Code starts (startup, clear, or compact)
|
||||
**When:** Claude Code Setup phase, before every session.
|
||||
|
||||
**What it does:**
|
||||
1. Checks if dependencies need installation (version marker)
|
||||
2. Only runs `npm install` when necessary:
|
||||
- First-time installation
|
||||
- Version changed in package.json
|
||||
3. Provides Windows-specific error messages
|
||||
4. Starts Bun worker service
|
||||
1. Reads the `.install-version` marker written by the npx installer.
|
||||
2. Compares it against the currently loaded plugin version.
|
||||
3. On mismatch, writes `run: npx claude-mem repair` to stderr.
|
||||
4. Always exits 0 (non-blocking, sub-100ms).
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"matcher": "startup|clear|compact",
|
||||
"Setup": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/../scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 300
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/version-check.js",
|
||||
"timeout": 60
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -109,14 +106,11 @@ Claude-Mem uses lifecycle hook scripts across 5 lifecycle events. SessionStart r
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- ✅ Version caching (`.install-version` file)
|
||||
- ✅ Fast when already installed (~10ms vs 2-5 seconds)
|
||||
- ✅ Cross-platform compatible
|
||||
- ✅ Helpful Windows error messages for build tools
|
||||
- ✅ Sub-100ms version-marker check (no I/O beyond reading the marker)
|
||||
- ✅ Always exit 0 — never blocks a session
|
||||
- ✅ Clear repair instructions on stderr when the plugin was upgraded externally (e.g. `claude plugin update`)
|
||||
|
||||
**v5.0.3 Enhancement:** Smart caching eliminates redundant installs
|
||||
|
||||
**Source:** `scripts/smart-install.js`
|
||||
**Source:** `scripts/version-check.js`. The matching installer logic lives in `npx claude-mem install` / `npx claude-mem repair`, which install Bun + uv globally, run `bun install` in the plugin cache, and write the `.install-version` marker — all behind a visible clack spinner.
|
||||
|
||||
---
|
||||
|
||||
@@ -124,7 +118,7 @@ Claude-Mem uses lifecycle hook scripts across 5 lifecycle events. SessionStart r
|
||||
|
||||
**Purpose:** Inject relevant context from previous sessions
|
||||
|
||||
**When:** Claude Code starts (runs after smart-install pre-hook)
|
||||
**When:** Claude Code starts (runs after the worker-start SessionStart entry)
|
||||
|
||||
**What it does:**
|
||||
1. Extracts project name from current working directory
|
||||
@@ -429,7 +423,7 @@ sequenceDiagram
|
||||
|
||||
| Event | Timing | Blocking | Timeout | Output Handling |
|
||||
|-------|--------|----------|---------|-----------------|
|
||||
| **SessionStart (smart-install)** | Before session | No | 300s | stderr (log only) |
|
||||
| **Setup (version-check)** | Before session | No | 60s | stderr hint on stale install (always exit 0) |
|
||||
| **SessionStart (worker-start)** | Before session | No | 60s | stderr (log only) |
|
||||
| **SessionStart (context)** | Before session | No | 60s | JSON → additionalContext (silent) |
|
||||
| **UserPromptSubmit** | Before processing | No | 60s | stdout → context |
|
||||
@@ -501,7 +495,7 @@ npm run worker:stop
|
||||
|
||||
### Worker HTTP API
|
||||
|
||||
**Technology:** Express.js REST API on port 37777
|
||||
**Technology:** Express.js REST API on the worker's per-user port (default `37700 + (uid % 100)`, override via `CLAUDE_MEM_WORKER_PORT`)
|
||||
|
||||
**Endpoints:**
|
||||
|
||||
@@ -692,23 +686,18 @@ claude --debug
|
||||
|
||||
| Hook | Average | p95 | p99 |
|
||||
|------|---------|-----|-----|
|
||||
| SessionStart (smart-install, cached) | 10ms | 20ms | 40ms |
|
||||
| SessionStart (smart-install, first run) | 2500ms | 5000ms | 8000ms |
|
||||
| Setup (version-check, marker matches) | 8ms | 20ms | 40ms |
|
||||
| Setup (version-check, marker mismatch — stderr hint, still non-blocking) | 10ms | 25ms | 50ms |
|
||||
| SessionStart (context) | 45ms | 120ms | 250ms |
|
||||
| SessionStart (user-message) | 5ms | 10ms | 15ms |
|
||||
| UserPromptSubmit | 12ms | 25ms | 50ms |
|
||||
| PostToolUse | 8ms | 15ms | 30ms |
|
||||
| SessionEnd | 5ms | 10ms | 20ms |
|
||||
|
||||
**Why smart-install is sometimes slow:**
|
||||
- First-time: Full npm install (2-5 seconds)
|
||||
- Cached: Version check only (~10ms)
|
||||
- Version change: Full npm install + worker restart
|
||||
|
||||
**Optimization (v5.0.3):**
|
||||
- Version caching with `.install-version` marker
|
||||
- Only install on version change or missing deps
|
||||
- Windows-specific error messages with build tool help
|
||||
**Why the Setup hook stays fast:**
|
||||
- The Setup hook only reads the `.install-version` marker — no `npm install`, no spawned subprocesses.
|
||||
- All heavy lifting (Bun + uv install, `bun install` inside the plugin cache) happens in `npx claude-mem install` / `npx claude-mem repair`, which run with a visible clack spinner outside the session lifecycle.
|
||||
- On marker mismatch the hook prints a one-line `run: npx claude-mem repair` hint to stderr and exits 0; the user opts into the slow path explicitly.
|
||||
|
||||
### Database Performance
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ npx claude-mem install
|
||||
```
|
||||
|
||||
The interactive installer will:
|
||||
- Detect your installed IDEs (Claude Code, Cursor, Gemini CLI, Windsurf, etc.)
|
||||
- Copy plugin files to the correct locations
|
||||
- Register the plugin with Claude Code
|
||||
- Install all dependencies (including Bun and uv)
|
||||
- Run a runtime check (auto-installs Bun and uv if missing)
|
||||
- Detect your installed IDEs (Claude Code, Cursor, Gemini CLI, Windsurf, OpenCode, Codex CLI) and let you multi-select which ones to wire up
|
||||
- Offer to install Claude Code if it isn't found
|
||||
- Prompt for an LLM provider (Claude Code auth, Gemini API key, or OpenRouter API key)
|
||||
- Prompt for the Claude model used to compress observations (Haiku / Sonnet / Opus) when the Claude provider is selected
|
||||
- Copy plugin files into the marketplace directory and register the plugin
|
||||
- Auto-start the worker service
|
||||
|
||||
### Option 2: Plugin Marketplace
|
||||
@@ -39,10 +41,11 @@ Both methods will automatically configure hooks and start the worker service. St
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **Bun**: JavaScript runtime and process manager (auto-installed if missing)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
- **Node.js**: 20.0.0 or higher
|
||||
- **Bun** ≥ 1.0 (auto-installed by `npx claude-mem install` if missing)
|
||||
- **uv** (auto-installed if missing — provides Python for Chroma's embedding service)
|
||||
- **Claude Code** or another supported IDE (Cursor, Gemini CLI, Windsurf, OpenCode, Codex CLI, OpenClaw)
|
||||
- **SQLite 3**: bundled via `bun:sqlite`
|
||||
|
||||
## Advanced Installation
|
||||
|
||||
@@ -73,7 +76,7 @@ npm run worker:status
|
||||
|
||||
#### 1. Automatic Dependency Installation
|
||||
|
||||
Dependencies are installed automatically during plugin installation. The SessionStart hook also ensures dependencies are up-to-date on each session start (this is fast and idempotent). Works cross-platform on Windows, macOS, and Linux.
|
||||
Dependencies are installed automatically by `npx claude-mem install` and `npx claude-mem repair`. Heavy lifting (Bun + uv install, `bun install` inside the plugin cache) happens behind a visible installer spinner. The Setup hook only performs a sub-100ms `version-check.js` read of the `.install-version` marker — on mismatch it prints `run: npx claude-mem repair` to stderr and exits 0, so it never blocks a session. Works cross-platform on Windows, macOS, and Linux.
|
||||
|
||||
#### 2. Verify Plugin Installation
|
||||
|
||||
@@ -110,13 +113,7 @@ npm run test:context
|
||||
|
||||
## Upgrading
|
||||
|
||||
Upgrades are automatic when updating via the plugin marketplace. Key changes in recent versions:
|
||||
|
||||
**v7.1.0**: PM2 replaced with native Bun process management. Migration is automatic on first hook trigger.
|
||||
|
||||
**v7.0.0+**: 11 configuration settings, dual-tag privacy system.
|
||||
|
||||
**v5.4.0+**: Skill-based search replaces MCP tools, saving ~2,250 tokens per session.
|
||||
Upgrades are automatic when updating via the plugin marketplace. After an external upgrade (for example `claude plugin update`), the Setup hook detects a version-marker mismatch and asks you to run `npx claude-mem repair`, which installs any missing runtime dependencies and refreshes the marker.
|
||||
|
||||
See [CHANGELOG](https://github.com/thedotmack/claude-mem/blob/main/CHANGELOG.md) for complete version history.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
- 🎭 **Mode System** - Switch between workflows (Code, Email Investigation, Chill)
|
||||
- 🔍 **MCP Search Tools** - Query your project history with natural language
|
||||
- 🧠 **Knowledge Agents** - Build queryable "brains" from your observation history
|
||||
- 🌐 **Web Viewer UI** - Real-time memory stream visualization at http://localhost:37777
|
||||
- 🌐 **Web Viewer UI** - Real-time memory stream visualization served by the local worker
|
||||
- 🔒 **Privacy Control** - Use `<private>` tags to exclude sensitive content from storage
|
||||
- ⚙️ **Context Configuration** - Fine-grained control over what context gets injected
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
@@ -66,40 +66,22 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
```
|
||||
|
||||
**Core Components:**
|
||||
1. **4 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop
|
||||
2. **Smart Install** - Cached dependency checker (pre-hook script)
|
||||
3. **Worker Service** - HTTP API on port 37777 managed by Bun
|
||||
4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
|
||||
5. **MCP Search Tools** - Query historical context with natural language
|
||||
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Summary (Stop), SessionEnd
|
||||
2. **Worker Service** - Express HTTP API managed by Bun on a per-user port (default `37700 + (uid % 100)`)
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
|
||||
4. **Chroma Vector Index** - Optional embedding-based semantic search
|
||||
5. **MCP / Skill Search Tools** - 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.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **Bun**: JavaScript runtime and process manager (auto-installed if missing)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
## What's New
|
||||
|
||||
**v9.0.0 - Live Context:**
|
||||
- **Folder Context Files**: Auto-generated `CLAUDE.md` in project folders with activity timelines
|
||||
- **Worktree Support**: Unified context from parent repos and git worktrees
|
||||
- **Configurable Observation Limits**: Control how many observations appear in context
|
||||
- **Windows Fixes**: Resolved IPC detection and hook execution issues
|
||||
- **Settings Auto-Creation**: `settings.json` now auto-creates on first run
|
||||
- **MCP Tools Naming**: Updated from "mem-search skill" to "MCP tools" terminology
|
||||
|
||||
**v7.1.0 - Bun Migration:**
|
||||
- Replaced PM2 with native Bun process management
|
||||
- Switched from better-sqlite3 to bun:sqlite for faster database access
|
||||
- Simplified cross-platform support
|
||||
|
||||
**v7.0.0 - Context Configuration:**
|
||||
- 11 settings for fine-grained control over context injection
|
||||
- Dual-tag privacy system (`<private>` tags)
|
||||
- **Node.js**: 20.0.0 or higher
|
||||
- **Bun** ≥ 1.0 (auto-installed by `npx claude-mem install` if missing)
|
||||
- **uv** (auto-installed if missing — provides Python for Chroma)
|
||||
- **Claude Code** (or another supported IDE: Cursor, Gemini CLI, Windsurf, OpenCode, Codex CLI, OpenClaw)
|
||||
- **SQLite 3** — bundled via `bun:sqlite`
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ icon: plug
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Version:** 7.0.0 (December 2025)
|
||||
**Target Audience:** Developers building claude-mem integrations (VSCode extensions, IDE plugins, CLI tools)
|
||||
**Target Audience:** Developers building claude-mem integrations (VSCode extensions, IDE plugins, CLI tools).
|
||||
</Note>
|
||||
|
||||
## Quick Reference
|
||||
@@ -14,8 +13,11 @@ icon: plug
|
||||
### Worker Service Basics
|
||||
|
||||
```typescript
|
||||
const WORKER_BASE_URL = 'http://localhost:37777';
|
||||
const DEFAULT_PORT = 37777; // Override with CLAUDE_MEM_WORKER_PORT
|
||||
// Resolve the worker port at runtime. The default is per-user (37700 + uid % 100),
|
||||
// or whatever the user set via CLAUDE_MEM_WORKER_PORT / settings.json. Read it from
|
||||
// process.env.CLAUDE_MEM_WORKER_PORT, then ~/.claude-mem/settings.json
|
||||
// (CLAUDE_MEM_WORKER_PORT key), then fall back to the deterministic default.
|
||||
const WORKER_BASE_URL = `http://127.0.0.1:${workerPort}`;
|
||||
```
|
||||
|
||||
### Most Common Operations
|
||||
@@ -46,9 +48,10 @@ GET /api/context/recent?project=my-project&limit=3
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
CLAUDE_MEM_MODEL=claude-sonnet-4-6 # Model for observations/summaries
|
||||
CLAUDE_MEM_MODEL=claude-haiku-4-5-20251001 # Default Claude model for observations/summaries
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS=50 # Observations injected at SessionStart
|
||||
CLAUDE_MEM_WORKER_PORT=37777 # Worker service port
|
||||
CLAUDE_MEM_WORKER_PORT= # Optional override; default = 37700 + (uid % 100)
|
||||
CLAUDE_MEM_DATA_DIR= # Optional override for the data directory
|
||||
CLAUDE_MEM_PYTHON_VERSION=3.13 # Python version for chroma-mcp
|
||||
```
|
||||
|
||||
@@ -68,7 +71,7 @@ npm run worker:status # Check worker status
|
||||
|
||||
```plaintext
|
||||
Platform Hook/Extension
|
||||
→ HTTP Request to Worker (localhost:37777)
|
||||
→ HTTP Request to Worker (`${WORKER_BASE_URL}` — per-user, default 37700+uid%100)
|
||||
→ Route Handler (SessionRoutes/DataRoutes/SearchRoutes/etc.)
|
||||
→ Domain Service (SessionManager/SearchManager/DatabaseManager)
|
||||
→ Database (SQLite3 + Chroma vector DB)
|
||||
@@ -311,7 +314,7 @@ GET /api/stats
|
||||
"uptime": 12345,
|
||||
"activeSessions": 2,
|
||||
"sseClients": 1,
|
||||
"port": 37777
|
||||
"port": 37742
|
||||
},
|
||||
"database": {
|
||||
"path": "~/.claude-mem/claude-mem.db",
|
||||
@@ -796,7 +799,7 @@ interface TimelineItem {
|
||||
async function onPostToolUse(context: HookContext) {
|
||||
const { session_id, tool_name, tool_input, tool_result, cwd } = context;
|
||||
|
||||
const response = await fetch('http://localhost:37777/api/sessions/observations', {
|
||||
const response = await fetch(`${WORKER_BASE_URL}/api/sessions/observations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -818,7 +821,7 @@ interface TimelineItem {
|
||||
async function onSummary(context: HookContext) {
|
||||
const { session_id, last_user_message, last_assistant_message } = context;
|
||||
|
||||
await fetch('http://localhost:37777/api/sessions/summarize', {
|
||||
await fetch(`${WORKER_BASE_URL}/api/sessions/summarize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -835,7 +838,7 @@ interface TimelineItem {
|
||||
async function onSessionEnd(context: HookContext) {
|
||||
const { session_id } = context;
|
||||
|
||||
await fetch('http://localhost:37777/api/sessions/complete', {
|
||||
await fetch(`${WORKER_BASE_URL}/api/sessions/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -867,7 +870,7 @@ const searchTool: SearchTool = {
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:37777/api/search?query=${encodeURIComponent(query)}&format=index&limit=10`
|
||||
`${WORKER_BASE_URL}/api/search?query=${encodeURIComponent(query)}&format=index&limit=10`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -905,7 +908,7 @@ const participant = vscode.chat.createChatParticipant('claude-mem', async (reque
|
||||
stream.markdown(`Searching memory for: ${request.prompt}\n\n`);
|
||||
|
||||
const response = await fetch(
|
||||
`http://localhost:37777/api/search?query=${encodeURIComponent(request.prompt)}&format=index&limit=5`
|
||||
`${WORKER_BASE_URL}/api/search?query=${encodeURIComponent(request.prompt)}&format=index&limit=5`
|
||||
);
|
||||
|
||||
const results = await response.json();
|
||||
@@ -931,7 +934,7 @@ async function callWorkerWithFallback<T>(
|
||||
options?: RequestInit
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:37777${endpoint}`, {
|
||||
const response = await fetch(`${WORKER_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
signal: AbortSignal.timeout(5000) // 5s timeout
|
||||
});
|
||||
@@ -975,7 +978,7 @@ async function retryWithBackoff<T>(
|
||||
```typescript
|
||||
async function isWorkerHealthy(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://localhost:37777/api/health', {
|
||||
const response = await fetch(`${WORKER_BASE_URL}/api/health`, {
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
return response.ok;
|
||||
@@ -1017,7 +1020,7 @@ class WorkerTimeoutError extends Error {
|
||||
|
||||
```typescript
|
||||
function connectToSSE(onEvent: (event: any) => void) {
|
||||
const eventSource = new EventSource('http://localhost:37777/stream');
|
||||
const eventSource = new EventSource(`${WORKER_BASE_URL}/stream`);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
@@ -1138,8 +1141,8 @@ esbuild.build({
|
||||
</Step>
|
||||
<Step title="Terminal 3: Test API manually">
|
||||
```bash
|
||||
curl http://localhost:37777/api/health
|
||||
curl "http://localhost:37777/api/search?query=test&limit=5"
|
||||
curl http://127.0.0.1:$WORKER_PORT/api/health
|
||||
curl "http://127.0.0.1:$WORKER_PORT/api/search?query=test&limit=5"
|
||||
```
|
||||
</Step>
|
||||
<Step title="VSCode: Launch extension host">
|
||||
@@ -1239,8 +1242,8 @@ describe('Worker Integration', () => {
|
||||
<AccordionGroup>
|
||||
<Accordion title="Phase 1: Connection & Health">
|
||||
- [ ] Worker starts successfully (`npm run worker:status`)
|
||||
- [ ] Health endpoint responds (`curl http://localhost:37777/api/health`)
|
||||
- [ ] SSE stream connects (`curl http://localhost:37777/stream`)
|
||||
- [ ] Health endpoint responds (`curl http://127.0.0.1:$WORKER_PORT/api/health`)
|
||||
- [ ] SSE stream connects (`curl http://127.0.0.1:$WORKER_PORT/stream`)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Phase 2: Session Lifecycle">
|
||||
@@ -1284,8 +1287,11 @@ describe('Worker Integration', () => {
|
||||
export class WorkerClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(port: number = 37777) {
|
||||
this.baseUrl = `http://localhost:${port}`;
|
||||
// Resolve the active worker port via env / settings.json, falling back
|
||||
// to the deterministic per-user default. See parseWorkerPort() for an
|
||||
// example helper; never hardcode a single value.
|
||||
constructor(port: number = resolveWorkerPort()) {
|
||||
this.baseUrl = `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
async isHealthy(): Promise<boolean> {
|
||||
|
||||
@@ -21,24 +21,24 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
||||
|
||||
---
|
||||
|
||||
## v5.x Specific Issues
|
||||
## Common Issues
|
||||
|
||||
### Viewer UI Not Loading
|
||||
|
||||
**Symptoms**: Cannot access http://localhost:37777, page doesn't load, or shows connection error.
|
||||
**Symptoms**: Cannot reach the viewer URL, page doesn't load, or browser shows a connection error.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check if worker is running on port 37777:
|
||||
1. Find the worker port. The default is `37700 + (uid % 100)`. The configured port is the value of `CLAUDE_MEM_WORKER_PORT` in `~/.claude-mem/settings.json`; the running worker also reports it on `/api/health`:
|
||||
```bash
|
||||
lsof -i :37777
|
||||
# or
|
||||
PORT=$(jq -r .CLAUDE_MEM_WORKER_PORT ~/.claude-mem/settings.json)
|
||||
lsof -i :$PORT
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
2. Verify worker is healthy:
|
||||
```bash
|
||||
curl http://localhost:37777/health
|
||||
curl http://127.0.0.1:$PORT/health
|
||||
```
|
||||
|
||||
3. Check worker logs for errors:
|
||||
@@ -51,9 +51,8 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
5. Check for port conflicts:
|
||||
5. Pin a fixed port if the auto-assigned one collides:
|
||||
```bash
|
||||
# If port 37777 is in use by another service
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
npm run worker:restart
|
||||
```
|
||||
@@ -170,7 +169,6 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
||||
|
||||
4. Restart Claude Code after manual install
|
||||
|
||||
|
||||
## Worker Service Issues
|
||||
|
||||
### Worker Service Not Starting
|
||||
@@ -230,7 +228,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
||||
|
||||
4. Verify new port:
|
||||
```bash
|
||||
cat ~/.claude-mem/worker.port
|
||||
curl -s http://127.0.0.1:$CLAUDE_MEM_WORKER_PORT/api/health | jq .port
|
||||
```
|
||||
|
||||
### Worker Keeps Crashing
|
||||
@@ -954,11 +952,11 @@ npm run worker:status
|
||||
# View logs
|
||||
npm run worker:logs
|
||||
|
||||
# Check port file
|
||||
cat ~/.claude-mem/worker.port
|
||||
# Check configured port (per-user default = 37700 + uid % 100)
|
||||
jq -r .CLAUDE_MEM_WORKER_PORT ~/.claude-mem/settings.json
|
||||
|
||||
# Test worker health
|
||||
curl http://localhost:37777/health
|
||||
# Test worker health (substitute PORT for the value above)
|
||||
curl "http://127.0.0.1:$PORT/health"
|
||||
```
|
||||
|
||||
### Database Inspection
|
||||
|
||||
@@ -34,8 +34,6 @@ Each folder's `CLAUDE.md` contains a "Recent Activity" section showing:
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Jan 4, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|
||||
@@ -76,7 +76,7 @@ This means Claude "remembers" what happened in previous sessions!
|
||||
|
||||
### Worker Management
|
||||
|
||||
v4.0+ auto-starts the worker on first session. Manual commands below are optional.
|
||||
The worker auto-starts on the first SessionStart hook, so you usually don't need these commands. They're handy for diagnostics.
|
||||
|
||||
```bash
|
||||
# Start worker service (optional - auto-starts automatically)
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
# claude-mem SWE-bench agent image
|
||||
# Plan: .claude/plans/swebench-claude-mem-docker.md (Phase 1)
|
||||
#
|
||||
# Produces `claude-mem/swebench-agent:latest`: Claude Code CLI 2.1.114 +
|
||||
# locally-built claude-mem plugin, ready to run headlessly per SWE-bench
|
||||
# instance. Auth (ANTHROPIC_API_KEY) is passed at runtime, never baked in.
|
||||
|
||||
FROM node:20-bookworm-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# System dependencies:
|
||||
# git, curl, ca-certificates, unzip — base tooling (Bun installer needs unzip)
|
||||
# jq — JSONL assembly in run-instance.sh
|
||||
# uuid-runtime — uuidgen for per-instance session IDs (Phase 2)
|
||||
# sqlite3 — verifies the claude-mem observations DB
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
@@ -25,46 +14,25 @@ RUN apt-get update \
|
||||
sqlite3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Bun (claude-mem worker service runs under Bun). Installed to a system
|
||||
# location so the non-root runtime user can execute it.
|
||||
ENV BUN_INSTALL="/usr/local/bun"
|
||||
RUN curl -fsSL https://bun.sh/install | bash \
|
||||
&& chmod -R a+rX /usr/local/bun
|
||||
ENV PATH="/usr/local/bun/bin:${PATH}"
|
||||
|
||||
# uv (provides Python for Chroma per CLAUDE.md). Installed to a system
|
||||
# location, same reason.
|
||||
ENV UV_INSTALL_DIR="/usr/local/bin"
|
||||
# Group the chmod so the trailing `|| true` only absorbs chmod failures; without
|
||||
# this grouping, bash precedence (`&&` binds tighter than `||`) would silently
|
||||
# mask a failed `curl|sh` install step.
|
||||
RUN set -eux \
|
||||
&& curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||
&& { chmod a+rX /usr/local/bin/uv /usr/local/bin/uvx 2>/dev/null || true; }
|
||||
|
||||
# Claude Code CLI — PINNED to the version whose flag surface was verified in
|
||||
# the plan (Phase 0). Do NOT bump without re-verifying flags.
|
||||
RUN npm install -g @anthropic-ai/claude-code@2.1.114
|
||||
|
||||
# Locally-built claude-mem plugin. The build-agent-image.sh wrapper runs
|
||||
# `npm run build` before `docker build`, so plugin/ is populated in the build
|
||||
# context. We do NOT install claude-mem from npm — we want the current
|
||||
# worktree under test.
|
||||
COPY plugin/ /opt/claude-mem/
|
||||
|
||||
# Runner script — entrypoint for per-instance invocation (Phase 2 deliverable).
|
||||
COPY evals/swebench/run-instance.sh /evals/swebench/run-instance.sh
|
||||
RUN chmod +x /evals/swebench/run-instance.sh
|
||||
|
||||
# Pre-create per-instance config dirs. run-instance.sh overrides HOME to a
|
||||
# scratch dir for isolation, but having these present keeps tools from
|
||||
# bailing if they probe the default locations before HOME is set.
|
||||
RUN mkdir -p /root/.claude /root/.claude-mem
|
||||
|
||||
# Non-root user. Claude Code refuses `--dangerously-skip-permissions` /
|
||||
# `--permission-mode bypassPermissions` when euid==0 as a safety rail, so we
|
||||
# need an unprivileged user for headless batch runs. node:20 already ships a
|
||||
# `node` user at uid 1000 — reuse it.
|
||||
RUN mkdir -p /home/node/.claude /home/node/.claude-mem \
|
||||
&& chown -R node:node /home/node /opt/claude-mem
|
||||
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the claude-mem SWE-bench agent image.
|
||||
# Plan: .claude/plans/swebench-claude-mem-docker.md (Phase 1, step 2)
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve repo root (two levels up from this script: evals/swebench -> repo).
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# 1. Build the plugin so plugin/ is populated for the COPY step in the Dockerfile.
|
||||
npm run build
|
||||
|
||||
# 2. Build the agent image. Context is the repo root so both plugin/ and
|
||||
# evals/swebench/run-instance.sh are reachable.
|
||||
docker build \
|
||||
-f evals/swebench/Dockerfile.agent \
|
||||
-t claude-mem/swebench-agent:latest \
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# eval.sh — Thin wrapper around `python -m swebench.harness.run_evaluation`.
|
||||
#
|
||||
# Required env:
|
||||
# RUN_ID Identifier for this evaluation run (matches predictions dir).
|
||||
# Optional env:
|
||||
# MAX_WORKERS Parallel worker count for the harness (default: 4).
|
||||
# DATASET HF dataset name (default: princeton-nlp/SWE-bench_Verified).
|
||||
# TIMEOUT Per-instance timeout in seconds (default: 1800).
|
||||
#
|
||||
# Reports land at:
|
||||
# logs/run_evaluation/$RUN_ID/claude-opus-4-7+claude-mem/<instance_id>/report.json
|
||||
|
||||
: "${RUN_ID:?RUN_ID is required (e.g. RUN_ID=smoke-001)}"
|
||||
MAX_WORKERS="${MAX_WORKERS:-4}"
|
||||
DATASET="${DATASET:-princeton-nlp/SWE-bench_Verified}"
|
||||
@@ -30,7 +18,6 @@ if [[ ! -f "$PREDICTIONS" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Harness REQUIRES Docker — fail fast with a clean message if it's not running.
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "ERROR: docker CLI not found on PATH. The SWE-bench harness requires Docker." >&2
|
||||
exit 1
|
||||
@@ -40,13 +27,11 @@ if ! docker info >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create/reuse a dedicated venv so we don't pollute the system Python.
|
||||
VENV_DIR=".venv-swebench"
|
||||
if [[ ! -d "$VENV_DIR" ]]; then
|
||||
echo "[eval.sh] Creating Python venv at $VENV_DIR ..."
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
echo "[eval.sh] Installing/updating swebench in $VENV_DIR ..."
|
||||
|
||||
+15
-54
@@ -37,9 +37,6 @@ from typing import Any, Iterable
|
||||
|
||||
from datasets import load_dataset
|
||||
|
||||
|
||||
# Hidden-from-agent fields per the plan. We MUST NOT pass these to the agent
|
||||
# container — they are evaluator-only ground truth.
|
||||
HIDDEN_AGENT_FIELDS = (
|
||||
"patch",
|
||||
"test_patch",
|
||||
@@ -49,7 +46,6 @@ HIDDEN_AGENT_FIELDS = (
|
||||
"version",
|
||||
)
|
||||
|
||||
|
||||
def extract_oauth_credentials() -> Path | None:
|
||||
"""
|
||||
Extract Claude Code OAuth credentials (from a Max/Pro subscription) to a
|
||||
@@ -76,12 +72,8 @@ def extract_oauth_credentials() -> Path | None:
|
||||
)
|
||||
temp_path = Path(temp.name)
|
||||
temp.close()
|
||||
# Clean up on process exit, even on crash.
|
||||
atexit.register(lambda: temp_path.unlink(missing_ok=True))
|
||||
|
||||
# macOS: try Keychain first (primary storage on Darwin). On miss, fall
|
||||
# through to the on-disk credentials file — some macOS setups (older CLI,
|
||||
# migrated machines) only have the file form.
|
||||
if platform.system() == "Darwin":
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
@@ -100,15 +92,12 @@ def extract_oauth_credentials() -> Path | None:
|
||||
temp_path.write_text(completed.stdout.strip(), encoding="utf-8")
|
||||
temp_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
||||
return temp_path
|
||||
# else fall through to the on-disk credentials check below
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"WARN: `security` command not available; trying on-disk creds.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# fall through to the on-disk credentials check below
|
||||
|
||||
# Both platforms (and macOS fallback): read the on-disk credentials file.
|
||||
creds_file = Path.home() / ".claude" / ".credentials.json"
|
||||
if creds_file.exists():
|
||||
temp_path.write_text(creds_file.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
@@ -124,7 +113,6 @@ def extract_oauth_credentials() -> Path | None:
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run the claude-mem SWE-bench agent on a batch of instances.",
|
||||
@@ -199,7 +187,6 @@ def parse_args() -> argparse.Namespace:
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def select_instances(
|
||||
dataset: Iterable[dict[str, Any]],
|
||||
instance_ids: list[str] | None,
|
||||
@@ -221,7 +208,6 @@ def select_instances(
|
||||
rows = rows[:limit]
|
||||
return rows
|
||||
|
||||
|
||||
def append_prediction_row(
|
||||
predictions_path: Path,
|
||||
instance_id: str,
|
||||
@@ -240,14 +226,12 @@ def append_prediction_row(
|
||||
with predictions_path.open("a", encoding="utf-8") as fp:
|
||||
fp.write(line)
|
||||
|
||||
|
||||
def copy_log_if_exists(src: Path, dst: Path) -> None:
|
||||
"""Copy a log file from the shared scratch volume into the run-log directory, if present."""
|
||||
if src.exists() and src.is_file():
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
|
||||
def run_one_instance(
|
||||
instance: dict[str, Any],
|
||||
image: str,
|
||||
@@ -278,7 +262,6 @@ def run_one_instance(
|
||||
instance_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
stderr_log_path = instance_log_dir / "stderr.log"
|
||||
|
||||
# Per-instance scratch dir — MUST NOT be shared across containers.
|
||||
scratch_dir = Path(tempfile.mkdtemp(prefix=f"swebench-{instance_id}-"))
|
||||
problem_file = scratch_dir / "problem.txt"
|
||||
problem_file.write_text(problem_statement, encoding="utf-8")
|
||||
@@ -286,18 +269,9 @@ def run_one_instance(
|
||||
status: str = "failed"
|
||||
model_patch: str = ""
|
||||
|
||||
# Uniquely named so the TimeoutExpired handler can kill it without racing
|
||||
# other instances on the host.
|
||||
container_name = f"swebench-agent-{instance_id}-{os.getpid()}-{threading.get_ident()}"
|
||||
|
||||
try:
|
||||
# The orchestrator owns JSONL writes under `predictions_lock` to avoid
|
||||
# racy concurrent appends across containers — so we DO NOT mount the
|
||||
# predictions directory into the container. Instead, the agent writes
|
||||
# its authoritative diff to /scratch/model_patch.diff (via
|
||||
# CLAUDE_MEM_OUTPUT_DIR), plus ingest/fix logs to the same dir. The
|
||||
# 5th CLI arg to run-instance.sh is only used in standalone smoke-test
|
||||
# mode; here we point it at a throwaway path inside the container.
|
||||
cmd: list[str] = [
|
||||
"docker",
|
||||
"run",
|
||||
@@ -317,7 +291,6 @@ def run_one_instance(
|
||||
f"{oauth_creds_path}:/auth/.credentials.json:ro",
|
||||
]
|
||||
else:
|
||||
# Pay-per-call path.
|
||||
cmd += ["-e", "ANTHROPIC_API_KEY"]
|
||||
cmd += [
|
||||
image,
|
||||
@@ -336,18 +309,12 @@ def run_one_instance(
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
# Persist stderr so post-mortem is possible even on success.
|
||||
stderr_log_path.write_text(
|
||||
f"=== STDOUT ===\n{completed.stdout}\n=== STDERR ===\n{completed.stderr}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
if completed.returncode == 0:
|
||||
# Read the diff the agent wrote to the shared predictions volume.
|
||||
# The container writes its own prediction line; we prefer to
|
||||
# write our own authoritative row here from the diff file the
|
||||
# agent left in /scratch. If the agent wrote a diff file, use
|
||||
# it; otherwise fall back to empty patch.
|
||||
diff_file = scratch_dir / "model_patch.diff"
|
||||
if diff_file.exists():
|
||||
diff_text = diff_file.read_text(encoding="utf-8")
|
||||
@@ -355,19 +322,14 @@ def run_one_instance(
|
||||
model_patch = diff_text
|
||||
status = "succeeded"
|
||||
else:
|
||||
status = "failed" # empty diff
|
||||
status = "failed"
|
||||
else:
|
||||
# Container did not leave a diff file — treat as failure
|
||||
# but still emit an empty-patch row below.
|
||||
status = "failed"
|
||||
else:
|
||||
status = "failed"
|
||||
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
status = "timed_out"
|
||||
# subprocess.run killed the docker CLI, but the container may
|
||||
# still be running. Force-remove it by name so we don't leak
|
||||
# containers across the batch.
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", container_name],
|
||||
capture_output=True,
|
||||
@@ -381,11 +343,9 @@ def run_one_instance(
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Copy per-turn logs left by the agent in the shared scratch volume.
|
||||
copy_log_if_exists(scratch_dir / "ingest.jsonl", instance_log_dir / "ingest.jsonl")
|
||||
copy_log_if_exists(scratch_dir / "fix.jsonl", instance_log_dir / "fix.jsonl")
|
||||
|
||||
# Always write a row — never silently drop an instance.
|
||||
append_prediction_row(
|
||||
predictions_path=predictions_path,
|
||||
instance_id=instance_id,
|
||||
@@ -394,7 +354,7 @@ def run_one_instance(
|
||||
lock=predictions_lock,
|
||||
)
|
||||
|
||||
except Exception as exc: # pragma: no cover — defensive
|
||||
except Exception as exc:
|
||||
status = "failed"
|
||||
try:
|
||||
stderr_log_path.write_text(
|
||||
@@ -411,12 +371,10 @@ def run_one_instance(
|
||||
lock=predictions_lock,
|
||||
)
|
||||
finally:
|
||||
# Per-instance scratch must not leak across containers.
|
||||
shutil.rmtree(scratch_dir, ignore_errors=True)
|
||||
|
||||
return status, instance_id
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
@@ -434,9 +392,8 @@ def main() -> int:
|
||||
)
|
||||
|
||||
predictions_dir = predictions_path.parent
|
||||
run_dir = predictions_dir # logs land in evals/swebench/runs/<run_id>/<instance_id>/
|
||||
run_dir = predictions_dir
|
||||
predictions_dir.mkdir(parents=True, exist_ok=True)
|
||||
# Don't silently discard partial results from a prior run.
|
||||
if predictions_path.exists() and predictions_path.stat().st_size > 0:
|
||||
if not args.overwrite:
|
||||
print(
|
||||
@@ -451,7 +408,6 @@ def main() -> int:
|
||||
)
|
||||
predictions_path.write_text("", encoding="utf-8")
|
||||
|
||||
# Resolve auth: OAuth (Max/Pro subscription) or API key.
|
||||
oauth_creds_path: Path | None = None
|
||||
if args.auth in ("oauth", "auto"):
|
||||
oauth_creds_path = extract_oauth_credentials()
|
||||
@@ -488,10 +444,6 @@ def main() -> int:
|
||||
print("No instances selected; nothing to do.", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
# Scrub hidden-from-agent fields defensively. The agent container only
|
||||
# receives instance_id/repo/base_commit/problem_statement via CLI args +
|
||||
# the per-instance problem file — the hidden fields never leave this
|
||||
# process. This loop makes that invariant explicit.
|
||||
for row in instances:
|
||||
for key in HIDDEN_AGENT_FIELDS:
|
||||
row.pop(key, None)
|
||||
@@ -530,12 +482,23 @@ def main() -> int:
|
||||
instance_id = future_to_id[future]
|
||||
try:
|
||||
status, _ = future.result()
|
||||
except Exception as exc: # pragma: no cover — defensive
|
||||
except Exception as exc:
|
||||
status = "failed"
|
||||
print(
|
||||
f"[{instance_id}] orchestrator future raised: {exc!r}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# The orchestrator died before run_one_instance could write a
|
||||
# row. Append a fallback so this instance still appears in
|
||||
# predictions.jsonl — preserving the "never drop an instance"
|
||||
# guarantee that downstream evaluation depends on.
|
||||
append_prediction_row(
|
||||
predictions_path=predictions_path,
|
||||
instance_id=instance_id,
|
||||
model_patch="",
|
||||
model_name_or_path=model_name_or_path,
|
||||
lock=predictions_lock,
|
||||
)
|
||||
|
||||
if status == "succeeded":
|
||||
succeeded += 1
|
||||
@@ -553,9 +516,7 @@ def main() -> int:
|
||||
print(
|
||||
f"{total} total, {succeeded} succeeded, {failed} failed, {timed_out} timed out",
|
||||
)
|
||||
# Per plan: exit 0 even if some instances failed.
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# run-instance.sh — runs Claude Code + claude-mem against a single SWE-bench
|
||||
# instance using the two-turn protocol (ingest, then fix), and appends a
|
||||
# prediction JSONL row to OUT_PREDICTIONS_PATH.
|
||||
#
|
||||
# Usage:
|
||||
# run-instance.sh INSTANCE_ID REPO_SLUG BASE_COMMIT PROBLEM_STATEMENT_FILE OUT_PREDICTIONS_PATH
|
||||
#
|
||||
# Required env:
|
||||
# ANTHROPIC_API_KEY
|
||||
|
||||
if [[ $# -ne 5 ]]; then
|
||||
echo "Usage: $0 INSTANCE_ID REPO_SLUG BASE_COMMIT PROBLEM_STATEMENT_FILE OUT_PREDICTIONS_PATH" >&2
|
||||
exit 2
|
||||
@@ -22,12 +12,6 @@ BASE_COMMIT="$3"
|
||||
PROBLEM_STATEMENT_FILE="$4"
|
||||
OUT_PREDICTIONS_PATH="$5"
|
||||
|
||||
# Auth: either ANTHROPIC_API_KEY (pay-per-call) OR a pre-extracted OAuth
|
||||
# credentials file from a Claude Max/Pro subscription (flat-fee, but subject
|
||||
# to Anthropic's usage limits — batch-scale runs may exhaust the 5h window).
|
||||
# run-batch.py extracts OAuth creds from host Keychain/file and mounts them
|
||||
# at CLAUDE_MEM_CREDENTIALS_FILE; standalone smoke-test can do the same, or
|
||||
# set ANTHROPIC_API_KEY directly.
|
||||
if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${CLAUDE_MEM_CREDENTIALS_FILE:-}" ]]; then
|
||||
echo "ERROR: one of ANTHROPIC_API_KEY or CLAUDE_MEM_CREDENTIALS_FILE is required" >&2
|
||||
exit 1
|
||||
@@ -45,33 +29,20 @@ fi
|
||||
|
||||
MODEL_NAME="claude-opus-4-7+claude-mem"
|
||||
|
||||
# Per-instance ephemeral scratch dir — isolates ~/.claude/ and ~/.claude-mem/.
|
||||
SCRATCH=$(mktemp -d)
|
||||
REPO_DIR="$SCRATCH/repo"
|
||||
MEM_DIR="$SCRATCH/.claude-mem"
|
||||
CLAUDE_DIR="$SCRATCH/.claude"
|
||||
mkdir -p "$MEM_DIR" "$CLAUDE_DIR"
|
||||
|
||||
# If using OAuth, seed the isolated CLAUDE_DIR with the mounted credentials
|
||||
# file so Claude Code finds them at HOME=$SCRATCH → ~/.claude/.credentials.json.
|
||||
# chmod 600 to match what `claude login` writes (it checks permissions).
|
||||
if [[ -n "${CLAUDE_MEM_CREDENTIALS_FILE:-}" ]]; then
|
||||
cp "$CLAUDE_MEM_CREDENTIALS_FILE" "$CLAUDE_DIR/.credentials.json"
|
||||
chmod 600 "$CLAUDE_DIR/.credentials.json"
|
||||
fi
|
||||
|
||||
# Directory where artifacts the batch orchestrator reads (model_patch.diff,
|
||||
# ingest.jsonl, fix.jsonl) are written. When run via `docker run -v
|
||||
# <host-scratch>:/scratch` from run-batch.py, the orchestrator sets
|
||||
# CLAUDE_MEM_OUTPUT_DIR=/scratch so these files are visible on the host. In
|
||||
# standalone/smoke-test mode the default keeps artifacts in the ephemeral
|
||||
# scratch dir alongside the repo.
|
||||
OUTPUT_DIR="${CLAUDE_MEM_OUTPUT_DIR:-$SCRATCH}"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Always write a prediction row (even on failure) so batch mode stays aligned.
|
||||
# The trap emits an empty-patch row if we exit before the success path sets
|
||||
# PREDICTION_EMITTED=1, then cleans up SCRATCH.
|
||||
DIFF_OUT="$OUTPUT_DIR/model_patch.diff"
|
||||
INGEST_LOG="$OUTPUT_DIR/ingest.jsonl"
|
||||
FIX_LOG="$OUTPUT_DIR/fix.jsonl"
|
||||
@@ -80,7 +51,6 @@ PREDICTION_EMITTED=0
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
if [[ "$PREDICTION_EMITTED" -ne 1 ]]; then
|
||||
# Ensure the orchestrator sees an (empty) diff file even on early exit.
|
||||
: > "$DIFF_OUT" 2>/dev/null || true
|
||||
jq -nc \
|
||||
--arg id "$INSTANCE_ID" \
|
||||
@@ -94,11 +64,6 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Shallow clone + fetch the exact commit. Saves minutes on large repos
|
||||
# (sympy/django/scikit-learn) vs. a full-history clone. Fallback to a full
|
||||
# clone if the server rejects the by-commit fetch (GitHub supports
|
||||
# uploadpack.allowReachableSHA1InWant by default on public repos, but mirrors
|
||||
# may not).
|
||||
if ! { git clone --depth 1 --no-single-branch "https://github.com/${REPO_SLUG}.git" "$REPO_DIR" \
|
||||
&& git -C "$REPO_DIR" fetch --depth 1 origin "$BASE_COMMIT"; }; then
|
||||
echo "WARN: shallow fetch failed; falling back to full clone" >&2
|
||||
@@ -107,7 +72,6 @@ if ! { git clone --depth 1 --no-single-branch "https://github.com/${REPO_SLUG}.g
|
||||
fi
|
||||
git -C "$REPO_DIR" reset --hard "$BASE_COMMIT"
|
||||
|
||||
# ---------- Turn 1: Ingest (populate memory via PostToolUse hook) ----------
|
||||
INGEST_PROMPT="Please learn about the codebase by systematically and thoroughly reading EVERY SOURCE FILE IN FULL, no matter how many there are. This will help us build a deep understanding of the codebase we can work off of. Don't worry about cost. This is critical and non-negotiable."
|
||||
|
||||
SESSION_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
||||
@@ -131,7 +95,6 @@ if [[ "$INGEST_EXIT" -ne 0 ]]; then
|
||||
echo "WARN: ingest turn exited with $INGEST_EXIT; continuing to fix turn" >&2
|
||||
fi
|
||||
|
||||
# ---------- Turn 2: Fix (consume memory via mem-search slash command) ----------
|
||||
PROBLEM=$(cat "$PROBLEM_STATEMENT_FILE")
|
||||
QUERY=$(printf '%s' "$PROBLEM" | tr -s '[:space:]' ' ' | cut -c1-200)
|
||||
|
||||
@@ -161,9 +124,6 @@ if [[ "$FIX_EXIT" -ne 0 ]]; then
|
||||
echo "WARN: fix turn exited with $FIX_EXIT; will still emit prediction row" >&2
|
||||
fi
|
||||
|
||||
# ---------- Capture diff and emit prediction row ----------
|
||||
# Write the diff to DIFF_OUT first (authoritative for the batch orchestrator),
|
||||
# then read it back for the JSONL row (kept for standalone/smoke-test use).
|
||||
git -C "$REPO_DIR" diff > "$DIFF_OUT" || : > "$DIFF_OUT"
|
||||
DIFF=$(cat "$DIFF_OUT")
|
||||
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# smoke-test.sh — runs ONE SWE-bench instance end-to-end against the agent
|
||||
# container using OAuth credentials extracted from the host. Use this to
|
||||
# verify the two-turn protocol + /claude-mem:mem-search slash resolution
|
||||
# before kicking off a batch run.
|
||||
#
|
||||
# Usage:
|
||||
# evals/swebench/smoke-test.sh [INSTANCE_ID]
|
||||
#
|
||||
# Defaults to sympy__sympy-24152 (an easy Verified instance) if no arg given.
|
||||
#
|
||||
# Outputs:
|
||||
# evals/swebench/runs/smoke/<INSTANCE_ID>/{ingest.jsonl,fix.jsonl,model_patch.diff}
|
||||
# evals/swebench/runs/smoke/predictions.jsonl
|
||||
|
||||
INSTANCE_ID="${1:-sympy__sympy-24152}"
|
||||
DATASET="${DATASET:-princeton-nlp/SWE-bench_Lite}"
|
||||
IMAGE="${IMAGE:-claude-mem/swebench-agent:latest}"
|
||||
@@ -26,12 +12,9 @@ RUN_DIR="$REPO_ROOT/evals/swebench/runs/smoke/$INSTANCE_ID"
|
||||
PREDICTIONS="$REPO_ROOT/evals/swebench/runs/smoke/predictions.jsonl"
|
||||
mkdir -p "$RUN_DIR" "$(dirname "$PREDICTIONS")"
|
||||
|
||||
# --- Extract OAuth credentials ---
|
||||
CREDS_FILE="$(mktemp -t claude-mem-creds.XXXXXX.json)"
|
||||
trap 'rm -f "$CREDS_FILE"' EXIT
|
||||
|
||||
# Try macOS Keychain first (primary on Darwin), then fall through to the
|
||||
# on-disk credentials file — matches docker/claude-mem/run.sh behavior.
|
||||
creds_obtained=0
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if security find-generic-password -s 'Claude Code-credentials' -w > "$CREDS_FILE" 2>/dev/null \
|
||||
@@ -49,7 +32,6 @@ if [[ "$creds_obtained" -eq 0 ]]; then
|
||||
fi
|
||||
chmod 600 "$CREDS_FILE"
|
||||
|
||||
# --- Fetch instance data from HuggingFace via a small Python helper ---
|
||||
INSTANCE_JSON="$(mktemp)"
|
||||
trap 'rm -f "$CREDS_FILE" "$INSTANCE_JSON"' EXIT
|
||||
python3 - "$INSTANCE_ID" "$DATASET" > "$INSTANCE_JSON" <<'PY'
|
||||
@@ -75,10 +57,6 @@ PY
|
||||
SCRATCH="$(mktemp -d -t claude-mem-smoke.XXXXXX)"
|
||||
trap 'rm -f "$CREDS_FILE" "$INSTANCE_JSON"; rm -rf "$SCRATCH"' EXIT
|
||||
|
||||
# Parse the instance JSON once: print repo + base_commit to stdout, write the
|
||||
# problem statement directly to $SCRATCH/problem.txt. INSTANCE_JSON is passed
|
||||
# as argv so stdin is free for the `python3 -` heredoc script body (previously
|
||||
# both were competing for stdin, which made json.load see the heredoc's EOF).
|
||||
read -r REPO BASE_COMMIT < <(
|
||||
python3 - "$SCRATCH" "$INSTANCE_JSON" <<'PY'
|
||||
import json, os, sys
|
||||
@@ -94,9 +72,6 @@ echo "=== Running $INSTANCE_ID ($REPO @ $BASE_COMMIT) ===" >&2
|
||||
echo "Scratch: $SCRATCH" >&2
|
||||
echo "Logs will land in: $RUN_DIR" >&2
|
||||
|
||||
# Pick a wall-clock timeout binary. Linux ships `timeout`; macOS needs
|
||||
# `gtimeout` from coreutils (brew install coreutils). If neither is available,
|
||||
# warn and run without a cap — the smoke test is manual anyway.
|
||||
TIMEOUT_CMD=()
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
TIMEOUT_CMD=(timeout "$TIMEOUT")
|
||||
@@ -106,8 +81,6 @@ else
|
||||
echo "WARN: no \`timeout\`/\`gtimeout\` on PATH; container runs uncapped" >&2
|
||||
fi
|
||||
|
||||
# Name the container so we can force-remove it if the wall-clock timeout
|
||||
# fires (SIGTERM from timeout leaves the container state open briefly).
|
||||
CONTAINER_NAME="claude-mem-smoke-$INSTANCE_ID-$$"
|
||||
|
||||
set +e
|
||||
@@ -123,18 +96,14 @@ DOCKER_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [[ "$DOCKER_EXIT" -eq 124 ]]; then
|
||||
# `timeout` signals TERM and returns 124 on timeout. Force-remove the
|
||||
# container in case docker hasn't reaped it yet.
|
||||
echo "ERROR: docker run exceeded ${TIMEOUT}s wall-clock; removing container" >&2
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Copy artifacts from scratch → RUN_DIR
|
||||
for f in ingest.jsonl fix.jsonl model_patch.diff; do
|
||||
[[ -f "$SCRATCH/$f" ]] && cp "$SCRATCH/$f" "$RUN_DIR/$f"
|
||||
done
|
||||
|
||||
# Emit authoritative prediction row
|
||||
DIFF_FILE="$SCRATCH/model_patch.diff"
|
||||
DIFF=""
|
||||
[[ -f "$DIFF_FILE" ]] && DIFF="$(cat "$DIFF_FILE")"
|
||||
|
||||
@@ -10,7 +10,6 @@ import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_expected_instance_ids(predictions_path: Path) -> list[str]:
|
||||
"""Read instance_ids from a predictions.jsonl file (one JSON object per line)."""
|
||||
instance_ids: list[str] = []
|
||||
@@ -38,7 +37,6 @@ def load_expected_instance_ids(predictions_path: Path) -> list[str]:
|
||||
instance_ids.append(instance_id)
|
||||
return instance_ids
|
||||
|
||||
|
||||
def load_run_results(
|
||||
run_id: str,
|
||||
model_name: str,
|
||||
@@ -81,8 +79,6 @@ def load_run_results(
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
# SWE-bench harness typically nests per-instance data under the
|
||||
# instance_id key; fall back to the top-level dict for flexibility.
|
||||
inner = report_data.get(instance_id, report_data)
|
||||
resolved_value = inner.get("resolved")
|
||||
if resolved_value is True:
|
||||
@@ -116,7 +112,6 @@ def load_run_results(
|
||||
"error_count": error_count,
|
||||
}
|
||||
|
||||
|
||||
def format_resolved_cell(resolved: bool | None) -> str:
|
||||
if resolved is True:
|
||||
return "yes"
|
||||
@@ -124,7 +119,6 @@ def format_resolved_cell(resolved: bool | None) -> str:
|
||||
return "no"
|
||||
return "error"
|
||||
|
||||
|
||||
def render_summary_markdown(run_id: str, results: dict) -> str:
|
||||
total = (
|
||||
results["resolved_count"]
|
||||
@@ -147,13 +141,11 @@ def render_summary_markdown(run_id: str, results: dict) -> str:
|
||||
for instance_id, record in results["per_instance"].items():
|
||||
resolved_cell = format_resolved_cell(record["resolved"])
|
||||
notes_cell = record.get("notes", "") or ""
|
||||
# Escape pipe chars in notes to avoid breaking markdown tables.
|
||||
notes_cell = notes_cell.replace("|", "\\|")
|
||||
lines.append(f"| {instance_id} | {resolved_cell} | {notes_cell} |")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_diff_markdown(
|
||||
current_run_id: str,
|
||||
other_run_id: str,
|
||||
@@ -214,7 +206,6 @@ def render_diff_markdown(
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Summarize SWE-bench evaluation run results."
|
||||
@@ -242,7 +233,6 @@ def main() -> int:
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Resolve repo root from this script's location: evals/swebench/summarize.py
|
||||
script_path = Path(__file__).resolve()
|
||||
repo_root = script_path.parent.parent.parent
|
||||
|
||||
@@ -303,6 +293,5 @@ def main() -> int:
|
||||
print(str(output_path))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# claude-mem installer redirect
|
||||
# The old curl-pipe-bash installer has been replaced by npx claude-mem.
|
||||
# This script now redirects users to the new install method.
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
NC='\033[0m'
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}The curl-pipe-bash installer has been replaced.${NC}"
|
||||
@@ -19,7 +14,7 @@ echo -e "${GREEN}Install claude-mem with a single command:${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}npx claude-mem install${NC}"
|
||||
echo ""
|
||||
echo -e "This requires Node.js >= 18. Get it from ${CYAN}https://nodejs.org${NC}"
|
||||
echo -e "This requires Node.js >= 20. Get it from ${CYAN}https://nodejs.org${NC}"
|
||||
echo ""
|
||||
echo -e "For more info, visit: ${CYAN}https://docs.claude-mem.ai/installation${NC}"
|
||||
echo ""
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// claude-mem installer redirect
|
||||
// The old bundled installer has been replaced by npx claude-mem.
|
||||
// This script now redirects users to the new install method.
|
||||
|
||||
console.log('');
|
||||
console.log('\x1b[33mThe bundled installer has been replaced.\x1b[0m');
|
||||
console.log('');
|
||||
|
||||
@@ -1,35 +1,18 @@
|
||||
# Dockerfile.e2e — End-to-end test: install claude-mem plugin on a real OpenClaw instance
|
||||
# Simulates the complete plugin installation flow a user would follow.
|
||||
#
|
||||
# Usage:
|
||||
# docker build -f Dockerfile.e2e -t openclaw-e2e-test . && docker run --rm openclaw-e2e-test
|
||||
#
|
||||
# Interactive (for human testing):
|
||||
# docker run --rm -it openclaw-e2e-test /bin/bash
|
||||
|
||||
FROM ghcr.io/openclaw/openclaw:main
|
||||
|
||||
USER root
|
||||
|
||||
# Install curl for health checks in e2e-verify.sh, and TypeScript for building
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN npm install -g typescript@5
|
||||
|
||||
# Create staging directory for the plugin source
|
||||
WORKDIR /tmp/claude-mem-plugin
|
||||
|
||||
# Copy plugin source files
|
||||
COPY package.json tsconfig.json openclaw.plugin.json ./
|
||||
COPY src/ ./src/
|
||||
|
||||
# Build the plugin (TypeScript → JavaScript)
|
||||
# NODE_ENV=production is set in the base image; override to install devDependencies
|
||||
RUN NODE_ENV=development npm install && npx tsc
|
||||
|
||||
# Create the installable plugin package:
|
||||
# OpenClaw `plugins install` expects package.json with openclaw.extensions field.
|
||||
# The package name must match the plugin ID in openclaw.plugin.json (claude-mem).
|
||||
# Only include the main plugin entry point, not test/mock files.
|
||||
RUN mkdir -p /tmp/claude-mem-installable/dist && \
|
||||
cp dist/index.js /tmp/claude-mem-installable/dist/ && \
|
||||
cp dist/index.d.ts /tmp/claude-mem-installable/dist/ 2>/dev/null || true && \
|
||||
@@ -45,25 +28,19 @@ RUN mkdir -p /tmp/claude-mem-installable/dist && \
|
||||
require('fs').writeFileSync('/tmp/claude-mem-installable/package.json', JSON.stringify(pkg, null, 2)); \
|
||||
"
|
||||
|
||||
# Switch back to app directory and node user for installation
|
||||
WORKDIR /app
|
||||
USER node
|
||||
|
||||
# Create the OpenClaw config directory
|
||||
RUN mkdir -p /home/node/.openclaw
|
||||
|
||||
# Install the plugin using OpenClaw's official CLI
|
||||
RUN node openclaw.mjs plugins install /tmp/claude-mem-installable
|
||||
|
||||
# Enable the plugin
|
||||
RUN node openclaw.mjs plugins enable claude-mem
|
||||
|
||||
# Copy the e2e verification script and mock worker
|
||||
COPY --chown=node:node e2e-verify.sh /app/e2e-verify.sh
|
||||
USER root
|
||||
RUN chmod +x /app/e2e-verify.sh && \
|
||||
cp /tmp/claude-mem-plugin/dist/mock-worker.js /app/mock-worker.js
|
||||
USER node
|
||||
|
||||
# Default: run the automated verification
|
||||
CMD ["/bin/bash", "/app/e2e-verify.sh"]
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# e2e-verify.sh — Automated E2E verification for claude-mem plugin on OpenClaw
|
||||
#
|
||||
# This script verifies the complete plugin installation and operation flow:
|
||||
# 1. Plugin is installed and visible in OpenClaw
|
||||
# 2. Plugin loads correctly when gateway starts
|
||||
# 3. Mock worker SSE stream is consumed by the plugin
|
||||
# 4. Observations are received and formatted
|
||||
#
|
||||
# Exit 0 = all checks passed, Exit 1 = failure
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -32,11 +23,8 @@ section() {
|
||||
echo "=== $1 ==="
|
||||
}
|
||||
|
||||
# ─── Phase 1: Plugin Discovery ───
|
||||
|
||||
section "Phase 1: Plugin Discovery"
|
||||
|
||||
# Check plugin is listed
|
||||
PLUGIN_LIST=$(node /app/openclaw.mjs plugins list 2>&1)
|
||||
if echo "$PLUGIN_LIST" | grep -q "claude-mem"; then
|
||||
pass "Plugin appears in 'plugins list'"
|
||||
@@ -45,7 +33,6 @@ else
|
||||
echo "$PLUGIN_LIST"
|
||||
fi
|
||||
|
||||
# Check plugin info
|
||||
PLUGIN_INFO=$(node /app/openclaw.mjs plugins info claude-mem 2>&1 || true)
|
||||
if echo "$PLUGIN_INFO" | grep -qi "claude-mem"; then
|
||||
pass "Plugin info shows claude-mem details"
|
||||
@@ -54,11 +41,9 @@ else
|
||||
echo "$PLUGIN_INFO"
|
||||
fi
|
||||
|
||||
# Check plugin is enabled
|
||||
if echo "$PLUGIN_LIST" | grep -A1 "claude-mem" | grep -qi "enabled\|loaded"; then
|
||||
pass "Plugin is enabled"
|
||||
else
|
||||
# Try to check via info
|
||||
if echo "$PLUGIN_INFO" | grep -qi "enabled\|loaded"; then
|
||||
pass "Plugin is enabled (via info)"
|
||||
else
|
||||
@@ -67,7 +52,6 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check plugin doctor reports no issues
|
||||
DOCTOR_OUT=$(node /app/openclaw.mjs plugins doctor 2>&1 || true)
|
||||
if echo "$DOCTOR_OUT" | grep -qi "no.*issue\|0 issue"; then
|
||||
pass "Plugin doctor reports no issues"
|
||||
@@ -76,17 +60,12 @@ else
|
||||
echo "$DOCTOR_OUT"
|
||||
fi
|
||||
|
||||
# ─── Phase 2: Plugin Files ───
|
||||
|
||||
section "Phase 2: Plugin Files"
|
||||
|
||||
# Check extension directory exists
|
||||
EXTENSIONS_DIR="/home/node/.openclaw/extensions/openclaw-plugin"
|
||||
if [ ! -d "$EXTENSIONS_DIR" ]; then
|
||||
# Try alternative naming
|
||||
EXTENSIONS_DIR="/home/node/.openclaw/extensions/claude-mem"
|
||||
if [ ! -d "$EXTENSIONS_DIR" ]; then
|
||||
# Search for it
|
||||
FOUND_DIR=$(find /home/node/.openclaw/extensions/ -name "openclaw.plugin.json" -exec dirname {} \; 2>/dev/null | head -1 || true)
|
||||
if [ -n "$FOUND_DIR" ]; then
|
||||
EXTENSIONS_DIR="$FOUND_DIR"
|
||||
@@ -101,7 +80,6 @@ else
|
||||
ls -la /home/node/.openclaw/extensions/ 2>/dev/null || echo " (extensions dir not found)"
|
||||
fi
|
||||
|
||||
# Check key files exist
|
||||
for FILE in "openclaw.plugin.json" "dist/index.js" "package.json"; do
|
||||
if [ -f "$EXTENSIONS_DIR/$FILE" ]; then
|
||||
pass "File exists: $FILE"
|
||||
@@ -110,16 +88,12 @@ for FILE in "openclaw.plugin.json" "dist/index.js" "package.json"; do
|
||||
fi
|
||||
done
|
||||
|
||||
# ─── Phase 3: Mock Worker + Plugin Integration ───
|
||||
|
||||
section "Phase 3: Mock Worker + Plugin Integration"
|
||||
|
||||
# Start mock worker in background
|
||||
echo " Starting mock claude-mem worker..."
|
||||
node /app/mock-worker.js &
|
||||
MOCK_PID=$!
|
||||
|
||||
# Wait for mock worker to be ready
|
||||
for i in $(seq 1 10); do
|
||||
if curl -sf http://localhost:37777/health > /dev/null 2>&1; then
|
||||
break
|
||||
@@ -134,7 +108,6 @@ else
|
||||
kill $MOCK_PID 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Test SSE stream connectivity (curl with max-time to capture initial SSE frame)
|
||||
SSE_TEST=$(curl -s --max-time 2 http://localhost:37777/stream 2>/dev/null || true)
|
||||
if echo "$SSE_TEST" | grep -q "connected"; then
|
||||
pass "SSE stream returns connected event"
|
||||
@@ -143,13 +116,8 @@ else
|
||||
echo " Got: $(echo "$SSE_TEST" | head -5)"
|
||||
fi
|
||||
|
||||
# ─── Phase 4: Gateway + Plugin Load ───
|
||||
|
||||
section "Phase 4: Gateway Startup with Plugin"
|
||||
|
||||
# Create a minimal config that enables the plugin with the mock worker.
|
||||
# The memory slot must be set to "claude-mem" to match what `plugins install` configured.
|
||||
# Gateway auth is disabled via token for headless testing.
|
||||
mkdir -p /home/node/.openclaw
|
||||
cat > /home/node/.openclaw/openclaw.json << 'EOFCONFIG'
|
||||
{
|
||||
@@ -183,16 +151,13 @@ EOFCONFIG
|
||||
|
||||
pass "OpenClaw config written with plugin enabled"
|
||||
|
||||
# Start gateway in background and capture output
|
||||
GATEWAY_LOG="/tmp/gateway.log"
|
||||
echo " Starting OpenClaw gateway (timeout 15s)..."
|
||||
OPENCLAW_GATEWAY_TOKEN=e2e-test-token timeout 15 node /app/openclaw.mjs gateway --allow-unconfigured --verbose --token e2e-test-token > "$GATEWAY_LOG" 2>&1 &
|
||||
GATEWAY_PID=$!
|
||||
|
||||
# Give the gateway time to start and load plugins
|
||||
sleep 5
|
||||
|
||||
# Check if gateway started
|
||||
if kill -0 $GATEWAY_PID 2>/dev/null; then
|
||||
pass "Gateway process is running"
|
||||
else
|
||||
@@ -201,7 +166,6 @@ else
|
||||
cat "$GATEWAY_LOG" 2>/dev/null | tail -30
|
||||
fi
|
||||
|
||||
# Check gateway log for plugin load messages
|
||||
if grep -qi "claude-mem" "$GATEWAY_LOG" 2>/dev/null; then
|
||||
pass "Gateway log mentions claude-mem plugin"
|
||||
else
|
||||
@@ -210,29 +174,24 @@ else
|
||||
tail -20 "$GATEWAY_LOG" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Check for plugin loaded message
|
||||
if grep -q "plugin loaded" "$GATEWAY_LOG" 2>/dev/null || grep -q "v1.0.0" "$GATEWAY_LOG" 2>/dev/null; then
|
||||
pass "Plugin load message found in gateway log"
|
||||
else
|
||||
fail "Plugin load message not found"
|
||||
fi
|
||||
|
||||
# Check for observation feed messages
|
||||
if grep -qi "observation feed" "$GATEWAY_LOG" 2>/dev/null; then
|
||||
pass "Observation feed activity in gateway log"
|
||||
else
|
||||
fail "No observation feed activity detected"
|
||||
fi
|
||||
|
||||
# Check for SSE connection to mock worker
|
||||
if grep -qi "connected.*SSE\|SSE.*stream\|connecting.*SSE" "$GATEWAY_LOG" 2>/dev/null; then
|
||||
pass "SSE connection activity detected"
|
||||
else
|
||||
fail "No SSE connection activity in log"
|
||||
fi
|
||||
|
||||
# ─── Cleanup ───
|
||||
|
||||
section "Cleanup"
|
||||
kill $GATEWAY_PID 2>/dev/null || true
|
||||
kill $MOCK_PID 2>/dev/null || true
|
||||
@@ -240,8 +199,6 @@ wait $GATEWAY_PID 2>/dev/null || true
|
||||
wait $MOCK_PID 2>/dev/null || true
|
||||
echo " Processes stopped."
|
||||
|
||||
# ─── Summary ───
|
||||
|
||||
echo ""
|
||||
echo "==============================="
|
||||
echo " E2E Test Results"
|
||||
|
||||
+2
-250
@@ -1,27 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# claude-mem OpenClaw Plugin Installer
|
||||
# Installs the claude-mem persistent memory plugin for OpenClaw gateways.
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://install.cmem.ai/openclaw.sh | bash
|
||||
# # Or with options:
|
||||
# curl -fsSL https://install.cmem.ai/openclaw.sh | bash -s -- --provider=gemini --api-key=YOUR_KEY
|
||||
# # Direct execution:
|
||||
# bash install.sh [--non-interactive] [--upgrade] [--provider=claude|gemini|openrouter] [--api-key=KEY]
|
||||
|
||||
###############################################################################
|
||||
# Constants
|
||||
###############################################################################
|
||||
|
||||
readonly MIN_BUN_VERSION="1.1.14"
|
||||
readonly INSTALLER_VERSION="1.0.0"
|
||||
|
||||
###############################################################################
|
||||
# Argument parsing
|
||||
###############################################################################
|
||||
|
||||
NON_INTERACTIVE=""
|
||||
CLI_PROVIDER=""
|
||||
CLI_API_KEY=""
|
||||
@@ -68,37 +50,23 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
###############################################################################
|
||||
# TTY detection — ensure interactive prompts work under curl | bash
|
||||
# When piped, stdin reads from curl's output, not the terminal.
|
||||
# We open /dev/tty on fd 3 and read interactive input from there.
|
||||
###############################################################################
|
||||
|
||||
TTY_FD=0
|
||||
|
||||
setup_tty() {
|
||||
if [[ -t 0 ]]; then
|
||||
# stdin IS a terminal — use it directly
|
||||
TTY_FD=0
|
||||
elif [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
# In non-interactive mode, do not require /dev/tty
|
||||
TTY_FD=0
|
||||
elif [[ -r /dev/tty ]]; then
|
||||
# stdin is piped (curl | bash) but /dev/tty is available and readable
|
||||
exec 3</dev/tty
|
||||
TTY_FD=3
|
||||
else
|
||||
# No terminal available at all
|
||||
echo "Error: No terminal available for interactive prompts." >&2
|
||||
echo "Use --non-interactive or run directly: bash install.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Color utilities — auto-detect terminal color support
|
||||
###############################################################################
|
||||
|
||||
if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then
|
||||
readonly COLOR_RED='\033[0;31m'
|
||||
readonly COLOR_GREEN='\033[0;32m'
|
||||
@@ -132,17 +100,10 @@ prompt_user() {
|
||||
echo -en "${COLOR_CYAN}?${COLOR_RESET} $* "
|
||||
}
|
||||
|
||||
# Read a line from the terminal (works even when stdin is piped from curl)
|
||||
# Callers always pass -r via $@; shellcheck can't see through the delegation
|
||||
read_tty() {
|
||||
# shellcheck disable=SC2162
|
||||
read "$@" <&"$TTY_FD"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Global cleanup trap — removes temp directories on unexpected exit
|
||||
###############################################################################
|
||||
|
||||
CLEANUP_DIRS=()
|
||||
|
||||
register_cleanup_dir() {
|
||||
@@ -166,10 +127,6 @@ cleanup_on_exit() {
|
||||
|
||||
trap cleanup_on_exit EXIT
|
||||
|
||||
###############################################################################
|
||||
# Prerequisite checks
|
||||
###############################################################################
|
||||
|
||||
check_git() {
|
||||
if command -v git &>/dev/null; then
|
||||
return 0
|
||||
@@ -196,24 +153,17 @@ check_git() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Port conflict detection — check if port 37777 is already in use
|
||||
###############################################################################
|
||||
|
||||
check_port_37777() {
|
||||
local port_in_use=""
|
||||
|
||||
# Try lsof first (macOS/Linux)
|
||||
if command -v lsof &>/dev/null; then
|
||||
if lsof -i :37777 -sTCP:LISTEN &>/dev/null; then
|
||||
port_in_use="true"
|
||||
fi
|
||||
# Fallback to ss (Linux)
|
||||
elif command -v ss &>/dev/null; then
|
||||
if ss -tlnp 2>/dev/null | grep -q ':37777 '; then
|
||||
port_in_use="true"
|
||||
fi
|
||||
# Fallback to curl probe
|
||||
elif command -v curl &>/dev/null; then
|
||||
local response
|
||||
response="$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:37777/api/health" 2>/dev/null)" || true
|
||||
@@ -223,36 +173,23 @@ check_port_37777() {
|
||||
fi
|
||||
|
||||
if [[ "$port_in_use" == "true" ]]; then
|
||||
return 0 # port IS in use
|
||||
return 0
|
||||
fi
|
||||
return 1 # port is free
|
||||
return 1
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Upgrade detection — check if claude-mem is already installed
|
||||
###############################################################################
|
||||
|
||||
is_claude_mem_installed() {
|
||||
# Check if the plugin directory exists with the worker script
|
||||
if find_claude_mem_install_dir 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# JSON manipulation helper — jq with python3/node fallback
|
||||
# Usage: ensure_jq_or_fallback <json_file> <jq_filter> [jq_args...]
|
||||
# For simple read operations, returns the result on stdout.
|
||||
# For write operations, updates the file in-place.
|
||||
###############################################################################
|
||||
|
||||
ensure_jq_or_fallback() {
|
||||
local json_file="$1"
|
||||
shift
|
||||
local jq_filter="$1"
|
||||
shift
|
||||
# remaining args are passed as jq --arg pairs
|
||||
|
||||
if command -v jq &>/dev/null; then
|
||||
local tmp_file
|
||||
@@ -262,29 +199,16 @@ ensure_jq_or_fallback() {
|
||||
fi
|
||||
|
||||
if command -v python3 &>/dev/null; then
|
||||
# For complex jq filters, fall back to node instead
|
||||
# Python is used only for simple operations
|
||||
:
|
||||
fi
|
||||
|
||||
# Fallback to node (always available — it's a dependency)
|
||||
# This is a passthrough; callers that need node-specific logic
|
||||
# should use node -e directly. This function is for jq compatibility.
|
||||
warn "jq not found — using node for JSON manipulation"
|
||||
return 1
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Parse /api/health JSON response — extract worker metadata into globals
|
||||
# Uses jq → python3 → node fallback chain (matching installer conventions)
|
||||
# Sets: WORKER_VERSION, WORKER_AI_PROVIDER, WORKER_AI_AUTH_METHOD,
|
||||
# WORKER_INITIALIZED, WORKER_REPORTED_PID, WORKER_UPTIME
|
||||
###############################################################################
|
||||
|
||||
parse_health_json() {
|
||||
local raw_json="$1"
|
||||
|
||||
# Reset all health globals before parsing
|
||||
WORKER_VERSION=""
|
||||
WORKER_AI_PROVIDER=""
|
||||
WORKER_AI_AUTH_METHOD=""
|
||||
@@ -296,7 +220,6 @@ parse_health_json() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try jq first (fastest, most reliable)
|
||||
if command -v jq &>/dev/null; then
|
||||
WORKER_VERSION="$(echo "$raw_json" | jq -r '.version // empty' 2>/dev/null)" || true
|
||||
WORKER_AI_PROVIDER="$(echo "$raw_json" | jq -r '.ai.provider // empty' 2>/dev/null)" || true
|
||||
@@ -307,7 +230,6 @@ parse_health_json() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try python3 fallback
|
||||
if command -v python3 &>/dev/null; then
|
||||
local parsed
|
||||
parsed="$(INSTALLER_HEALTH_JSON="$raw_json" python3 -c "
|
||||
@@ -337,7 +259,6 @@ except Exception:
|
||||
WORKER_INITIALIZED="${health_fields[3]:-}"
|
||||
WORKER_REPORTED_PID="${health_fields[4]:-}"
|
||||
WORKER_UPTIME="${health_fields[5]:-}"
|
||||
# Normalize python's None/empty representations
|
||||
[[ "$WORKER_VERSION" == "None" ]] && WORKER_VERSION=""
|
||||
[[ "$WORKER_AI_PROVIDER" == "None" ]] && WORKER_AI_PROVIDER=""
|
||||
[[ "$WORKER_AI_AUTH_METHOD" == "None" ]] && WORKER_AI_AUTH_METHOD=""
|
||||
@@ -348,7 +269,6 @@ except Exception:
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback to node (always available — it's a dependency)
|
||||
local parsed
|
||||
parsed="$(INSTALLER_HEALTH_JSON="$raw_json" node -e "
|
||||
try {
|
||||
@@ -380,10 +300,6 @@ except Exception:
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Format uptime from milliseconds to human-readable (e.g., "2m 15s", "1h 23m")
|
||||
###############################################################################
|
||||
|
||||
format_uptime_ms() {
|
||||
local ms="$1"
|
||||
local secs=$((ms / 1000))
|
||||
@@ -396,10 +312,6 @@ format_uptime_ms() {
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Banner
|
||||
###############################################################################
|
||||
|
||||
print_banner() {
|
||||
echo -e "${COLOR_MAGENTA}${COLOR_BOLD}"
|
||||
cat << 'BANNER'
|
||||
@@ -413,10 +325,6 @@ BANNER
|
||||
echo ""
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Platform detection
|
||||
###############################################################################
|
||||
|
||||
PLATFORM=""
|
||||
IS_WSL=""
|
||||
|
||||
@@ -448,10 +356,6 @@ detect_platform() {
|
||||
info "Detected platform: ${PLATFORM}${IS_WSL:+ (WSL)}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Version comparison — returns 0 if $1 >= $2
|
||||
###############################################################################
|
||||
|
||||
version_gte() {
|
||||
local v1="$1" v2="$2"
|
||||
local -a parts1 parts2
|
||||
@@ -467,21 +371,14 @@ version_gte() {
|
||||
return 0
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Bun detection and installation
|
||||
# Translated from plugin/scripts/smart-install.js patterns
|
||||
###############################################################################
|
||||
|
||||
BUN_PATH=""
|
||||
|
||||
find_bun_path() {
|
||||
# Try PATH first
|
||||
if command -v bun &>/dev/null; then
|
||||
BUN_PATH="$(command -v bun)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check common installation paths (handles fresh installs before PATH reload)
|
||||
local -a bun_paths=(
|
||||
"${HOME}/.bun/bin/bun"
|
||||
"/usr/local/bin/bun"
|
||||
@@ -504,7 +401,6 @@ check_bun() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify minimum version
|
||||
local bun_version
|
||||
bun_version="$("$BUN_PATH" --version 2>/dev/null)" || return 1
|
||||
|
||||
@@ -529,7 +425,6 @@ install_bun() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Re-detect after install (installer may have placed it in ~/.bun/bin)
|
||||
if ! find_bun_path; then
|
||||
error "Bun installation completed but binary not found in expected locations"
|
||||
error "Please restart your terminal and re-run this installer."
|
||||
@@ -541,21 +436,14 @@ install_bun() {
|
||||
success "Bun ${bun_version} installed at ${BUN_PATH}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# uv detection and installation
|
||||
# Translated from plugin/scripts/smart-install.js patterns
|
||||
###############################################################################
|
||||
|
||||
UV_PATH=""
|
||||
|
||||
find_uv_path() {
|
||||
# Try PATH first
|
||||
if command -v uv &>/dev/null; then
|
||||
UV_PATH="$(command -v uv)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check common installation paths (handles fresh installs before PATH reload)
|
||||
local -a uv_paths=(
|
||||
"${HOME}/.local/bin/uv"
|
||||
"${HOME}/.cargo/bin/uv"
|
||||
@@ -597,7 +485,6 @@ install_uv() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Re-detect after install
|
||||
if ! find_uv_path; then
|
||||
error "uv installation completed but binary not found in expected locations"
|
||||
error "Please restart your terminal and re-run this installer."
|
||||
@@ -609,14 +496,9 @@ install_uv() {
|
||||
success "uv ${uv_version} installed at ${UV_PATH}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# OpenClaw gateway detection
|
||||
###############################################################################
|
||||
|
||||
OPENCLAW_PATH=""
|
||||
|
||||
find_openclaw() {
|
||||
# Try PATH first — check both "openclaw" and "openclaw.mjs" binary names
|
||||
for bin_name in openclaw openclaw.mjs; do
|
||||
if command -v "$bin_name" &>/dev/null; then
|
||||
OPENCLAW_PATH="$(command -v "$bin_name")"
|
||||
@@ -624,7 +506,6 @@ find_openclaw() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Check common installation paths
|
||||
local -a openclaw_paths=(
|
||||
"${HOME}/.openclaw/openclaw.mjs"
|
||||
"/usr/local/bin/openclaw.mjs"
|
||||
@@ -634,7 +515,6 @@ find_openclaw() {
|
||||
"${HOME}/.npm-global/bin/openclaw"
|
||||
)
|
||||
|
||||
# Also check for node_modules in common project locations
|
||||
if [[ -n "${NODE_PATH:-}" ]]; then
|
||||
openclaw_paths+=("${NODE_PATH}/openclaw/openclaw.mjs")
|
||||
fi
|
||||
@@ -667,7 +547,6 @@ check_openclaw() {
|
||||
success "OpenClaw gateway found at ${OPENCLAW_PATH}"
|
||||
}
|
||||
|
||||
# Run openclaw command — uses node for .mjs files, direct execution otherwise
|
||||
run_openclaw() {
|
||||
if [[ "$OPENCLAW_PATH" == *.mjs ]]; then
|
||||
node "$OPENCLAW_PATH" "$@"
|
||||
@@ -676,17 +555,10 @@ run_openclaw() {
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Plugin installation — clone, build, install, enable
|
||||
# Flow based on openclaw/Dockerfile.e2e
|
||||
###############################################################################
|
||||
|
||||
CLAUDE_MEM_REPO="https://github.com/thedotmack/claude-mem.git"
|
||||
CLAUDE_MEM_BRANCH="${CLI_BRANCH:-main}"
|
||||
PLUGIN_FRESHLY_INSTALLED=""
|
||||
|
||||
# Resolve the target extension directory.
|
||||
# Priority: existing installPath from config > plugins.load.paths > default.
|
||||
resolve_extension_dir() {
|
||||
local oc_config="${HOME}/.openclaw/openclaw.json"
|
||||
if [[ -f "$oc_config" ]] && command -v node &>/dev/null; then
|
||||
@@ -722,12 +594,10 @@ resolve_extension_dir() {
|
||||
CLAUDE_MEM_EXTENSION_DIR=""
|
||||
|
||||
install_plugin() {
|
||||
# Check for git before attempting clone
|
||||
check_git
|
||||
|
||||
CLAUDE_MEM_EXTENSION_DIR="$(resolve_extension_dir)"
|
||||
|
||||
# Remove existing plugin installation to allow clean re-install
|
||||
local existing_plugin_dir="$CLAUDE_MEM_EXTENSION_DIR"
|
||||
if [[ -d "$existing_plugin_dir" ]]; then
|
||||
info "Removing existing claude-mem plugin at ${existing_plugin_dir}..."
|
||||
@@ -747,7 +617,6 @@ install_plugin() {
|
||||
|
||||
local plugin_src="${build_dir}/claude-mem/openclaw"
|
||||
|
||||
# Build the TypeScript plugin
|
||||
info "Building TypeScript plugin..."
|
||||
if ! (cd "$plugin_src" && NODE_ENV=development npm install --ignore-scripts 2>&1 && npx tsc 2>&1); then
|
||||
error "Failed to build the claude-mem OpenClaw plugin"
|
||||
@@ -755,7 +624,6 @@ install_plugin() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create minimal installable package (matches Dockerfile.e2e pattern)
|
||||
local installable_dir="${build_dir}/claude-mem-installable"
|
||||
mkdir -p "${installable_dir}/dist"
|
||||
|
||||
@@ -763,7 +631,6 @@ install_plugin() {
|
||||
cp "${plugin_src}/dist/index.d.ts" "${installable_dir}/dist/" 2>/dev/null || true
|
||||
cp "${plugin_src}/openclaw.plugin.json" "${installable_dir}/"
|
||||
|
||||
# Generate the installable package.json with openclaw.extensions field
|
||||
INSTALLER_PACKAGE_DIR="$installable_dir" node -e "
|
||||
const pkg = {
|
||||
name: 'claude-mem',
|
||||
@@ -775,11 +642,6 @@ install_plugin() {
|
||||
require('fs').writeFileSync(process.env.INSTALLER_PACKAGE_DIR + '/package.json', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
|
||||
# Clean up stale claude-mem plugin entry before installing.
|
||||
# If the config references claude-mem but the plugin isn't installed,
|
||||
# OpenClaw's config validator blocks ALL CLI commands (including plugins install).
|
||||
# We temporarily remove the entry and save the config so `plugins install` can run,
|
||||
# then `plugins install` + `plugins enable` will re-create it properly.
|
||||
local oc_config="${HOME}/.openclaw/openclaw.json"
|
||||
local saved_plugin_config=""
|
||||
if [[ -f "$oc_config" ]]; then
|
||||
@@ -808,7 +670,6 @@ install_plugin() {
|
||||
" 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
# Install the plugin using OpenClaw's CLI
|
||||
info "Installing claude-mem plugin into OpenClaw..."
|
||||
if ! run_openclaw plugins install "$installable_dir" 2>&1; then
|
||||
error "Failed to install claude-mem plugin"
|
||||
@@ -816,7 +677,6 @@ install_plugin() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Enable the plugin
|
||||
info "Enabling claude-mem plugin..."
|
||||
if ! run_openclaw plugins enable claude-mem 2>&1; then
|
||||
error "Failed to enable claude-mem plugin"
|
||||
@@ -824,9 +684,6 @@ install_plugin() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure claude-mem is present in plugins.allow after successful install+enable.
|
||||
# Some OpenClaw environments require explicit allowlisting for local plugins.
|
||||
# This write is guaranteed: if config doesn't exist, configure_memory_slot() will create it.
|
||||
if [[ -f "$oc_config" ]]; then
|
||||
if ! INSTALLER_CONFIG_FILE="$oc_config" node -e "
|
||||
const fs = require('fs');
|
||||
@@ -845,10 +702,7 @@ install_plugin() {
|
||||
warn "Failed to write plugins.allow — claude-mem may need manual allowlisting"
|
||||
fi
|
||||
else
|
||||
# Config doesn't exist yet; configure_memory_slot() will create it with plugins.allow
|
||||
# We'll add claude-mem to the allowlist in a follow-up step after config is materialized
|
||||
info "OpenClaw config not yet materialized; will ensure allowlist in post-install"
|
||||
# Force config materialization by running a harmless OpenClaw command
|
||||
if run_openclaw status --json >/dev/null 2>&1 && [[ -f "$oc_config" ]]; then
|
||||
if ! INSTALLER_CONFIG_FILE="$oc_config" node -e "
|
||||
const fs = require('fs');
|
||||
@@ -867,8 +721,6 @@ install_plugin() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restore saved plugin config (workerPort, syncMemoryFile, observationFeed, etc.)
|
||||
# from any pre-existing installation that was temporarily removed above.
|
||||
if [[ -n "$saved_plugin_config" && "$saved_plugin_config" != "{}" ]]; then
|
||||
info "Restoring previous plugin configuration..."
|
||||
INSTALLER_CONFIG_FILE="$oc_config" INSTALLER_SAVED_CONFIG="$saved_plugin_config" node -e "
|
||||
@@ -885,23 +737,14 @@ install_plugin() {
|
||||
|
||||
success "claude-mem plugin installed and enabled"
|
||||
|
||||
# ── Copy core plugin files (worker, hooks, scripts) to extension directory ──
|
||||
# The OpenClaw extension only contains the gateway hook (dist/index.js).
|
||||
# The actual worker service and Claude Code hooks live in the plugin/ directory
|
||||
# of the main repo. We copy them so find_claude_mem_install_dir() can locate
|
||||
# the worker-service.cjs and the worker runs the updated version.
|
||||
local extension_dir="$CLAUDE_MEM_EXTENSION_DIR"
|
||||
local repo_root="${build_dir}/claude-mem"
|
||||
|
||||
if [[ -d "$extension_dir" && -d "${repo_root}/plugin" ]]; then
|
||||
info "Copying core plugin files to ${extension_dir}..."
|
||||
|
||||
# Copy plugin/ directory (worker service, hooks, scripts, skills, UI)
|
||||
cp -R "${repo_root}/plugin" "${extension_dir}/"
|
||||
|
||||
# Merge the canonical version from root package.json into the existing
|
||||
# extension package.json, preserving the openclaw.extensions field that
|
||||
# plugin discovery requires.
|
||||
local root_version
|
||||
root_version="$(node -e "console.log(require('${repo_root}/package.json').version)")"
|
||||
node -e "
|
||||
@@ -920,11 +763,6 @@ install_plugin() {
|
||||
PLUGIN_FRESHLY_INSTALLED="true"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Memory slot configuration
|
||||
# Sets plugins.slots.memory = "claude-mem" in ~/.openclaw/openclaw.json
|
||||
###############################################################################
|
||||
|
||||
configure_memory_slot() {
|
||||
local config_dir="${HOME}/.openclaw"
|
||||
local config_file="${config_dir}/openclaw.json"
|
||||
@@ -932,7 +770,6 @@ configure_memory_slot() {
|
||||
mkdir -p "$config_dir"
|
||||
|
||||
if [[ ! -f "$config_file" ]]; then
|
||||
# No config file exists — create one with the memory slot
|
||||
info "Creating OpenClaw configuration with claude-mem memory slot..."
|
||||
INSTALLER_CONFIG_FILE="$config_file" node -e "
|
||||
const config = {
|
||||
@@ -955,10 +792,8 @@ configure_memory_slot() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Config file exists — update it to set the memory slot
|
||||
info "Updating OpenClaw configuration to use claude-mem memory slot..."
|
||||
|
||||
# Use node for reliable JSON manipulation
|
||||
INSTALLER_CONFIG_FILE="$config_file" node -e "
|
||||
const fs = require('fs');
|
||||
const configPath = process.env.INSTALLER_CONFIG_FILE;
|
||||
@@ -998,11 +833,6 @@ configure_memory_slot() {
|
||||
success "Memory slot set to claude-mem in ${config_file}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# AI Provider setup — interactive provider selection
|
||||
# Reads defaults from SettingsDefaultsManager.ts (single source of truth)
|
||||
###############################################################################
|
||||
|
||||
AI_PROVIDER=""
|
||||
AI_PROVIDER_API_KEY=""
|
||||
|
||||
@@ -1026,7 +856,6 @@ setup_ai_provider() {
|
||||
info "AI Provider Configuration"
|
||||
echo ""
|
||||
|
||||
# Handle --provider flag (pre-selected via CLI)
|
||||
if [[ -n "$CLI_PROVIDER" ]]; then
|
||||
case "$CLI_PROVIDER" in
|
||||
claude)
|
||||
@@ -1060,7 +889,6 @@ setup_ai_provider() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Handle non-interactive mode (no --provider flag)
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
info "Non-interactive mode: defaulting to Claude Max Plan (no API key needed)"
|
||||
AI_PROVIDER="claude"
|
||||
@@ -1124,19 +952,12 @@ setup_ai_provider() {
|
||||
done
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Write settings.json — creates ~/.claude-mem/settings.json with all defaults
|
||||
# Schema: flat key-value (not nested { env: {...} })
|
||||
# Defaults sourced from SettingsDefaultsManager.ts
|
||||
###############################################################################
|
||||
|
||||
write_settings() {
|
||||
local settings_dir="${HOME}/.claude-mem"
|
||||
local settings_file="${settings_dir}/settings.json"
|
||||
|
||||
mkdir -p "$settings_dir"
|
||||
|
||||
# Pass provider and API key via environment variables to avoid shell-to-JS injection
|
||||
INSTALLER_AI_PROVIDER="$AI_PROVIDER" \
|
||||
INSTALLER_AI_API_KEY="$AI_PROVIDER_API_KEY" \
|
||||
INSTALLER_SETTINGS_FILE="$settings_file" \
|
||||
@@ -1226,11 +1047,6 @@ write_settings() {
|
||||
success "Settings written to ${settings_file}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Locate the installed claude-mem plugin directory
|
||||
# Checks common OpenClaw and Claude Code plugin install paths
|
||||
###############################################################################
|
||||
|
||||
CLAUDE_MEM_INSTALL_DIR=""
|
||||
|
||||
find_claude_mem_install_dir() {
|
||||
@@ -1250,7 +1066,6 @@ find_claude_mem_install_dir() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Fallback: search for the worker script under common plugin roots
|
||||
local -a roots=(
|
||||
"${HOME}/.openclaw"
|
||||
"${HOME}/.claude/plugins"
|
||||
@@ -1260,7 +1075,6 @@ find_claude_mem_install_dir() {
|
||||
local found
|
||||
found="$(find "$root" -name "worker-service.cjs" -path "*/plugin/scripts/*" 2>/dev/null | head -n 1)" || true
|
||||
if [[ -n "$found" ]]; then
|
||||
# Strip /plugin/scripts/worker-service.cjs to get the install dir
|
||||
CLAUDE_MEM_INSTALL_DIR="${found%/plugin/scripts/worker-service.cjs}"
|
||||
return 0
|
||||
fi
|
||||
@@ -1271,11 +1085,6 @@ find_claude_mem_install_dir() {
|
||||
return 1
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Worker service startup
|
||||
# Starts the claude-mem worker using bun in the background
|
||||
###############################################################################
|
||||
|
||||
WORKER_PID=""
|
||||
WORKER_VERSION=""
|
||||
WORKER_AI_PROVIDER=""
|
||||
@@ -1305,7 +1114,6 @@ start_worker() {
|
||||
|
||||
mkdir -p "$log_dir"
|
||||
|
||||
# Ensure bun path is available
|
||||
if [[ -z "$BUN_PATH" ]]; then
|
||||
if ! find_bun_path; then
|
||||
error "Bun not found — cannot start worker service"
|
||||
@@ -1313,12 +1121,10 @@ start_worker() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start worker in background with nohup
|
||||
CLAUDE_MEM_WORKER_PORT=37777 nohup "$BUN_PATH" "$worker_script" \
|
||||
>> "$log_file" 2>&1 &
|
||||
WORKER_PID=$!
|
||||
|
||||
# Write PID file for future management
|
||||
local pid_file="${HOME}/.claude-mem/worker.pid"
|
||||
mkdir -p "${HOME}/.claude-mem"
|
||||
INSTALLER_PID_FILE="$pid_file" INSTALLER_WORKER_PID="$WORKER_PID" node -e "
|
||||
@@ -1335,13 +1141,6 @@ start_worker() {
|
||||
info "Logs: ${log_file}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Health verification — two-stage: health (alive) then readiness (initialized)
|
||||
# Stage 1: Poll /api/health for HTTP 200 (worker process is running)
|
||||
# Stage 2: Poll /api/readiness for HTTP 200 (worker is fully initialized)
|
||||
# Total budget: 30 attempts (30 seconds) shared across both stages
|
||||
###############################################################################
|
||||
|
||||
verify_health() {
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
@@ -1351,7 +1150,6 @@ verify_health() {
|
||||
|
||||
info "Verifying worker health..."
|
||||
|
||||
# ── Stage 1: Wait for /api/health to return HTTP 200 (worker is alive) ──
|
||||
while (( attempt <= max_attempts )); do
|
||||
local http_status
|
||||
http_status="$(curl -s -o /dev/null -w "%{http_code}" "$health_url" 2>/dev/null)" || true
|
||||
@@ -1359,7 +1157,6 @@ verify_health() {
|
||||
if [[ "$http_status" == "200" ]]; then
|
||||
health_alive=true
|
||||
|
||||
# Fetch the full health response body and parse metadata
|
||||
local body
|
||||
body="$(curl -s "$health_url" 2>/dev/null)" || true
|
||||
parse_health_json "$body"
|
||||
@@ -1374,7 +1171,6 @@ verify_health() {
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
# If health never responded, the worker is not running at all
|
||||
if [[ "$health_alive" != "true" ]]; then
|
||||
warn "Worker health check timed out after ${max_attempts} attempts"
|
||||
warn "The worker may still be starting up. Check status with:"
|
||||
@@ -1383,7 +1179,6 @@ verify_health() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── Stage 2: Wait for /api/readiness to return HTTP 200 (fully initialized) ──
|
||||
attempt=$((attempt + 1))
|
||||
while (( attempt <= max_attempts )); do
|
||||
local readiness_status
|
||||
@@ -1399,17 +1194,12 @@ verify_health() {
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
# Readiness timed out but health is OK — worker is running, just not fully initialized yet
|
||||
warn "Worker is running but initialization is still in progress"
|
||||
warn "This is normal on first run — the worker will finish initializing in the background."
|
||||
warn "Check readiness with: curl http://127.0.0.1:37777/api/readiness"
|
||||
return 0
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Observation feed setup — optional interactive channel configuration
|
||||
###############################################################################
|
||||
|
||||
FEED_CHANNEL=""
|
||||
FEED_TARGET_ID=""
|
||||
FEED_CONFIGURED=false
|
||||
@@ -1531,10 +1321,6 @@ setup_observation_feed() {
|
||||
FEED_CONFIGURED=true
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Write observation feed config into ~/.openclaw/openclaw.json
|
||||
###############################################################################
|
||||
|
||||
write_observation_feed_config() {
|
||||
if [[ "$FEED_CONFIGURED" != "true" ]]; then
|
||||
return 0
|
||||
@@ -1550,7 +1336,6 @@ write_observation_feed_config() {
|
||||
|
||||
info "Writing observation feed configuration..."
|
||||
|
||||
# Use jq if available, fall back to python3, then node for JSON manipulation
|
||||
if command -v jq &>/dev/null; then
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
@@ -1592,7 +1377,6 @@ with open(config_path, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
"
|
||||
else
|
||||
# Fallback to node (always available since it's a dependency)
|
||||
INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \
|
||||
INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \
|
||||
INSTALLER_CONFIG_FILE="$config_file" \
|
||||
@@ -1638,10 +1422,6 @@ with open(config_path, 'w') as f:
|
||||
info "the feed is connected."
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Completion summary
|
||||
###############################################################################
|
||||
|
||||
print_completion_summary() {
|
||||
local provider_display=""
|
||||
case "$AI_PROVIDER" in
|
||||
@@ -1661,7 +1441,6 @@ print_completion_summary() {
|
||||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Dependencies installed (Bun, uv)"
|
||||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} OpenClaw gateway detected"
|
||||
|
||||
# Show installed version from health data if available
|
||||
if [[ -n "$WORKER_VERSION" ]]; then
|
||||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} claude-mem v${COLOR_BOLD}${WORKER_VERSION}${COLOR_RESET} installed and running"
|
||||
else
|
||||
@@ -1670,7 +1449,6 @@ print_completion_summary() {
|
||||
|
||||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Memory slot configured"
|
||||
|
||||
# Show AI provider with auth method from health data if available
|
||||
if [[ -n "$WORKER_AI_AUTH_METHOD" ]]; then
|
||||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} AI provider: ${COLOR_BOLD}${WORKER_AI_PROVIDER} (${WORKER_AI_AUTH_METHOD})${COLOR_RESET}"
|
||||
else
|
||||
@@ -1689,7 +1467,6 @@ print_completion_summary() {
|
||||
echo -e " ${COLOR_YELLOW}⚠${COLOR_RESET} Worker may not be running — check logs at ~/.claude-mem/logs/"
|
||||
fi
|
||||
|
||||
# Show initialization warning if worker is alive but not yet initialized
|
||||
if [[ "$WORKER_INITIALIZED" != "true" ]] && { [[ -n "$WORKER_REPORTED_PID" ]] || { [[ -n "$WORKER_PID" ]] && kill -0 "$WORKER_PID" 2>/dev/null; }; }; then
|
||||
echo -e " ${COLOR_YELLOW}⚠${COLOR_RESET} Worker is starting but still initializing (this is normal on first run)"
|
||||
fi
|
||||
@@ -1717,16 +1494,11 @@ print_completion_summary() {
|
||||
echo ""
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Main
|
||||
###############################################################################
|
||||
|
||||
main() {
|
||||
setup_tty
|
||||
print_banner
|
||||
detect_platform
|
||||
|
||||
# --- Step 1: Dependencies ---
|
||||
echo ""
|
||||
info "${COLOR_BOLD}[1/8]${COLOR_RESET} Checking dependencies..."
|
||||
echo ""
|
||||
@@ -1742,12 +1514,10 @@ main() {
|
||||
echo ""
|
||||
success "All dependencies satisfied"
|
||||
|
||||
# --- Step 2: OpenClaw gateway ---
|
||||
echo ""
|
||||
info "${COLOR_BOLD}[2/8]${COLOR_RESET} Locating OpenClaw gateway..."
|
||||
check_openclaw
|
||||
|
||||
# --- Step 3: Plugin installation (skip if upgrading and already installed) ---
|
||||
echo ""
|
||||
info "${COLOR_BOLD}[3/8]${COLOR_RESET} Installing claude-mem plugin..."
|
||||
|
||||
@@ -1758,22 +1528,18 @@ main() {
|
||||
install_plugin
|
||||
fi
|
||||
|
||||
# --- Step 4: Memory slot configuration ---
|
||||
echo ""
|
||||
info "${COLOR_BOLD}[4/8]${COLOR_RESET} Configuring memory slot..."
|
||||
configure_memory_slot
|
||||
|
||||
# --- Step 5: AI provider setup ---
|
||||
echo ""
|
||||
info "${COLOR_BOLD}[5/8]${COLOR_RESET} AI provider setup..."
|
||||
setup_ai_provider
|
||||
|
||||
# --- Step 6: Write settings ---
|
||||
echo ""
|
||||
info "${COLOR_BOLD}[6/8]${COLOR_RESET} Writing settings..."
|
||||
write_settings
|
||||
|
||||
# --- Step 7: Start worker and verify ---
|
||||
echo ""
|
||||
info "${COLOR_BOLD}[7/8]${COLOR_RESET} Starting worker service..."
|
||||
|
||||
@@ -1781,8 +1547,6 @@ main() {
|
||||
warn "Port 37777 is already in use (worker may already be running)"
|
||||
info "Checking if the existing service is healthy..."
|
||||
if verify_health; then
|
||||
# verify_health already called parse_health_json — WORKER_* globals are set.
|
||||
# Determine the expected version from the installed plugin's package.json.
|
||||
local expected_version=""
|
||||
if [[ -n "$CLAUDE_MEM_INSTALL_DIR" ]] || find_claude_mem_install_dir; then
|
||||
expected_version="$(INSTALLER_PKG="${CLAUDE_MEM_INSTALL_DIR}/package.json" node -e "
|
||||
@@ -1793,8 +1557,6 @@ main() {
|
||||
|
||||
local needs_restart=""
|
||||
|
||||
# If we just installed fresh plugin files, always restart the worker
|
||||
# to pick up the new version — even if the old worker was healthy.
|
||||
if [[ "$PLUGIN_FRESHLY_INSTALLED" == "true" ]]; then
|
||||
if [[ -n "$WORKER_VERSION" && -n "$expected_version" && "$WORKER_VERSION" != "$expected_version" ]]; then
|
||||
info "Upgrading worker from v${WORKER_VERSION} to v${expected_version}..."
|
||||
@@ -1804,32 +1566,26 @@ main() {
|
||||
needs_restart="true"
|
||||
fi
|
||||
|
||||
# Check if worker version is outdated compared to installed version
|
||||
if [[ "$needs_restart" != "true" && -n "$WORKER_VERSION" && -n "$expected_version" && "$WORKER_VERSION" != "$expected_version" ]]; then
|
||||
info "Upgrading worker from v${WORKER_VERSION} to v${expected_version}..."
|
||||
needs_restart="true"
|
||||
fi
|
||||
|
||||
# Check if AI provider doesn't match current configuration
|
||||
if [[ "$needs_restart" != "true" && -n "$WORKER_AI_PROVIDER" && -n "$AI_PROVIDER" && "$WORKER_AI_PROVIDER" != "$AI_PROVIDER" ]]; then
|
||||
warn "Worker is using ${WORKER_AI_PROVIDER} but you configured ${AI_PROVIDER} — restarting to apply"
|
||||
needs_restart="true"
|
||||
fi
|
||||
|
||||
# Restart worker if needed: kill old process, start fresh
|
||||
if [[ "$needs_restart" == "true" ]]; then
|
||||
info "Stopping existing worker..."
|
||||
# Try graceful shutdown via API first, fall back to SIGTERM
|
||||
curl -s -X POST "http://127.0.0.1:37777/api/admin/shutdown" >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
|
||||
# If still running, send SIGTERM to known PID
|
||||
if check_port_37777; then
|
||||
if [[ -n "$WORKER_REPORTED_PID" ]]; then
|
||||
kill "$WORKER_REPORTED_PID" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
# Check PID file as fallback
|
||||
local pid_file="${HOME}/.claude-mem/worker.pid"
|
||||
if [[ -f "$pid_file" ]]; then
|
||||
local file_pid
|
||||
@@ -1844,14 +1600,12 @@ main() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start fresh worker
|
||||
if start_worker; then
|
||||
verify_health || true
|
||||
else
|
||||
warn "Worker restart failed — you can start it manually later"
|
||||
fi
|
||||
else
|
||||
# No restart needed — show healthy status
|
||||
local uptime_display=""
|
||||
if [[ -n "$WORKER_UPTIME" && "$WORKER_UPTIME" =~ ^[0-9]+$ && "$WORKER_UPTIME" != "0" ]]; then
|
||||
uptime_display="$(format_uptime_ms "$WORKER_UPTIME")"
|
||||
@@ -1888,13 +1642,11 @@ main() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Step 8: Observation feed setup (optional) ---
|
||||
echo ""
|
||||
info "${COLOR_BOLD}[8/8]${COLOR_RESET} Observation feed setup..."
|
||||
setup_observation_feed
|
||||
write_observation_feed_config
|
||||
|
||||
# --- Completion ---
|
||||
print_completion_summary
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Claude-Mem (Persistent Memory)",
|
||||
"description": "Official OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
|
||||
"kind": "memory",
|
||||
"version": "10.4.1",
|
||||
"version": "12.5.1",
|
||||
"author": "thedotmack",
|
||||
"homepage": "https://claude-mem.com",
|
||||
"skills": ["skills/make-plan", "skills/do"],
|
||||
|
||||
@@ -241,7 +241,6 @@ describe("Observation I/O event handlers", () => {
|
||||
body: parsedBody,
|
||||
});
|
||||
|
||||
// Handle different endpoints
|
||||
if (req.url === "/api/health") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: "ok" }));
|
||||
@@ -311,7 +310,6 @@ describe("Observation I/O event handlers", () => {
|
||||
sessionId: "test-session-1",
|
||||
}, { sessionKey: "agent-1" });
|
||||
|
||||
// Wait for HTTP request
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const initRequest = receivedRequests.find((r) => r.url === "/api/sessions/init");
|
||||
@@ -358,11 +356,9 @@ describe("Observation I/O event handlers", () => {
|
||||
const { api, fireEvent } = createMockApi({ workerPort });
|
||||
claudeMemPlugin(api);
|
||||
|
||||
// Establish contentSessionId via session_start
|
||||
await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "test-agent" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Fire tool result event
|
||||
await fireEvent("tool_result_persist", {
|
||||
toolName: "Read",
|
||||
params: { file_path: "/src/index.ts" },
|
||||
@@ -420,11 +416,9 @@ describe("Observation I/O event handlers", () => {
|
||||
const { api, fireEvent } = createMockApi({ workerPort });
|
||||
claudeMemPlugin(api);
|
||||
|
||||
// Establish session
|
||||
await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "summarize-test" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Fire agent end
|
||||
await fireEvent("agent_end", {
|
||||
messages: [
|
||||
{ role: "user", content: "help me" },
|
||||
@@ -817,12 +811,10 @@ describe("SSE stream integration", () => {
|
||||
|
||||
await getService().start({});
|
||||
|
||||
// Wait for connection
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
assert.ok(logs.some((l) => l.includes("Connecting to SSE stream")));
|
||||
|
||||
// Send an SSE event
|
||||
const observation = {
|
||||
type: "new_observation",
|
||||
observation: {
|
||||
@@ -841,7 +833,6 @@ describe("SSE stream integration", () => {
|
||||
res.write(`data: ${JSON.stringify(observation)}\n\n`);
|
||||
}
|
||||
|
||||
// Wait for processing
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
@@ -863,7 +854,6 @@ describe("SSE stream integration", () => {
|
||||
await getService().start({});
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
// Send non-observation events
|
||||
for (const res of serverResponses) {
|
||||
res.write(`data: ${JSON.stringify({ type: "processing_status", isProcessing: true })}\n\n`);
|
||||
res.write(`data: ${JSON.stringify({ type: "session_started", sessionId: "abc" })}\n\n`);
|
||||
@@ -974,8 +964,6 @@ describe("SSE stream integration", () => {
|
||||
});
|
||||
|
||||
describe("circuit breaker", () => {
|
||||
// Reset circuit breaker state before each test by firing gateway_start.
|
||||
// The circuit is module-level state, so tests would otherwise bleed into each other.
|
||||
beforeEach(async () => {
|
||||
const { api, fireEvent } = createMockApi({ workerPort: 59999 });
|
||||
claudeMemPlugin(api);
|
||||
@@ -985,17 +973,12 @@ describe("circuit breaker", () => {
|
||||
it("opens after threshold failures and stops further requests", async () => {
|
||||
const { api, logs, fireEvent } = createMockApi({ workerPort: 59999 });
|
||||
claudeMemPlugin(api);
|
||||
// Reset circuit inside the test body to guard against timers from preceding
|
||||
// tests (e.g. completionDelayMs timers) that may fire between beforeEach and here.
|
||||
await fireEvent("gateway_start", {}, {});
|
||||
|
||||
// Fire threshold+1 calls so the circuit is open by the end of the loop
|
||||
// regardless of whether a concurrent timer fires at the exact boundary.
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-open-${i}` });
|
||||
}
|
||||
|
||||
// Circuit is now OPEN. Subsequent calls must be silently dropped.
|
||||
const logCountBeforeDrop = logs.length;
|
||||
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-drop" });
|
||||
const noisyDropLogs = logs.slice(logCountBeforeDrop).filter(
|
||||
@@ -1010,18 +993,14 @@ describe("circuit breaker", () => {
|
||||
await fireEvent("gateway_start", {}, {});
|
||||
const logsAfterReset = logs.length;
|
||||
|
||||
// Fire exactly threshold (3) calls
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-log-${i}` });
|
||||
}
|
||||
|
||||
const newLogs = logs.slice(logsAfterReset);
|
||||
// At least some failures should have been logged (circuit was active)
|
||||
assert.ok(newLogs.length > 0, "threshold calls should produce log output");
|
||||
// Exactly one disabling warning should appear
|
||||
const disablingLogs = newLogs.filter((l) => l.includes("disabling requests"));
|
||||
assert.equal(disablingLogs.length, 1, "should emit exactly one disabling warning when circuit opens");
|
||||
// The last call (the threshold-crossing one) should NOT log an individual failure
|
||||
const failureLogs = newLogs.filter((l) => l.includes("failed:"));
|
||||
assert.ok(failureLogs.length < 3, "threshold-crossing call should not log an individual failure");
|
||||
});
|
||||
@@ -1031,12 +1010,10 @@ describe("circuit breaker", () => {
|
||||
claudeMemPlugin(api);
|
||||
await fireEvent("gateway_start", {}, {});
|
||||
|
||||
// Open the circuit by firing threshold+1 calls
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-reset-${i}` });
|
||||
}
|
||||
|
||||
// Confirm circuit is open (call is silently dropped)
|
||||
const logCountWhileOpen = logs.length;
|
||||
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-while-open" });
|
||||
assert.equal(
|
||||
@@ -1045,10 +1022,8 @@ describe("circuit breaker", () => {
|
||||
"call while circuit is open should be silently dropped"
|
||||
);
|
||||
|
||||
// gateway_start resets the circuit
|
||||
await fireEvent("gateway_start", {}, {});
|
||||
|
||||
// Next call should attempt to connect again (not silently drop)
|
||||
const logCountAfterReset = logs.length;
|
||||
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-after-reset" });
|
||||
const newLogs = logs.slice(logCountAfterReset);
|
||||
@@ -1059,26 +1034,18 @@ describe("circuit breaker", () => {
|
||||
});
|
||||
|
||||
it("HALF_OPEN allows only a single probe — non-2xx keeps circuit open, 2xx closes it", async () => {
|
||||
// ---- Phase 1: open the circuit via network failures (unreachable port) ----
|
||||
// Reset circuit state first
|
||||
const resetMock = createMockApi({ workerPort: 59999 });
|
||||
claudeMemPlugin(resetMock.api);
|
||||
await resetMock.fireEvent("gateway_start", {}, {});
|
||||
|
||||
// Drive 4 failures to ensure circuit is OPEN
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await resetMock.fireEvent("before_agent_start", { prompt: "probe-test" }, { sessionKey: `probe-phase1-${i}` });
|
||||
}
|
||||
|
||||
// ---- Phase 2: advance clock so cooldown has elapsed ----
|
||||
// _circuitOpenedAt was set during Phase 1 using the real Date.now().
|
||||
// Advancing Date.now by 31s means the next circuitAllow call sees the cooldown elapsed.
|
||||
const realDateNow = Date.now.bind(Date);
|
||||
Date.now = () => realDateNow() + 31_000;
|
||||
|
||||
try {
|
||||
// ---- Phase 3: non-2xx probe — circuit should stay OPEN ----
|
||||
// Start a server that returns 500 for all requests
|
||||
let serverA: Server | null = null;
|
||||
const portA: number = await new Promise((resolve) => {
|
||||
serverA = createServer((_req: IncomingMessage, res: ServerResponse) => {
|
||||
@@ -1091,31 +1058,19 @@ describe("circuit breaker", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Reuse the same module-level circuit state — just change the worker port.
|
||||
// Create a new mock api instance pointed at server A (500 responder).
|
||||
const mockA = createMockApi({ workerPort: portA });
|
||||
claudeMemPlugin(mockA.api);
|
||||
// Do NOT fire gateway_start here — we want the OPEN circuit state from Phase 1.
|
||||
|
||||
// The circuit is OPEN but the mocked clock says cooldown elapsed.
|
||||
// The next call should: transition to HALF_OPEN, set _halfOpenProbeInFlight=true,
|
||||
// send the probe to server A (which returns 500), then call circuitOnFailure
|
||||
// and re-open the circuit.
|
||||
const logCountAtProbe = mockA.logs.length;
|
||||
await mockA.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-call-non2xx" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const probeALogs = mockA.logs.slice(logCountAtProbe);
|
||||
// After a 500 response, circuitOnFailure is called which logs "disabling requests"
|
||||
// (because state was HALF_OPEN) and logger.warn logs the 500 status.
|
||||
assert.ok(
|
||||
probeALogs.some((l) => l.includes("disabling") || l.includes("returned 500") || l.includes("Worker POST")),
|
||||
"non-2xx probe should keep circuit open (expected disabling or 500 status log)"
|
||||
);
|
||||
|
||||
// Verify probe flag resets: a second call with cooldown elapsed should be allowed as a new probe
|
||||
// (i.e., _halfOpenProbeInFlight was cleared by circuitOnFailure).
|
||||
// But without advancing time further the circuit is OPEN again — so calls are dropped.
|
||||
const logCountAfterFailedProbe = mockA.logs.length;
|
||||
await mockA.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-concurrent" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
@@ -1126,22 +1081,14 @@ describe("circuit breaker", () => {
|
||||
|
||||
serverA!.close();
|
||||
|
||||
// ---- Phase 4: 2xx probe — circuit should close ----
|
||||
// Re-open the circuit with fresh failures, then probe with a 200-returning server.
|
||||
// Reset circuit state first.
|
||||
const resetMock2 = createMockApi({ workerPort: 59999 });
|
||||
claudeMemPlugin(resetMock2.api);
|
||||
await resetMock2.fireEvent("gateway_start", {}, {});
|
||||
|
||||
// Drive failures (still using mocked Date.now, but _circuitOpenedAt will be set to
|
||||
// the mocked time, so cooldown is NOT elapsed yet from the mocked perspective).
|
||||
// We need to temporarily restore real Date.now while opening the circuit, then
|
||||
// re-mock it for the probe.
|
||||
Date.now = realDateNow;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await resetMock2.fireEvent("before_agent_start", { prompt: "probe-test" }, { sessionKey: `probe-phase4-${i}` });
|
||||
}
|
||||
// Re-advance the clock past cooldown
|
||||
Date.now = () => realDateNow() + 31_000;
|
||||
|
||||
let serverB: Server | null = null;
|
||||
@@ -1158,7 +1105,6 @@ describe("circuit breaker", () => {
|
||||
|
||||
const mockB = createMockApi({ workerPort: portB });
|
||||
claudeMemPlugin(mockB.api);
|
||||
// Do NOT fire gateway_start — reuse OPEN circuit state from resetMock2.
|
||||
|
||||
const logCountBeforeSuccessProbe = mockB.logs.length;
|
||||
await mockB.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-call-2xx" });
|
||||
|
||||
+2
-132
@@ -1,9 +1,4 @@
|
||||
// No file-system imports needed — context is injected via system prompt hook,
|
||||
// not by writing to MEMORY.md.
|
||||
|
||||
// Minimal type declarations for the OpenClaw Plugin SDK.
|
||||
// These match the real OpenClawPluginApi provided by the gateway at runtime.
|
||||
// See: https://docs.openclaw.ai/plugin
|
||||
|
||||
interface PluginLogger {
|
||||
debug?: (message: string) => void;
|
||||
@@ -30,7 +25,6 @@ interface PluginCommandContext {
|
||||
|
||||
type PluginCommandResult = string | { text: string } | { text: string; format?: string };
|
||||
|
||||
// OpenClaw event types for agent lifecycle
|
||||
interface BeforeAgentStartEvent {
|
||||
prompt?: string;
|
||||
}
|
||||
@@ -136,10 +130,6 @@ interface OpenClawPluginApi {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SSE Observation Feed Types
|
||||
// ============================================================================
|
||||
|
||||
interface ObservationSSEPayload {
|
||||
id: number;
|
||||
memory_session_id: string;
|
||||
@@ -166,10 +156,6 @@ interface SSENewObservationEvent {
|
||||
|
||||
type ConnectionState = "disconnected" | "connected" | "reconnecting";
|
||||
|
||||
// ============================================================================
|
||||
// Plugin Configuration
|
||||
// ============================================================================
|
||||
|
||||
interface FeedEmojiConfig {
|
||||
primary?: string;
|
||||
claudeCode?: string;
|
||||
@@ -193,16 +179,10 @@ interface ClaudeMemPluginConfig {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
|
||||
const MAX_SSE_BUFFER_SIZE = 1024 * 1024;
|
||||
const DEFAULT_WORKER_PORT = 37777;
|
||||
const DEFAULT_WORKER_HOST = "127.0.0.1";
|
||||
|
||||
// Emoji pool for deterministic auto-assignment to unknown agents.
|
||||
// Uses a hash of the agentId to pick a consistent emoji — no persistent state needed.
|
||||
const EMOJI_POOL = [
|
||||
"🔧","📐","🔍","💻","🧪","🐛","🛡️","☁️","📦","🎯",
|
||||
"🔮","⚡","🌊","🎨","📊","🚀","🔬","🏗️","📝","🎭",
|
||||
@@ -216,7 +196,6 @@ function poolEmojiForAgent(agentId: string): string {
|
||||
return EMOJI_POOL[Math.abs(hash) % EMOJI_POOL.length];
|
||||
}
|
||||
|
||||
// Default emoji values — overridden by user config via observationFeed.emojis
|
||||
const DEFAULT_PRIMARY_EMOJI = "🦞";
|
||||
const DEFAULT_CLAUDE_CODE_EMOJI = "⌨️";
|
||||
const DEFAULT_CLAUDE_CODE_LABEL = "Claude Code Session";
|
||||
@@ -233,19 +212,15 @@ function buildGetSourceLabel(
|
||||
|
||||
return function getSourceLabel(project: string | null | undefined): string {
|
||||
if (!project) return fallback;
|
||||
// OpenClaw agent projects are formatted as "openclaw-<agentId>"
|
||||
if (project.startsWith("openclaw-")) {
|
||||
const agentId = project.slice("openclaw-".length);
|
||||
if (!agentId) return `${primary} openclaw`;
|
||||
const emoji = pinnedAgents[agentId] || poolEmojiForAgent(agentId);
|
||||
return `${emoji} ${agentId}`;
|
||||
}
|
||||
// OpenClaw project without agent suffix
|
||||
if (project === "openclaw") {
|
||||
return `${primary} openclaw`;
|
||||
}
|
||||
// Everything else is a Claude Code session. Keep the project identifier
|
||||
// visible so concurrent sessions can be distinguished in the feed.
|
||||
const trimmedLabel = claudeCodeLabel.trim();
|
||||
if (!trimmedLabel) {
|
||||
return `${claudeCode} ${project}`;
|
||||
@@ -254,24 +229,12 @@ function buildGetSourceLabel(
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Worker HTTP Client
|
||||
// ============================================================================
|
||||
|
||||
let _workerHost = DEFAULT_WORKER_HOST;
|
||||
|
||||
function workerBaseUrl(port: number): string {
|
||||
return `http://${_workerHost}:${port}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Worker Circuit Breaker
|
||||
// ============================================================================
|
||||
// Prevents CPU-spinning retry loops when the worker is unreachable.
|
||||
// After CIRCUIT_BREAKER_THRESHOLD consecutive network errors, the circuit
|
||||
// opens and all worker calls are silently dropped for CIRCUIT_BREAKER_COOLDOWN_MS.
|
||||
// After the cooldown, one probe attempt is allowed to check if the worker recovered.
|
||||
|
||||
const CIRCUIT_BREAKER_THRESHOLD = 3;
|
||||
const CIRCUIT_BREAKER_COOLDOWN_MS = 30_000;
|
||||
|
||||
@@ -294,7 +257,6 @@ function circuitAllow(logger: PluginLogger): boolean {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// HALF_OPEN: allow one probe through
|
||||
if (_halfOpenProbeInFlight) return false;
|
||||
_halfOpenProbeInFlight = true;
|
||||
return true;
|
||||
@@ -429,10 +391,6 @@ async function workerGetJson(
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SSE Observation Feed
|
||||
// ============================================================================
|
||||
|
||||
function formatObservationMessage(
|
||||
observation: ObservationSSEPayload,
|
||||
getSourceLabel: (project: string | null | undefined) => string,
|
||||
@@ -446,8 +404,6 @@ function formatObservationMessage(
|
||||
return message;
|
||||
}
|
||||
|
||||
// Explicit mapping from channel name to [runtime namespace key, send function name].
|
||||
// These match the PluginRuntime.channel structure in the OpenClaw SDK.
|
||||
const CHANNEL_SEND_MAP: Record<string, { namespace: string; functionName: string }> = {
|
||||
telegram: { namespace: "telegram", functionName: "sendMessageTelegram" },
|
||||
whatsapp: { namespace: "whatsapp", functionName: "sendMessageWhatsApp" },
|
||||
@@ -491,7 +447,6 @@ function sendToChannel(
|
||||
text: string,
|
||||
botToken?: string
|
||||
): Promise<void> {
|
||||
// If a dedicated bot token is provided for Telegram, send directly
|
||||
if (botToken && channel === "telegram") {
|
||||
return sendDirectTelegram(botToken, to, text, api.logger);
|
||||
}
|
||||
@@ -514,7 +469,6 @@ function sendToChannel(
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// WhatsApp requires a third options argument with { verbose: boolean }
|
||||
const args: unknown[] = channel === "whatsapp"
|
||||
? [to, text, { verbose: false }]
|
||||
: [to, text];
|
||||
@@ -579,7 +533,6 @@ async function connectToSSEStream(
|
||||
buffer = frames.pop() || "";
|
||||
|
||||
for (const frame of frames) {
|
||||
// SSE spec: concatenate all data: lines with \n
|
||||
const dataLines = frame
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
@@ -620,10 +573,6 @@ async function connectToSSEStream(
|
||||
setConnectionState("disconnected");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Plugin Entry Point
|
||||
// ============================================================================
|
||||
|
||||
export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig;
|
||||
const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT;
|
||||
@@ -638,14 +587,11 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
return baseProjectName;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Session tracking for observation I/O
|
||||
// ------------------------------------------------------------------
|
||||
const sessionIds = new Map<string, string>();
|
||||
const canonicalSessionKeys = new Map<string, string>();
|
||||
const sessionAliasesByCanonicalKey = new Map<string, Set<string>>();
|
||||
const recentPromptInits = new Map<string, number>();
|
||||
const syncMemoryFile = userConfig.syncMemoryFile !== false; // default true
|
||||
const syncMemoryFile = userConfig.syncMemoryFile !== false;
|
||||
const syncMemoryFileExclude = new Set(userConfig.syncMemoryFileExclude || []);
|
||||
|
||||
function getContentSessionId(sessionKey?: string): string {
|
||||
@@ -707,9 +653,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
}
|
||||
const cacheKey = `${contentSessionId}::${project}::${prompt}`;
|
||||
const lastSeenAt = recentPromptInits.get(cacheKey);
|
||||
// Note: cache is set unconditionally before return. If workerPost fails
|
||||
// after this check, a retry within 2s would be incorrectly skipped.
|
||||
// Acceptable because before_agent_start is not retried by the runtime.
|
||||
recentPromptInits.set(cacheKey, now);
|
||||
return typeof lastSeenAt === "number" && now - lastSeenAt <= 2000;
|
||||
}
|
||||
@@ -728,14 +671,10 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
sessionIds.delete(canonicalKey);
|
||||
}
|
||||
|
||||
// TTL cache for context injection to avoid re-fetching on every LLM turn.
|
||||
// before_prompt_build fires on every turn; caching for 60s keeps the worker
|
||||
// load manageable while still picking up new observations reasonably quickly.
|
||||
const CONTEXT_CACHE_TTL_MS = 60_000;
|
||||
const contextCache = new Map<string, { text: string; fetchedAt: number }>();
|
||||
|
||||
async function getContextForPrompt(ctx?: EventContext): Promise<string | null> {
|
||||
// Include both the base project and agent-scoped project (e.g. "openclaw" + "openclaw-main")
|
||||
const projects = [baseProjectName];
|
||||
const agentProject = ctx ? getProjectName(ctx) : null;
|
||||
if (agentProject && agentProject !== baseProjectName) {
|
||||
@@ -743,7 +682,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
}
|
||||
const cacheKey = projects.join(",");
|
||||
|
||||
// Return cached context if still fresh
|
||||
const cached = contextCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.fetchedAt < CONTEXT_CACHE_TTL_MS) {
|
||||
return cached.text;
|
||||
@@ -762,36 +700,21 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: session_start — track session (fires on /new, /reset)
|
||||
// Init is deferred to before_agent_start to avoid duplicate prompt records.
|
||||
// ------------------------------------------------------------------
|
||||
api.on("session_start", async (_event, ctx) => {
|
||||
const { contentSessionId } = rememberSessionContext(ctx);
|
||||
api.logger.info(`[claude-mem] Session tracking initialized: ${contentSessionId}`);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: message_received — alias tracking only; init deferred to before_agent_start
|
||||
// ------------------------------------------------------------------
|
||||
api.on("message_received", async (event, ctx) => {
|
||||
const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
|
||||
api.logger.info(`[claude-mem] Message received — prompt capture deferred to before_agent_start: session=${canonicalKey} contentSessionId=${contentSessionId} hasContent=${Boolean(event.content)}`);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: after_compaction — preserve session tracking after context compaction.
|
||||
// Re-init is intentionally NOT called here; the worker retains session state
|
||||
// independently and re-initializing would create duplicate prompt records.
|
||||
// ------------------------------------------------------------------
|
||||
api.on("after_compaction", async (_event, ctx) => {
|
||||
const { contentSessionId } = rememberSessionContext(ctx);
|
||||
api.logger.info(`[claude-mem] Session preserved after compaction: ${contentSessionId}`);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: before_agent_start — single init point with dedup guard
|
||||
// ------------------------------------------------------------------
|
||||
api.on("before_agent_start", async (event, ctx) => {
|
||||
const { contentSessionId } = rememberSessionContext(ctx);
|
||||
const projectName = getProjectName(ctx);
|
||||
@@ -802,8 +725,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize session in the worker so observations are not skipped
|
||||
// (the privacy check requires a stored user prompt to exist)
|
||||
await workerPost(workerPort, "/api/sessions/init", {
|
||||
contentSessionId,
|
||||
project: projectName,
|
||||
@@ -813,14 +734,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
api.logger.info(`[claude-mem] Session initialized via before_agent_start: contentSessionId=${contentSessionId} project=${projectName}`);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: before_prompt_build — inject context into system prompt
|
||||
//
|
||||
// Instead of writing to MEMORY.md (which conflicts with agent-curated
|
||||
// memory), inject the observation timeline via appendSystemContext.
|
||||
// This keeps MEMORY.md under the agent's control while still providing
|
||||
// cross-session context to the LLM.
|
||||
// ------------------------------------------------------------------
|
||||
api.on("before_prompt_build", async (_event, ctx) => {
|
||||
if (!shouldInjectContext(ctx)) return;
|
||||
|
||||
@@ -831,20 +744,15 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: tool_result_persist — record tool observations
|
||||
// ------------------------------------------------------------------
|
||||
api.on("tool_result_persist", (event, ctx) => {
|
||||
api.logger.info(`[claude-mem] tool_result_persist fired: tool=${event.toolName ?? "unknown"} agent=${ctx.agentId ?? "none"} session=${ctx.sessionKey ?? "none"}`);
|
||||
const toolName = event.toolName;
|
||||
if (!toolName) return;
|
||||
|
||||
// Skip memory_ tools to prevent recursive observation loops
|
||||
if (toolName.startsWith("memory_")) return;
|
||||
|
||||
const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
|
||||
|
||||
// Extract result text from all content blocks
|
||||
let toolResponseText = "";
|
||||
const content = event.message?.content;
|
||||
if (Array.isArray(content)) {
|
||||
@@ -854,15 +762,11 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// Truncate long responses to prevent oversized payloads
|
||||
const MAX_TOOL_RESPONSE_LENGTH = 1000;
|
||||
if (toolResponseText.length > MAX_TOOL_RESPONSE_LENGTH) {
|
||||
toolResponseText = toolResponseText.slice(0, MAX_TOOL_RESPONSE_LENGTH);
|
||||
}
|
||||
|
||||
// Resolve workspaceDir with fallback chain.
|
||||
// Empty cwd causes worker-side observation queueing failures,
|
||||
// so we drop the observation rather than sending cwd: "".
|
||||
const workspaceDir = ctx.workspaceDir;
|
||||
|
||||
if (!workspaceDir) {
|
||||
@@ -870,7 +774,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire-and-forget: send observation to worker
|
||||
workerPostFireAndForget(workerPort, "/api/sessions/observations", {
|
||||
contentSessionId,
|
||||
tool_name: toolName,
|
||||
@@ -880,13 +783,9 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
}, api.logger);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: agent_end — summarize session (worker self-completes)
|
||||
// ------------------------------------------------------------------
|
||||
api.on("agent_end", async (event, ctx) => {
|
||||
const { contentSessionId } = rememberSessionContext(ctx);
|
||||
|
||||
// Extract last assistant message for summarization
|
||||
let lastAssistantMessage = "";
|
||||
if (Array.isArray(event.messages)) {
|
||||
for (let i = event.messages.length - 1; i >= 0; i--) {
|
||||
@@ -905,25 +804,17 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Send summarize. The worker self-completes the session when its SDK-agent
|
||||
// generator drains; no explicit complete call needed.
|
||||
await workerPost(workerPort, "/api/sessions/summarize", {
|
||||
contentSessionId,
|
||||
last_assistant_message: lastAssistantMessage,
|
||||
}, api.logger);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: session_end — clean up session tracking to prevent unbounded growth
|
||||
// ------------------------------------------------------------------
|
||||
api.on("session_end", async (_event, ctx) => {
|
||||
clearSessionContext(ctx);
|
||||
api.logger.info(`[claude-mem] Session tracking cleaned up`);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: gateway_start — clear session tracking for fresh start
|
||||
// ------------------------------------------------------------------
|
||||
api.on("gateway_start", async () => {
|
||||
circuitReset();
|
||||
sessionIds.clear();
|
||||
@@ -934,9 +825,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
api.logger.info("[claude-mem] Gateway started — session tracking reset");
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Service: SSE observation feed → messaging channels
|
||||
// ------------------------------------------------------------------
|
||||
let sseAbortController: AbortController | null = null;
|
||||
let connectionState: ConnectionState = "disconnected";
|
||||
let connectionPromise: Promise<void> | null = null;
|
||||
@@ -1014,9 +902,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
return Math.max(1, Math.min(50, Math.trunc(parsed)));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Command: /claude_mem_feed — status & toggle
|
||||
// ------------------------------------------------------------------
|
||||
api.registerCommand({
|
||||
name: "claude_mem_feed",
|
||||
description: "Show or toggle Claude-Mem observation feed status",
|
||||
@@ -1050,10 +935,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
},
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Command: /claude-mem-search — query worker search API
|
||||
// Usage: /claude-mem-search <query> [limit]
|
||||
// ------------------------------------------------------------------
|
||||
api.registerCommand({
|
||||
name: "claude-mem-search",
|
||||
description: "Search Claude-Mem observations by query",
|
||||
@@ -1088,10 +969,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
},
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Command: /claude-mem-recent — recent context snapshot
|
||||
// Usage: /claude-mem-recent [project] [limit]
|
||||
// ------------------------------------------------------------------
|
||||
api.registerCommand({
|
||||
name: "claude-mem-recent",
|
||||
description: "Show recent Claude-Mem context for a project",
|
||||
@@ -1131,10 +1008,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
},
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Command: /claude-mem-timeline — search and timeline around best match
|
||||
// Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]
|
||||
// ------------------------------------------------------------------
|
||||
api.registerCommand({
|
||||
name: "claude-mem-timeline",
|
||||
description: "Find best memory match and show nearby timeline events",
|
||||
@@ -1185,9 +1058,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
},
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Command: /claude_mem_status — worker health check
|
||||
// ------------------------------------------------------------------
|
||||
api.registerCommand({
|
||||
name: "claude_mem_status",
|
||||
description: "Check Claude-Mem worker health and session status",
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-e2e.sh — Run E2E test of claude-mem plugin on real OpenClaw
|
||||
#
|
||||
# Usage:
|
||||
# ./test-e2e.sh # Automated E2E test (build + run + verify)
|
||||
# ./test-e2e.sh --interactive # Drop into shell for manual testing
|
||||
# ./test-e2e.sh --build-only # Just build the image, don't run
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test suite for openclaw/install.sh functions
|
||||
# Tests the OpenClaw gateway detection, plugin install, and memory slot config.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
INSTALL_SCRIPT="${SCRIPT_DIR}/install.sh"
|
||||
|
||||
@@ -11,10 +8,6 @@ TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
###############################################################################
|
||||
# Test helpers
|
||||
###############################################################################
|
||||
|
||||
test_pass() {
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
@@ -57,30 +50,17 @@ assert_file_exists() {
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Source the install script without running main()
|
||||
# We override main to be a no-op, then source the file.
|
||||
###############################################################################
|
||||
|
||||
source_install_functions() {
|
||||
# Create a temp file that overrides main and sources the install script
|
||||
local tmp_source
|
||||
tmp_source="$(mktemp)"
|
||||
# Extract everything except the final `main "$@"` invocation
|
||||
sed '$ d' "$INSTALL_SCRIPT" > "$tmp_source"
|
||||
# Override main to prevent execution
|
||||
echo 'main() { :; }' >> "$tmp_source"
|
||||
# Source it (suppress color output for cleaner tests)
|
||||
TERM=dumb source "$tmp_source"
|
||||
rm -f "$tmp_source"
|
||||
}
|
||||
|
||||
source_install_functions
|
||||
|
||||
###############################################################################
|
||||
# Test: detect_platform() — returns a valid platform string
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== detect_platform() ==="
|
||||
|
||||
@@ -118,7 +98,6 @@ test_detect_platform_is_idempotent() {
|
||||
test_detect_platform_is_idempotent
|
||||
|
||||
test_detect_platform_sets_iswsl_empty_on_non_wsl() {
|
||||
# Unless actually running on WSL, IS_WSL should be empty
|
||||
PLATFORM=""
|
||||
IS_WSL=""
|
||||
detect_platform >/dev/null 2>&1
|
||||
@@ -132,15 +111,10 @@ test_detect_platform_sets_iswsl_empty_on_non_wsl() {
|
||||
|
||||
test_detect_platform_sets_iswsl_empty_on_non_wsl
|
||||
|
||||
###############################################################################
|
||||
# Test: check_bun() — correctly detects bun presence/absence
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== check_bun() ==="
|
||||
|
||||
test_check_bun_detects_installed_bun() {
|
||||
# If bun is installed on this system, check_bun should succeed
|
||||
if command -v bun &>/dev/null; then
|
||||
BUN_PATH=""
|
||||
if check_bun >/dev/null 2>&1; then
|
||||
@@ -197,15 +171,12 @@ test_find_bun_path_checks_home_bun_bin() {
|
||||
HOME="$fake_home"
|
||||
BUN_PATH=""
|
||||
|
||||
# Create a fake bun binary in ~/.bun/bin/
|
||||
mkdir -p "${fake_home}/.bun/bin"
|
||||
cat > "${fake_home}/.bun/bin/bun" <<'FAKEBUN'
|
||||
#!/bin/bash
|
||||
echo "1.2.0"
|
||||
FAKEBUN
|
||||
chmod +x "${fake_home}/.bun/bin/bun"
|
||||
|
||||
# Hide bun from PATH
|
||||
local saved_path="$PATH"
|
||||
PATH="/nonexistent"
|
||||
|
||||
@@ -222,15 +193,10 @@ FAKEBUN
|
||||
|
||||
test_find_bun_path_checks_home_bun_bin
|
||||
|
||||
###############################################################################
|
||||
# Test: check_uv() — correctly detects uv presence/absence
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== check_uv() ==="
|
||||
|
||||
test_check_uv_detects_installed_uv() {
|
||||
# If uv is installed on this system, check_uv should succeed
|
||||
if command -v uv &>/dev/null; then
|
||||
UV_PATH=""
|
||||
if check_uv >/dev/null 2>&1; then
|
||||
@@ -253,9 +219,6 @@ test_check_uv_detects_installed_uv() {
|
||||
test_check_uv_detects_installed_uv
|
||||
|
||||
test_check_uv_fails_when_not_found() {
|
||||
# find_uv_path checks hardcoded system paths (/usr/local/bin/uv,
|
||||
# /opt/homebrew/bin/uv) that we can't override without root.
|
||||
# Skip if uv exists at any of those absolute paths.
|
||||
if [[ -x "/usr/local/bin/uv" ]] || [[ -x "/opt/homebrew/bin/uv" ]]; then
|
||||
test_pass "check_uv not-found test: skipped (uv installed at system path)"
|
||||
return 0
|
||||
@@ -295,15 +258,12 @@ test_find_uv_path_checks_local_bin() {
|
||||
HOME="$fake_home"
|
||||
UV_PATH=""
|
||||
|
||||
# Create a fake uv binary in ~/.local/bin/
|
||||
mkdir -p "${fake_home}/.local/bin"
|
||||
cat > "${fake_home}/.local/bin/uv" <<'FAKEUV'
|
||||
#!/bin/bash
|
||||
echo "uv 0.4.0"
|
||||
FAKEUV
|
||||
chmod +x "${fake_home}/.local/bin/uv"
|
||||
|
||||
# Hide uv from PATH
|
||||
local saved_path="$PATH"
|
||||
PATH="/nonexistent"
|
||||
|
||||
@@ -320,19 +280,13 @@ FAKEUV
|
||||
|
||||
test_find_uv_path_checks_local_bin
|
||||
|
||||
###############################################################################
|
||||
# Test: find_openclaw() — not found scenario
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== find_openclaw() ==="
|
||||
|
||||
# Save original PATH and test with empty locations
|
||||
ORIGINAL_PATH="$PATH"
|
||||
ORIGINAL_HOME="$HOME"
|
||||
|
||||
test_find_openclaw_not_found() {
|
||||
# Use a fake HOME where nothing exists
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
@@ -354,7 +308,6 @@ test_find_openclaw_not_found() {
|
||||
|
||||
test_find_openclaw_not_found
|
||||
|
||||
# Test: find_openclaw() — found in HOME/.openclaw/
|
||||
test_find_openclaw_in_home() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
@@ -379,10 +332,6 @@ test_find_openclaw_in_home() {
|
||||
|
||||
test_find_openclaw_in_home
|
||||
|
||||
###############################################################################
|
||||
# Test: configure_memory_slot() — creates new config
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== configure_memory_slot() ==="
|
||||
|
||||
@@ -396,7 +345,6 @@ test_configure_new_config() {
|
||||
local config_file="${fake_home}/.openclaw/openclaw.json"
|
||||
assert_file_exists "$config_file" "Config file created at ~/.openclaw/openclaw.json"
|
||||
|
||||
# Verify JSON structure
|
||||
local memory_slot
|
||||
memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")"
|
||||
assert_eq "claude-mem" "$memory_slot" "Memory slot set to claude-mem in new config"
|
||||
@@ -415,13 +363,11 @@ test_configure_new_config() {
|
||||
|
||||
test_configure_new_config
|
||||
|
||||
# Test: configure_memory_slot() — updates existing config
|
||||
test_configure_existing_config() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
|
||||
# Create an existing config with other settings
|
||||
mkdir -p "${fake_home}/.openclaw"
|
||||
local config_file="${fake_home}/.openclaw/openclaw.json"
|
||||
node -e "
|
||||
@@ -439,22 +385,18 @@ test_configure_existing_config() {
|
||||
|
||||
configure_memory_slot >/dev/null 2>&1
|
||||
|
||||
# Verify memory slot was updated
|
||||
local memory_slot
|
||||
memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")"
|
||||
assert_eq "claude-mem" "$memory_slot" "Memory slot updated from memory-core to claude-mem"
|
||||
|
||||
# Verify existing settings preserved
|
||||
local gateway_mode
|
||||
gateway_mode="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.gateway.mode);")"
|
||||
assert_eq "local" "$gateway_mode" "Existing gateway.mode setting preserved"
|
||||
|
||||
# Verify other plugin still present
|
||||
local other_plugin
|
||||
other_plugin="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['some-other-plugin'].enabled);")"
|
||||
assert_eq "true" "$other_plugin" "Existing plugin entries preserved"
|
||||
|
||||
# Verify claude-mem entry was added
|
||||
local cm_enabled
|
||||
cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")"
|
||||
assert_eq "true" "$cm_enabled" "claude-mem entry added and enabled"
|
||||
@@ -465,7 +407,6 @@ test_configure_existing_config() {
|
||||
|
||||
test_configure_existing_config
|
||||
|
||||
# Test: configure_memory_slot() — preserves existing claude-mem config
|
||||
test_configure_preserves_existing_cm_config() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
@@ -493,7 +434,6 @@ test_configure_preserves_existing_cm_config() {
|
||||
|
||||
configure_memory_slot >/dev/null 2>&1
|
||||
|
||||
# Should enable it but preserve existing config
|
||||
local cm_enabled
|
||||
cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")"
|
||||
assert_eq "true" "$cm_enabled" "claude-mem entry enabled when previously disabled"
|
||||
@@ -512,10 +452,6 @@ test_configure_preserves_existing_cm_config() {
|
||||
|
||||
test_configure_preserves_existing_cm_config
|
||||
|
||||
###############################################################################
|
||||
# Test: version_gte() — already exists from phase 1
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== version_gte() ==="
|
||||
|
||||
@@ -537,14 +473,9 @@ else
|
||||
test_fail "version_gte: 1.0.0 < 1.1.14"
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Test: Script structure validation
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== Script structure ==="
|
||||
|
||||
# Verify all required functions exist
|
||||
for fn in find_openclaw check_openclaw install_plugin configure_memory_slot; do
|
||||
if declare -f "$fn" &>/dev/null; then
|
||||
test_pass "Function ${fn}() is defined"
|
||||
@@ -553,10 +484,8 @@ for fn in find_openclaw check_openclaw install_plugin configure_memory_slot; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify the CLAUDE_MEM_REPO constant
|
||||
assert_contains "$CLAUDE_MEM_REPO" "github.com/thedotmack/claude-mem" "CLAUDE_MEM_REPO points to correct repository"
|
||||
|
||||
# Verify AI provider functions exist
|
||||
for fn in setup_ai_provider write_settings mask_api_key; do
|
||||
if declare -f "$fn" &>/dev/null; then
|
||||
test_pass "Function ${fn}() is defined"
|
||||
@@ -565,10 +494,6 @@ for fn in setup_ai_provider write_settings mask_api_key; do
|
||||
fi
|
||||
done
|
||||
|
||||
###############################################################################
|
||||
# Test: mask_api_key()
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== mask_api_key() ==="
|
||||
|
||||
@@ -581,15 +506,10 @@ assert_eq "****" "$masked_short" "mask_api_key masks keys <= 4 chars entirely"
|
||||
masked_five=$(mask_api_key "12345")
|
||||
assert_eq "*2345" "$masked_five" "mask_api_key masks 5-char key correctly"
|
||||
|
||||
###############################################################################
|
||||
# Test: setup_ai_provider() — non-interactive mode defaults to Claude
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== setup_ai_provider() ==="
|
||||
|
||||
test_setup_ai_provider_non_interactive() {
|
||||
# NON_INTERACTIVE is readonly, so test in a child bash that sources with --non-interactive
|
||||
local ai_result
|
||||
ai_result="$(bash -c '
|
||||
set -euo pipefail
|
||||
@@ -609,10 +529,6 @@ test_setup_ai_provider_non_interactive() {
|
||||
|
||||
test_setup_ai_provider_non_interactive
|
||||
|
||||
###############################################################################
|
||||
# Test: write_settings() — creates new settings.json with defaults
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== write_settings() ==="
|
||||
|
||||
@@ -628,7 +544,6 @@ test_write_settings_new_file() {
|
||||
local settings_file="${fake_home}/.claude-mem/settings.json"
|
||||
assert_file_exists "$settings_file" "settings.json created at ~/.claude-mem/settings.json"
|
||||
|
||||
# Verify it's valid JSON with expected defaults
|
||||
local provider
|
||||
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
|
||||
assert_eq "claude" "$provider" "CLAUDE_MEM_PROVIDER set to claude"
|
||||
@@ -651,7 +566,6 @@ test_write_settings_new_file() {
|
||||
|
||||
test_write_settings_new_file
|
||||
|
||||
# Test: write_settings() — Gemini provider with API key
|
||||
test_write_settings_gemini() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
@@ -681,7 +595,6 @@ test_write_settings_gemini() {
|
||||
|
||||
test_write_settings_gemini
|
||||
|
||||
# Test: write_settings() — OpenRouter provider with API key
|
||||
test_write_settings_openrouter() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
@@ -711,13 +624,11 @@ test_write_settings_openrouter() {
|
||||
|
||||
test_write_settings_openrouter
|
||||
|
||||
# Test: write_settings() — preserves existing user customizations
|
||||
test_write_settings_preserves_existing() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
|
||||
# Create existing settings with custom values
|
||||
mkdir -p "${fake_home}/.claude-mem"
|
||||
local settings_file="${fake_home}/.claude-mem/settings.json"
|
||||
node -e "
|
||||
@@ -730,22 +641,18 @@ test_write_settings_preserves_existing() {
|
||||
require('fs').writeFileSync('${settings_file}', JSON.stringify(settings, null, 2));
|
||||
"
|
||||
|
||||
# Now run write_settings with a new provider
|
||||
AI_PROVIDER="claude"
|
||||
AI_PROVIDER_API_KEY=""
|
||||
write_settings >/dev/null 2>&1
|
||||
|
||||
# Provider should be updated to claude
|
||||
local provider
|
||||
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
|
||||
assert_eq "claude" "$provider" "Preserve: provider updated to new selection"
|
||||
|
||||
# Custom port should be preserved (not overwritten by defaults)
|
||||
local custom_port
|
||||
custom_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")"
|
||||
assert_eq "38888" "$custom_port" "Preserve: existing custom WORKER_PORT preserved"
|
||||
|
||||
# Custom log level should be preserved
|
||||
local log_level
|
||||
log_level="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_LOG_LEVEL);")"
|
||||
assert_eq "DEBUG" "$log_level" "Preserve: existing custom LOG_LEVEL preserved"
|
||||
@@ -756,7 +663,6 @@ test_write_settings_preserves_existing() {
|
||||
|
||||
test_write_settings_preserves_existing
|
||||
|
||||
# Test: write_settings() — flat schema has all expected keys
|
||||
test_write_settings_complete_schema() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
@@ -768,18 +674,15 @@ test_write_settings_complete_schema() {
|
||||
|
||||
local settings_file="${fake_home}/.claude-mem/settings.json"
|
||||
|
||||
# Verify key count matches SettingsDefaultsManager (34 keys)
|
||||
local key_count
|
||||
key_count="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(Object.keys(s).length);")"
|
||||
|
||||
# Settings should have all 34 keys from SettingsDefaultsManager
|
||||
if (( key_count >= 30 )); then
|
||||
test_pass "Settings file has ${key_count} keys (complete schema)"
|
||||
else
|
||||
test_fail "Settings file has ${key_count} keys, expected >= 30" "Schema may be incomplete"
|
||||
fi
|
||||
|
||||
# Verify it does NOT have nested { env: {...} } format
|
||||
local has_env_key
|
||||
has_env_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.env !== undefined);")"
|
||||
assert_eq "false" "$has_env_key" "Settings uses flat schema (no nested 'env' key)"
|
||||
@@ -790,10 +693,6 @@ test_write_settings_complete_schema() {
|
||||
|
||||
test_write_settings_complete_schema
|
||||
|
||||
###############################################################################
|
||||
# Test: find_claude_mem_install_dir() — not found scenario
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== find_claude_mem_install_dir() ==="
|
||||
|
||||
@@ -817,14 +716,12 @@ test_find_install_dir_not_found() {
|
||||
|
||||
test_find_install_dir_not_found
|
||||
|
||||
# Test: find_claude_mem_install_dir() — found in ~/.openclaw/extensions/claude-mem/
|
||||
test_find_install_dir_openclaw_extensions() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
CLAUDE_MEM_INSTALL_DIR=""
|
||||
|
||||
# Create the expected directory structure
|
||||
mkdir -p "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts"
|
||||
touch "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts/worker-service.cjs"
|
||||
|
||||
@@ -841,7 +738,6 @@ test_find_install_dir_openclaw_extensions() {
|
||||
|
||||
test_find_install_dir_openclaw_extensions
|
||||
|
||||
# Test: find_claude_mem_install_dir() — found in ~/.claude/plugins/marketplaces/thedotmack/
|
||||
test_find_install_dir_marketplace() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
@@ -864,10 +760,6 @@ test_find_install_dir_marketplace() {
|
||||
|
||||
test_find_install_dir_marketplace
|
||||
|
||||
###############################################################################
|
||||
# Test: start_worker() — fails gracefully when install dir not found
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== start_worker() ==="
|
||||
|
||||
@@ -892,17 +784,10 @@ test_start_worker_no_install_dir() {
|
||||
|
||||
test_start_worker_no_install_dir
|
||||
|
||||
###############################################################################
|
||||
# Test: verify_health() — fails when no server is running
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== verify_health() ==="
|
||||
|
||||
test_verify_health_no_server() {
|
||||
# verify_health should fail gracefully when nothing is running on 37777
|
||||
# We use a very short test — just 1 attempt to keep the test fast
|
||||
# Override the function to test with fewer attempts by running in a subshell
|
||||
local result
|
||||
result="$(bash -c '
|
||||
set -euo pipefail
|
||||
@@ -912,31 +797,22 @@ test_verify_health_no_server() {
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
# Call verify_health which will attempt 10 polls — capture exit code
|
||||
verify_health 2>/dev/null && echo "PASS" || echo "FAIL"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
# Note: This test may take ~10 seconds due to polling
|
||||
# If curl is not available, it will also fail
|
||||
if [[ "$result" == *"FAIL"* ]]; then
|
||||
test_pass "verify_health returns failure when no server is running"
|
||||
else
|
||||
# Could pass if something is actually running on 37777
|
||||
test_pass "verify_health returned success (worker may already be running on 37777)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Only run the health check test if curl is available
|
||||
if command -v curl &>/dev/null; then
|
||||
test_verify_health_no_server
|
||||
else
|
||||
test_pass "verify_health test skipped (curl not available)"
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Test: print_completion_summary() — runs without error
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== print_completion_summary() ==="
|
||||
|
||||
@@ -987,10 +863,6 @@ test_print_completion_summary_openrouter() {
|
||||
|
||||
test_print_completion_summary_openrouter
|
||||
|
||||
###############################################################################
|
||||
# Test: Script structure — new functions exist
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== New function existence ==="
|
||||
|
||||
@@ -1002,14 +874,9 @@ for fn in find_claude_mem_install_dir start_worker verify_health print_completio
|
||||
fi
|
||||
done
|
||||
|
||||
###############################################################################
|
||||
# Test: main() function calls new functions in correct order
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== main() function structure ==="
|
||||
|
||||
# Verify main calls the new functions by checking the install.sh source
|
||||
test_main_calls_start_worker() {
|
||||
if grep -q 'start_worker' "$INSTALL_SCRIPT"; then
|
||||
test_pass "main() calls start_worker"
|
||||
@@ -1070,10 +937,6 @@ test_main_calls_write_observation_feed_config() {
|
||||
|
||||
test_main_calls_write_observation_feed_config
|
||||
|
||||
###############################################################################
|
||||
# Test: setup_observation_feed() — function exists and non-interactive skips
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== setup_observation_feed() ==="
|
||||
|
||||
@@ -1086,7 +949,6 @@ for fn in setup_observation_feed write_observation_feed_config; do
|
||||
done
|
||||
|
||||
test_setup_observation_feed_non_interactive() {
|
||||
# Non-interactive mode should skip feed setup without error
|
||||
local feed_result
|
||||
feed_result="$(bash -c '
|
||||
set -euo pipefail
|
||||
@@ -1108,10 +970,6 @@ test_setup_observation_feed_non_interactive() {
|
||||
|
||||
test_setup_observation_feed_non_interactive
|
||||
|
||||
###############################################################################
|
||||
# Test: write_observation_feed_config() — writes correct JSON structure
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== write_observation_feed_config() ==="
|
||||
|
||||
@@ -1120,7 +978,6 @@ test_write_observation_feed_config_writes_json() {
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
|
||||
# Create an existing openclaw.json with claude-mem entry
|
||||
mkdir -p "${fake_home}/.openclaw"
|
||||
local config_file="${fake_home}/.openclaw/openclaw.json"
|
||||
node -e "
|
||||
@@ -1144,7 +1001,6 @@ test_write_observation_feed_config_writes_json() {
|
||||
|
||||
write_observation_feed_config >/dev/null 2>&1
|
||||
|
||||
# Verify observationFeed was written
|
||||
local feed_enabled
|
||||
feed_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.enabled);")"
|
||||
assert_eq "true" "$feed_enabled" "observationFeed.enabled is true"
|
||||
@@ -1157,7 +1013,6 @@ test_write_observation_feed_config_writes_json() {
|
||||
feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")"
|
||||
assert_eq "123456789" "$feed_to" "observationFeed.to is 123456789"
|
||||
|
||||
# Verify existing config preserved
|
||||
local worker_port
|
||||
worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")"
|
||||
assert_eq "37777" "$worker_port" "Existing workerPort preserved after feed config write"
|
||||
@@ -1176,7 +1031,6 @@ test_write_observation_feed_config_skips_when_not_configured() {
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
|
||||
# Create minimal config
|
||||
mkdir -p "${fake_home}/.openclaw"
|
||||
local config_file="${fake_home}/.openclaw/openclaw.json"
|
||||
node -e "
|
||||
@@ -1187,7 +1041,6 @@ test_write_observation_feed_config_skips_when_not_configured() {
|
||||
|
||||
write_observation_feed_config >/dev/null 2>&1
|
||||
|
||||
# Config should be unchanged — no observationFeed key
|
||||
local has_feed
|
||||
has_feed="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries !== undefined);")"
|
||||
assert_eq "false" "$has_feed" "Config unchanged when FEED_CONFIGURED is false"
|
||||
@@ -1239,14 +1092,9 @@ test_write_observation_feed_config_discord() {
|
||||
|
||||
test_write_observation_feed_config_discord
|
||||
|
||||
###############################################################################
|
||||
# Test: write_observation_feed_config() — jq/python3/node fallback paths
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== write_observation_feed_config() — fallback paths ==="
|
||||
|
||||
# Helper: verify feed config JSON was written correctly
|
||||
verify_feed_config_json() {
|
||||
local config_file="$1" expected_channel="$2" expected_target="$3" label="$4"
|
||||
|
||||
@@ -1262,13 +1110,11 @@ verify_feed_config_json() {
|
||||
feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")"
|
||||
assert_eq "$expected_target" "$feed_to" "${label}: observationFeed.to correct"
|
||||
|
||||
# Verify existing config preserved
|
||||
local worker_port
|
||||
worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")"
|
||||
assert_eq "37777" "$worker_port" "${label}: existing workerPort preserved"
|
||||
}
|
||||
|
||||
# Create a seed config file for fallback tests
|
||||
create_seed_config() {
|
||||
local config_file="$1"
|
||||
mkdir -p "$(dirname "$config_file")"
|
||||
@@ -1288,7 +1134,6 @@ create_seed_config() {
|
||||
"
|
||||
}
|
||||
|
||||
# Test: jq path (if jq is available)
|
||||
test_write_feed_config_jq_path() {
|
||||
if ! command -v jq &>/dev/null; then
|
||||
test_pass "jq path: skipped (jq not installed)"
|
||||
@@ -1305,7 +1150,6 @@ test_write_feed_config_jq_path() {
|
||||
FEED_TARGET_ID="C01ABC2DEFG"
|
||||
FEED_CONFIGURED="true"
|
||||
|
||||
# jq is first in the chain, so just call directly
|
||||
write_observation_feed_config >/dev/null 2>&1
|
||||
|
||||
verify_feed_config_json "$config_file" "slack" "C01ABC2DEFG" "jq path"
|
||||
@@ -1319,7 +1163,6 @@ test_write_feed_config_jq_path() {
|
||||
|
||||
test_write_feed_config_jq_path
|
||||
|
||||
# Test: python3 fallback path (hide jq)
|
||||
test_write_feed_config_python3_path() {
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
test_pass "python3 path: skipped (python3 not installed)"
|
||||
@@ -1329,14 +1172,12 @@ test_write_feed_config_python3_path() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
|
||||
# Run in a subshell that hides jq from PATH
|
||||
local result
|
||||
result="$(bash -c '
|
||||
set -euo pipefail
|
||||
TERM=dumb
|
||||
export HOME="'"$fake_home"'"
|
||||
|
||||
# Create seed config using node (node is always available)
|
||||
mkdir -p "'"${fake_home}"'/.openclaw"
|
||||
node -e "
|
||||
const config = {
|
||||
@@ -1353,14 +1194,12 @@ test_write_feed_config_python3_path() {
|
||||
require(\"fs\").writeFileSync(\"'"${fake_home}"'/.openclaw/openclaw.json\", JSON.stringify(config, null, 2));
|
||||
"
|
||||
|
||||
# Source install.sh functions
|
||||
tmp=$(mktemp)
|
||||
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
|
||||
# Hide jq by creating a PATH without it
|
||||
SAFE_PATH=""
|
||||
IFS=":" read -ra path_parts <<< "$PATH"
|
||||
for p in "${path_parts[@]}"; do
|
||||
@@ -1378,7 +1217,6 @@ test_write_feed_config_python3_path() {
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
if [[ "$result" == *"DONE"* ]]; then
|
||||
# Verify the JSON using node
|
||||
local config_file="${fake_home}/.openclaw/openclaw.json"
|
||||
verify_feed_config_json "$config_file" "signal" "+15551234567" "python3 path"
|
||||
else
|
||||
@@ -1390,7 +1228,6 @@ test_write_feed_config_python3_path() {
|
||||
|
||||
test_write_feed_config_python3_path
|
||||
|
||||
# Test: node fallback path (hide both jq and python3)
|
||||
test_write_feed_config_node_path() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
@@ -1401,7 +1238,6 @@ test_write_feed_config_node_path() {
|
||||
TERM=dumb
|
||||
export HOME="'"$fake_home"'"
|
||||
|
||||
# Create seed config
|
||||
mkdir -p "'"${fake_home}"'/.openclaw"
|
||||
node -e "
|
||||
const config = {
|
||||
@@ -1418,22 +1254,12 @@ test_write_feed_config_node_path() {
|
||||
require(\"fs\").writeFileSync(\"'"${fake_home}"'/.openclaw/openclaw.json\", JSON.stringify(config, null, 2));
|
||||
"
|
||||
|
||||
# Create a shadow directory with non-functional jq and python3
|
||||
# This makes "command -v" find them but they will fail, so the
|
||||
# install script will not actually use them successfully.
|
||||
# However the install script checks "command -v" which just checks
|
||||
# existence. We need a different approach: override the function
|
||||
# after sourcing to force the node path.
|
||||
|
||||
# Source install.sh functions
|
||||
tmp=$(mktemp)
|
||||
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
|
||||
# Override write_observation_feed_config to only use the node path
|
||||
# by extracting just the node branch logic
|
||||
INSTALLER_FEED_CHANNEL="whatsapp" \
|
||||
INSTALLER_FEED_TARGET_ID="5511999887766@s.whatsapp.net" \
|
||||
INSTALLER_CONFIG_FILE="'"${fake_home}"'/.openclaw/openclaw.json" \
|
||||
@@ -1477,7 +1303,6 @@ test_write_feed_config_node_path() {
|
||||
|
||||
test_write_feed_config_node_path
|
||||
|
||||
# Test: write_observation_feed_config uses jq/python3/node fallback chain
|
||||
test_feed_config_fallback_chain_in_source() {
|
||||
if grep -q 'command -v jq' "$INSTALL_SCRIPT"; then
|
||||
test_pass "write_observation_feed_config checks for jq first"
|
||||
@@ -1500,10 +1325,6 @@ test_feed_config_fallback_chain_in_source() {
|
||||
|
||||
test_feed_config_fallback_chain_in_source
|
||||
|
||||
###############################################################################
|
||||
# Test: print_completion_summary() — shows observation feed status
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== print_completion_summary() — observation feed ==="
|
||||
|
||||
@@ -1547,10 +1368,6 @@ test_completion_summary_without_feed() {
|
||||
|
||||
test_completion_summary_without_feed
|
||||
|
||||
###############################################################################
|
||||
# Test: Channel type instructions exist in install.sh
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== Channel instructions ==="
|
||||
|
||||
@@ -1562,15 +1379,10 @@ for channel in telegram discord slack signal whatsapp line; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify specific instruction content
|
||||
assert_contains "$(grep -A2 'userinfobot' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "userinfobot" "Telegram instructions include @userinfobot"
|
||||
assert_contains "$(grep -A2 'Developer Mode' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "Developer Mode" "Discord instructions include Developer Mode"
|
||||
assert_contains "$(grep -A2 'C01ABC2DEFG' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "C01ABC2DEFG" "Slack instructions include sample channel ID"
|
||||
|
||||
###############################################################################
|
||||
# Test: TTY detection — setup_tty() and read_tty() exist
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== TTY detection ==="
|
||||
|
||||
@@ -1582,24 +1394,18 @@ for fn in setup_tty read_tty; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify TTY_FD is initialized (defaults to 0)
|
||||
if declare -p TTY_FD &>/dev/null; then
|
||||
test_pass "TTY_FD variable is defined"
|
||||
else
|
||||
test_fail "TTY_FD variable should be defined"
|
||||
fi
|
||||
|
||||
# Verify setup_tty is called from main()
|
||||
if grep -q 'setup_tty' "$INSTALL_SCRIPT"; then
|
||||
test_pass "main() calls setup_tty"
|
||||
else
|
||||
test_fail "main() should call setup_tty"
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Test: Argument parsing — --provider flag
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== Argument parsing — --provider flag ==="
|
||||
|
||||
@@ -1690,10 +1496,6 @@ test_provider_flag_invalid() {
|
||||
|
||||
test_provider_flag_invalid
|
||||
|
||||
###############################################################################
|
||||
# Test: Argument parsing — --non-interactive flag (new format)
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== Argument parsing — --non-interactive ==="
|
||||
|
||||
@@ -1740,16 +1542,10 @@ test_non_interactive_with_provider() {
|
||||
|
||||
test_non_interactive_with_provider
|
||||
|
||||
###############################################################################
|
||||
# Test: --non-interactive mode completes without hanging
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== --non-interactive full flow ==="
|
||||
|
||||
test_non_interactive_completes() {
|
||||
# Run the full setup_ai_provider + setup_observation_feed in non-interactive mode
|
||||
# This should complete without any prompts or hangs
|
||||
local result
|
||||
result="$(bash -c '
|
||||
set -euo pipefail
|
||||
@@ -1772,10 +1568,6 @@ test_non_interactive_completes() {
|
||||
|
||||
test_non_interactive_completes
|
||||
|
||||
###############################################################################
|
||||
# Test: Script structure — curl | bash usage comment
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== curl | bash usage comment ==="
|
||||
|
||||
@@ -1791,10 +1583,6 @@ else
|
||||
test_fail "install.sh should document --provider flag in usage comment"
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Test: write_settings with --provider flag end-to-end
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== write_settings with --provider flag ==="
|
||||
|
||||
@@ -1836,10 +1624,6 @@ test_write_settings_via_provider_flag() {
|
||||
|
||||
test_write_settings_via_provider_flag
|
||||
|
||||
###############################################################################
|
||||
# Test: --upgrade flag parsing
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== --upgrade flag parsing ==="
|
||||
|
||||
@@ -1903,10 +1687,6 @@ test_upgrade_not_set_by_default() {
|
||||
|
||||
test_upgrade_not_set_by_default
|
||||
|
||||
###############################################################################
|
||||
# Test: is_claude_mem_installed() — upgrade detection
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== is_claude_mem_installed() ==="
|
||||
|
||||
@@ -1916,7 +1696,6 @@ test_is_claude_mem_installed_found() {
|
||||
HOME="$fake_home"
|
||||
CLAUDE_MEM_INSTALL_DIR=""
|
||||
|
||||
# Create the expected directory structure
|
||||
mkdir -p "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts"
|
||||
touch "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts/worker-service.cjs"
|
||||
|
||||
@@ -1950,15 +1729,10 @@ test_is_claude_mem_installed_not_found() {
|
||||
|
||||
test_is_claude_mem_installed_not_found
|
||||
|
||||
###############################################################################
|
||||
# Test: check_git() — git availability check
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== check_git() ==="
|
||||
|
||||
test_check_git_available() {
|
||||
# git should be available in test environment
|
||||
if command -v git &>/dev/null; then
|
||||
local output
|
||||
output="$(check_git 2>&1)" || true
|
||||
@@ -1971,7 +1745,6 @@ test_check_git_available() {
|
||||
test_check_git_available
|
||||
|
||||
test_check_git_not_available() {
|
||||
# Test that check_git fails gracefully when git is not in PATH
|
||||
local exit_code=0
|
||||
PLATFORM="macos"
|
||||
bash -c '
|
||||
@@ -2035,10 +1808,6 @@ test_check_git_linux_message() {
|
||||
|
||||
test_check_git_linux_message
|
||||
|
||||
###############################################################################
|
||||
# Test: check_port_37777() — port conflict detection
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== check_port_37777() ==="
|
||||
|
||||
@@ -2052,10 +1821,6 @@ test_check_port_function_exists() {
|
||||
|
||||
test_check_port_function_exists
|
||||
|
||||
###############################################################################
|
||||
# Test: cleanup_on_exit() — global cleanup trap
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== cleanup_on_exit() ==="
|
||||
|
||||
@@ -2079,7 +1844,6 @@ test_register_cleanup_dir() {
|
||||
local test_dir
|
||||
test_dir="$(mktemp -d)"
|
||||
|
||||
# Save existing cleanup dirs
|
||||
local saved_dirs=("${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}")
|
||||
CLEANUP_DIRS=()
|
||||
|
||||
@@ -2091,17 +1855,12 @@ test_register_cleanup_dir() {
|
||||
test_fail "register_cleanup_dir should add directory to CLEANUP_DIRS"
|
||||
fi
|
||||
|
||||
# Restore
|
||||
CLEANUP_DIRS=("${saved_dirs[@]+"${saved_dirs[@]}"}")
|
||||
rm -rf "$test_dir"
|
||||
}
|
||||
|
||||
test_register_cleanup_dir
|
||||
|
||||
###############################################################################
|
||||
# Test: ensure_jq_or_fallback() — JSON utility function
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== ensure_jq_or_fallback() ==="
|
||||
|
||||
@@ -2138,10 +1897,6 @@ test_ensure_jq_with_jq_available() {
|
||||
|
||||
test_ensure_jq_with_jq_available
|
||||
|
||||
###############################################################################
|
||||
# Test: main() references new functions
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== main() references new functions ==="
|
||||
|
||||
@@ -2205,10 +1960,6 @@ test_usage_comment_includes_upgrade() {
|
||||
|
||||
test_usage_comment_includes_upgrade
|
||||
|
||||
###############################################################################
|
||||
# Test: Distribution readiness — URL, usage comment, SKILL.md reference
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== Distribution readiness ==="
|
||||
|
||||
@@ -2323,10 +2074,6 @@ test_skill_md_documents_options() {
|
||||
|
||||
test_skill_md_documents_options
|
||||
|
||||
###############################################################################
|
||||
# Summary
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Results: ${TESTS_PASSED}/${TESTS_RUN} passed, ${TESTS_FAILED} failed"
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* Smoke test for OpenClaw claude-mem plugin registration.
|
||||
* Validates the plugin structure works independently of the full OpenClaw runtime.
|
||||
*
|
||||
* Run: node test-sse-consumer.js
|
||||
*/
|
||||
|
||||
import claudeMemPlugin from "./dist/index.js";
|
||||
|
||||
@@ -49,10 +43,8 @@ const mockApi = {
|
||||
},
|
||||
};
|
||||
|
||||
// Call the default export with mock API
|
||||
claudeMemPlugin(mockApi);
|
||||
|
||||
// Verify registration
|
||||
let failures = 0;
|
||||
|
||||
if (!registeredService) {
|
||||
|
||||
+14
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "12.4.7",
|
||||
"version": "12.5.1",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -61,10 +61,11 @@
|
||||
"scripts": {
|
||||
"dev": "npm run build-and-sync",
|
||||
"build": "node scripts/sync-plugin-manifests.js && node scripts/build-hooks.js",
|
||||
"build-and-sync": "npm run build && npm run sync-marketplace && sleep 1 && cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:restart",
|
||||
"build-and-sync": "npm run build && npm run sync-marketplace && sleep 1 && (cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:restart) && npm run queue:clear",
|
||||
"sync-marketplace": "node scripts/sync-marketplace.cjs",
|
||||
"sync-marketplace:force": "node scripts/sync-marketplace.cjs --force",
|
||||
"build:binaries": "node scripts/build-worker-binary.js",
|
||||
"build:cli-binary": "bun build --compile --minify ./src/services/worker-service.ts --outfile plugin/scripts/claude-mem",
|
||||
"worker:logs": "tail -n 50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log",
|
||||
"worker:tail": "tail -f 50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log",
|
||||
"changelog:generate": "node scripts/generate-changelog.js",
|
||||
@@ -78,6 +79,9 @@
|
||||
"queue:clear": "bun scripts/clear-failed-queue.ts --all --force",
|
||||
"claude-md:regenerate": "bun scripts/regenerate-claude-md.ts",
|
||||
"claude-md:dry-run": "bun scripts/regenerate-claude-md.ts --dry-run",
|
||||
"strip-comments": "bun scripts/strip-comments.ts",
|
||||
"strip-comments:check": "bun scripts/strip-comments.ts --check",
|
||||
"strip-comments:dry-run": "bun scripts/strip-comments.ts --dry-run",
|
||||
"translate-readme": "bun scripts/translate-readme/cli.ts -v -o docs/i18n README.md",
|
||||
"translate:tier1": "npm run translate-readme -- zh zh-tw ja pt-br ko es de fr",
|
||||
"translate:tier2": "npm run translate-readme -- he ar ru pl cs nl tr uk",
|
||||
@@ -142,7 +146,12 @@
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"jimp": "^1.6.1",
|
||||
"np": "^11.2.0",
|
||||
"parse5": "^8.0.1",
|
||||
"postcss": "^8.5.13",
|
||||
"remark-mdx": "^3.1.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"tree-sitter-bash": "^0.25.1",
|
||||
"tree-sitter-c": "^0.24.1",
|
||||
"tree-sitter-cli": "^0.26.8",
|
||||
@@ -164,7 +173,9 @@
|
||||
"tree-sitter-typescript": "^0.23.2",
|
||||
"ts-prune": "^0.10.3",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.3"
|
||||
"typescript": "^6.0.3",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"tree-kill": "^1.2.2"
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
# Installer Streamline — Eliminate 30s Silent Dead Air
|
||||
|
||||
**Goal:** Move all heavy install work (Bun/uv install, `bun install` in plugin cache) into the `npx claude-mem install` flow with a visible spinner. Make hooks runtime-only — never installers.
|
||||
|
||||
**Net effect:**
|
||||
- `smart-install.js` runs in normal Claude Code lifecycle: 3 → 0 (or 1 via `npx claude-mem repair` after `claude plugin update`)
|
||||
- 30s silent dead air → visible spinner during `npx`
|
||||
- `npx claude-mem repair` becomes the canonical recovery entry point
|
||||
- ~420 lines of code deleted (smart-install.js × 2 + tests + docs)
|
||||
|
||||
**Out of scope:** `bun-runner.js` deletion (independent rework with Windows/stdin verification needs — ship later).
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Documentation Discovery (already complete)
|
||||
|
||||
These facts came from a discovery agent + direct file reads. Each implementation phase below cites them by line number; do not re-derive.
|
||||
|
||||
### Allowed APIs / patterns to copy
|
||||
|
||||
| Item | Location | What to copy |
|
||||
|---|---|---|
|
||||
| NPX command dispatcher | `src/npx-cli/index.ts:39–141` | Manual `switch (command)` on `process.argv.slice(2)`. Each case dynamic-imports its handler. |
|
||||
| `install` case (template for `repair`) | `src/npx-cli/index.ts:46–52` | `const { runInstallCommand } = await import('./commands/install.js'); await runInstallCommand({ ide: ideValue });` |
|
||||
| Plugin cache dir helper | `src/npx-cli/utils/paths.ts:32–34` | `pluginCacheDirectory(version)` → `~/.claude/plugins/cache/thedotmack/claude-mem/{version}/` |
|
||||
| `.install-version` marker readers | `src/services/context/ContextBuilder.ts:36,45` and `src/services/worker/BranchManager.ts:173,228` | These read/delete the marker. Marker schema (`{ version, bun, uv, installedAt }`) MUST be preserved. |
|
||||
| `clack` task pattern | `src/npx-cli/commands/install.ts:604–664` | `runTasks([{ title, task: async (message) => { … return 'Done OK' } }])` |
|
||||
|
||||
### Anti-patterns / API methods that DO NOT exist (avoid inventing)
|
||||
|
||||
- There is no existing `version-check.js` helper in `plugin/scripts/`. Phase 4 must create it.
|
||||
- `package.json#files` already globs `plugin/scripts/*.js` (line 50), so deleting `plugin/scripts/smart-install.js` requires no `package.json` change.
|
||||
- `scripts/smart-install.js` and `plugin/scripts/smart-install.js` are **both source files** kept in sync manually — there is no build step that copies one to the other. Both must be deleted in Phase 5.
|
||||
- `runSmartInstall()` (install.ts:325–345) shells `node smart-install.js`. After Phase 1 you can call the new module directly — do NOT shell out.
|
||||
- The `claude plugin install` exec at install.ts:113 has **only one caller** in the entire repo. Safe to remove.
|
||||
|
||||
### File inventory used by this plan
|
||||
|
||||
| File | Lines | Disposition |
|
||||
|---|---|---|
|
||||
| `src/npx-cli/commands/install.ts` | 761 | Edited heavily (Phase 2) |
|
||||
| `src/npx-cli/index.ts` | 147 | One case added (Phase 3) |
|
||||
| `plugin/hooks/hooks.json` | 93 | Setup hook command rewritten, SessionStart smart-install entry deleted (Phase 4) |
|
||||
| `scripts/smart-install.js` | 264 | DELETED (Phase 5) |
|
||||
| `plugin/scripts/smart-install.js` | ≈264 | DELETED (Phase 5) |
|
||||
| `tests/smart-install.test.ts` | 310 | DELETED (Phase 5) |
|
||||
| `tests/plugin-scripts-line-endings.test.ts` | 33 | One array entry removed (Phase 5) |
|
||||
| `plugin/scripts/version-check.js` | NEW | CREATED (Phase 4) |
|
||||
| `src/npx-cli/install/setup-runtime.ts` | NEW | CREATED (Phase 1) |
|
||||
| Docs (`docs/public/*.mdx`, `docs/architecture-overview.md`) | misc | Light edit (Phase 6) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Create `src/npx-cli/install/setup-runtime.ts`
|
||||
|
||||
**What to implement:** Port the smart-install.js logic to a TypeScript module that takes a target directory parameter (so it can install into the plugin cache dir, not just the marketplace dir). Three exported functions plus internal helpers.
|
||||
|
||||
**File to create:** `src/npx-cli/install/setup-runtime.ts`
|
||||
|
||||
**API surface (these names are used by Phase 2 and Phase 3 — do not rename):**
|
||||
|
||||
```ts
|
||||
export async function ensureBun(): Promise<{ bunPath: string; version: string }>;
|
||||
export async function ensureUv(): Promise<{ uvPath: string; version: string }>;
|
||||
export async function installPluginDependencies(targetDir: string, bunPath: string): Promise<void>;
|
||||
export function readInstallMarker(targetDir: string): { version: string; bun?: string; uv?: string; installedAt?: string } | null;
|
||||
export function writeInstallMarker(targetDir: string, version: string, bunVersion: string, uvVersion: string): void;
|
||||
export function isInstallCurrent(targetDir: string, expectedVersion: string): boolean;
|
||||
```
|
||||
|
||||
**Reference implementation to port from:** `scripts/smart-install.js:1–264`. Map old → new:
|
||||
|
||||
| smart-install.js | setup-runtime.ts |
|
||||
|---|---|
|
||||
| `getBunPath()` / `isBunInstalled()` / `installBun()` (lines 42–152) | private helpers consumed by `ensureBun()` |
|
||||
| `getUvPath()` / `isUvInstalled()` / `installUv()` (lines 77–194) | private helpers consumed by `ensureUv()` |
|
||||
| `needsInstall()` (lines 196–205) | `isInstallCurrent()` + `readInstallMarker()` |
|
||||
| `installDeps()` (lines 207–226) | `installPluginDependencies(targetDir, bunPath)` — accepts target dir as parameter |
|
||||
| `verifyCriticalModules()` (lines 228–246) | private helper called inside `installPluginDependencies` |
|
||||
| `MARKER` constant (line 32) | derive inside each function: `join(targetDir, '.install-version')` |
|
||||
| Top-level `try { … }` (lines 248–264) | DELETE — caller orchestrates |
|
||||
|
||||
**Key behavioral differences from smart-install.js:**
|
||||
- All functions take `targetDir` as a parameter (was a top-level `ROOT` constant).
|
||||
- `ensureBun()` / `ensureUv()` return their version strings rather than logging — caller decides what to display.
|
||||
- All functions throw on failure with descriptive `Error.message`. The `clack` `runTasks` wrapper in Phase 2 catches and renders.
|
||||
- `console.error` calls in install/uninstall paths become structured: throw a single `Error` with the manual install instructions in the message body.
|
||||
- Marker schema is preserved exactly (`{ version, bun, uv, installedAt }`) so existing readers in `ContextBuilder.ts:36` and `BranchManager.ts:173,228` continue to work.
|
||||
|
||||
**Verification checklist:**
|
||||
- [ ] `bun build src/npx-cli/install/setup-runtime.ts --target=node` succeeds (or whatever the project's TS check command is — confirm via `package.json#scripts`)
|
||||
- [ ] Marker file format is byte-identical to smart-install.js output (write a marker, diff against a marker written by the old code)
|
||||
- [ ] `grep -rn "ROOT" src/npx-cli/install/setup-runtime.ts` returns nothing — no top-level constants
|
||||
|
||||
**Anti-pattern guards:**
|
||||
- ❌ Do not invent a `bunInstall.ts` or `uvInstall.ts` split — keep all three in one file. They share helper code (paths, version probing).
|
||||
- ❌ Do not import from `plugin/scripts/smart-install.js` — it gets deleted in Phase 5.
|
||||
- ❌ Do not change the marker schema. Existing readers depend on `{ version }` field.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Rework `src/npx-cli/commands/install.ts`
|
||||
|
||||
**What to implement:** Drop `needsManualInstall` gating (always run copy/register/enable for every IDE), add a new unconditional "Setting up runtime" task before `setupIDEs`, neuter the claude-code `execSync` shell-out, delete `runSmartInstall()`, and add a `runRepairCommand()` export.
|
||||
|
||||
**File to edit:** `src/npx-cli/commands/install.ts`
|
||||
|
||||
### Edit 2A — Add import for setup-runtime (top of file, after other imports)
|
||||
|
||||
Insert after line 10 (after the `ensureWorkerStarted` import):
|
||||
|
||||
```ts
|
||||
import {
|
||||
ensureBun,
|
||||
ensureUv,
|
||||
installPluginDependencies,
|
||||
writeInstallMarker,
|
||||
isInstallCurrent,
|
||||
} from '../install/setup-runtime.js';
|
||||
```
|
||||
|
||||
### Edit 2B — Delete `runSmartInstall()` function
|
||||
|
||||
**Delete lines 325–345** (the entire `function runSmartInstall(): boolean { … }` block).
|
||||
|
||||
### Edit 2C — Drop `needsManualInstall` gating, ungate the runTasks block
|
||||
|
||||
**Line 589** currently reads:
|
||||
```ts
|
||||
const needsManualInstall = selectedIDEs.some((id) => id !== 'claude-code');
|
||||
```
|
||||
**Delete line 589.** Update line 593's `if (needsManualInstall) {` to just `{` (or unwrap the block — preferred). The `runTasks` block at lines 604–664 now runs unconditionally.
|
||||
|
||||
**Within that runTasks block:** delete the "Setting up Bun and uv" task entry (lines 656–663). Replace its slot with the new "Setting up runtime" task (Edit 2D).
|
||||
|
||||
### Edit 2D — Insert "Setting up runtime" task
|
||||
|
||||
Replace the deleted "Setting up Bun and uv" task (lines 656–663) with:
|
||||
|
||||
```ts
|
||||
{
|
||||
title: 'Setting up runtime (first install can take ~30s)',
|
||||
task: async (message) => {
|
||||
message('Checking Bun…');
|
||||
const { version: bunVersion } = await ensureBun();
|
||||
message('Checking uv…');
|
||||
const { version: uvVersion } = await ensureUv();
|
||||
const cacheDir = pluginCacheDirectory(version);
|
||||
if (!isInstallCurrent(cacheDir, version)) {
|
||||
message('Installing plugin dependencies…');
|
||||
const { bunPath } = await ensureBun();
|
||||
await installPluginDependencies(cacheDir, bunPath);
|
||||
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
|
||||
}
|
||||
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Place this AFTER the "Installing dependencies" (npm install) task — same ordering position the deleted task occupied.
|
||||
|
||||
### Edit 2E — Neuter the claude-code shell-out in `setupIDEs`
|
||||
|
||||
**Lines 110–123 currently:**
|
||||
```ts
|
||||
case 'claude-code': {
|
||||
try {
|
||||
execSync(
|
||||
'claude plugin marketplace add thedotmack/claude-mem && claude plugin install claude-mem',
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
log.success('Claude Code: plugin installed via CLI.');
|
||||
} catch (error: unknown) {
|
||||
console.error('[install] Claude Code plugin install error:', …);
|
||||
log.error('Claude Code: plugin install failed. Is `claude` CLI on your PATH?');
|
||||
failedIDEs.push(ideId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
**Replace with:**
|
||||
```ts
|
||||
case 'claude-code': {
|
||||
log.success('Claude Code: plugin registered (cache + settings written by npx).');
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
The cache dir, marketplace registration, plugin registration, and `enabledPlugins` flag have all been written by the (now ungated) runTasks block before `setupIDEs` is called. `claude plugin install` was duplicating that work and triggering the silent Setup hook — both reasons to drop it.
|
||||
|
||||
### Edit 2F — Add `runRepairCommand()` export
|
||||
|
||||
After `runInstallCommand()` (after line 761), append:
|
||||
|
||||
```ts
|
||||
export async function runRepairCommand(): Promise<void> {
|
||||
const version = readPluginVersion();
|
||||
const cacheDir = pluginCacheDirectory(version);
|
||||
|
||||
if (isInteractive) {
|
||||
p.intro(pc.bgCyan(pc.black(' claude-mem repair ')));
|
||||
} else {
|
||||
console.log('claude-mem repair');
|
||||
}
|
||||
log.info(`Version: ${pc.cyan(version)}`);
|
||||
|
||||
await runTasks([
|
||||
{
|
||||
title: 'Setting up runtime',
|
||||
task: async (message) => {
|
||||
message('Checking Bun…');
|
||||
const { version: bunVersion } = await ensureBun();
|
||||
message('Checking uv…');
|
||||
const { version: uvVersion } = await ensureUv();
|
||||
message('Reinstalling plugin dependencies…');
|
||||
const { bunPath } = await ensureBun();
|
||||
await installPluginDependencies(cacheDir, bunPath);
|
||||
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
|
||||
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (isInteractive) {
|
||||
p.outro(pc.green('claude-mem repair complete.'));
|
||||
} else {
|
||||
console.log('claude-mem repair complete.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`runRepairCommand` always runs the install (no `isInstallCurrent` short-circuit) — the user invoked `repair` because something is wrong, so don't gate on the marker.
|
||||
|
||||
**Verification checklist:**
|
||||
- [ ] `grep -n "needsManualInstall" src/npx-cli/commands/install.ts` returns nothing
|
||||
- [ ] `grep -n "runSmartInstall" src/npx-cli/commands/install.ts` returns nothing
|
||||
- [ ] `grep -n "claude plugin install" src/npx-cli/commands/install.ts` returns nothing
|
||||
- [ ] `grep -n "claude plugin marketplace add" src/npx-cli/commands/install.ts` returns nothing
|
||||
- [ ] `runRepairCommand` is exported and TypeScript compiles
|
||||
- [ ] `runInstallCommand` still exports the same `InstallOptions` shape (Phase 3 needs it untouched)
|
||||
|
||||
**Anti-pattern guards:**
|
||||
- ❌ Do not delete `runNpmInstallInMarketplace()` — it's still needed for the marketplace dir copy step (other IDEs use that dir).
|
||||
- ❌ Do not delete `copyPluginToMarketplace()` — non-claude-code IDEs read from `marketplaceDirectory()`.
|
||||
- ❌ Do not delete the `if (alreadyInstalled)` overwrite-confirm block (lines 538–562) — user-facing UX preserved.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Wire `npx claude-mem repair`
|
||||
|
||||
**What to implement:** Add a `repair` case to the npx-cli command dispatcher.
|
||||
|
||||
**File to edit:** `src/npx-cli/index.ts`
|
||||
|
||||
### Edit 3A — Add `repair` case
|
||||
|
||||
In the `switch` block (lines 39–141), copy the `install` case pattern from lines 46–52 and adapt:
|
||||
|
||||
```ts
|
||||
case 'repair': {
|
||||
const { runRepairCommand } = await import('./commands/install.js');
|
||||
await runRepairCommand();
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
Place it adjacent to the `install` case for discoverability.
|
||||
|
||||
### Edit 3B — Help text update (if applicable)
|
||||
|
||||
If `src/npx-cli/index.ts` has a help/usage block (look for `case 'help':` or default case), add `repair` to the list of commands with description: `Repair claude-mem runtime (re-runs Bun/uv setup and bun install in plugin cache).`
|
||||
|
||||
**Verification checklist:**
|
||||
- [ ] `npx claude-mem repair --help` (after build) shows the command
|
||||
- [ ] `npx claude-mem repair` runs `runRepairCommand` end to end on a corrupted cache (delete `.install-version` then run; should reinstall)
|
||||
- [ ] Help/usage output (if it exists) lists `repair`
|
||||
|
||||
**Anti-pattern guards:**
|
||||
- ❌ Do not add CLI flag parsing for `repair` (no flags needed).
|
||||
- ❌ Do not duplicate the `runRepairCommand` body in `index.ts` — dynamic import only.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Strip smart-install from hooks; add `version-check.js`
|
||||
|
||||
**What to implement:** Replace the Setup hook's `node smart-install.js` call with a fast version-marker check. Delete the SessionStart smart-install hook entry entirely.
|
||||
|
||||
### Edit 4A — Create `plugin/scripts/version-check.js`
|
||||
|
||||
**File to create:** `plugin/scripts/version-check.js` (new)
|
||||
|
||||
```js
|
||||
#!/usr/bin/env node
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
function resolveRoot() {
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
||||
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
if (existsSync(join(root, 'package.json'))) return root;
|
||||
}
|
||||
try {
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const candidate = dirname(scriptDir);
|
||||
if (existsSync(join(candidate, 'package.json'))) return candidate;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
const ROOT = resolveRoot();
|
||||
if (!ROOT) process.exit(0);
|
||||
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
const markerPath = join(ROOT, '.install-version');
|
||||
if (!existsSync(markerPath)) {
|
||||
console.error('claude-mem: runtime not yet set up — run: npx claude-mem repair');
|
||||
process.exit(0);
|
||||
}
|
||||
const marker = JSON.parse(readFileSync(markerPath, 'utf-8'));
|
||||
if (marker.version !== pkg.version) {
|
||||
console.error(`claude-mem: upgraded to v${pkg.version} — run: npx claude-mem repair`);
|
||||
}
|
||||
} catch {
|
||||
console.error('claude-mem: install marker unreadable — run: npx claude-mem repair');
|
||||
}
|
||||
process.exit(0);
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Sub-100ms (two synchronous file reads + JSON.parse + string compare).
|
||||
- Always exits 0 (non-blocking) per the project's exit-code strategy in CLAUDE.md.
|
||||
- Stderr message tells the user exactly what to run if a mismatch is detected.
|
||||
|
||||
### Edit 4B — Rewrite Setup hook command in `plugin/hooks/hooks.json`
|
||||
|
||||
**Lines 4–15** — replace the existing Setup hook command. Current command ends with `node "$_R/scripts/smart-install.js"`. Change it to `node "$_R/scripts/version-check.js"`. Everything before that (PATH export, `_R` resolution, cygpath) stays.
|
||||
|
||||
Concretely: the only change to line 11 is the trailing `smart-install.js` → `version-check.js`.
|
||||
|
||||
### Edit 4C — Delete SessionStart smart-install entry in `plugin/hooks/hooks.json`
|
||||
|
||||
**Lines 17–40** — the SessionStart hook array currently has THREE hook entries:
|
||||
1. `node "$_R/scripts/smart-install.js"` (lines 21–26) — DELETE this entire entry
|
||||
2. `node "$_R/scripts/bun-runner.js" "$_R/scripts/worker-service.cjs" start` (lines 27–32) — KEEP
|
||||
3. `node "$_R/scripts/bun-runner.js" "$_R/scripts/worker-service.cjs" hook claude-code context` (lines 33–38) — KEEP
|
||||
|
||||
After edit, the SessionStart `hooks` array has 2 entries instead of 3.
|
||||
|
||||
**Verification checklist:**
|
||||
- [ ] `cat plugin/hooks/hooks.json | jq '.hooks.Setup[0].hooks[0].command' | grep version-check.js` succeeds
|
||||
- [ ] `cat plugin/hooks/hooks.json | jq '.hooks.SessionStart[0].hooks | length'` returns `2`
|
||||
- [ ] `grep -c "smart-install" plugin/hooks/hooks.json` returns `0`
|
||||
- [ ] `node plugin/scripts/version-check.js` exits 0 in <500ms (time it)
|
||||
- [ ] On a fresh checkout (no `.install-version` marker), version-check stderr says "run: npx claude-mem repair"
|
||||
|
||||
**Anti-pattern guards:**
|
||||
- ❌ Do not change the exit code from 0 — Windows Terminal tab management depends on it (CLAUDE.md exit-code strategy).
|
||||
- ❌ Do not call out to Bun in version-check.js — Node-only, since this runs before we know Bun exists.
|
||||
- ❌ Do not add fancy logic (semver compare, partial recovery). String equality is correct: any version mismatch warrants a repair.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Delete dead code
|
||||
|
||||
**What to implement:** Delete smart-install source files and update tests.
|
||||
|
||||
### Edit 5A — Delete files
|
||||
|
||||
```
|
||||
rm scripts/smart-install.js
|
||||
rm plugin/scripts/smart-install.js
|
||||
rm tests/smart-install.test.ts
|
||||
```
|
||||
|
||||
### Edit 5B — Trim `tests/plugin-scripts-line-endings.test.ts`
|
||||
|
||||
**Line 12 (the `SHEBANG_SCRIPTS` array):** remove the `'smart-install.js'` entry. Keep the rest of the array intact.
|
||||
|
||||
If the array becomes empty after the removal, also remove the entry — but per discovery report it has multiple entries, so just delete the one line.
|
||||
|
||||
### Edit 5C — Add new test for setup-runtime module (optional but recommended)
|
||||
|
||||
**File to create:** `tests/setup-runtime.test.ts`
|
||||
|
||||
Cover:
|
||||
- `readInstallMarker` returns `null` for missing file
|
||||
- `writeInstallMarker` produces a JSON object matching the smart-install.js schema (`{ version, bun, uv, installedAt }`)
|
||||
- `isInstallCurrent` returns `false` for missing marker, `false` for version mismatch, `true` for match
|
||||
- (Skip Bun/uv install integration tests — those need a sandbox and fall outside this PR's scope.)
|
||||
|
||||
If you skip this, document why in the PR description.
|
||||
|
||||
**Verification checklist:**
|
||||
- [ ] `find . -name "smart-install*" -not -path "*/node_modules/*"` returns no results
|
||||
- [ ] `grep -rn "smart-install" tests/` returns no results
|
||||
- [ ] `npm test` (or whatever the project uses) passes
|
||||
- [ ] If `tests/setup-runtime.test.ts` was added, it passes
|
||||
|
||||
**Anti-pattern guards:**
|
||||
- ❌ Do not delete `tests/plugin-scripts-line-endings.test.ts` entirely — it tests other scripts too.
|
||||
- ❌ Do not delete `tests/bun-runner.test.ts` — bun-runner.js stays in this PR.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Update docs
|
||||
|
||||
**What to implement:** Sweep documentation to reflect the new install flow.
|
||||
|
||||
### Edit 6A — `docs/architecture-overview.md:36`
|
||||
|
||||
Update reference to smart-install. New copy: "On first install, `npx claude-mem install` sets up Bun and uv globally and runs `bun install` in the plugin cache. The Setup hook then runs a sub-100ms version check on every Claude Code startup; if the plugin was upgraded externally, the user is prompted to run `npx claude-mem repair`."
|
||||
|
||||
### Edit 6B — `docs/public/configuration.mdx:139,163` and `docs/public/development.mdx:42`
|
||||
|
||||
Replace any mention of smart-install behavior with the version-check + repair model. Two-sentence patches; preserve surrounding context.
|
||||
|
||||
### Edit 6C — `docs/public/hooks-architecture.mdx` (11 references)
|
||||
|
||||
This is the largest doc change. Walk each reference (lines 77, 103, 119, 127, 432, 695–696, 703 per discovery report). Update text describing the Setup hook to say it runs `version-check.js` (sub-100ms) instead of `smart-install.js`. Update SessionStart description to reflect 2 entries (worker start + context fetch) instead of 3.
|
||||
|
||||
### Edit 6D — `docs/public/architecture/` references (lines 149, 193)
|
||||
|
||||
Same pattern — replace smart-install lifecycle description with the npx-installer + version-check model.
|
||||
|
||||
### Edit 6E — Skip CHANGELOG
|
||||
|
||||
CLAUDE.md says: "No need to edit the changelog ever, it's generated automatically." Don't touch it.
|
||||
|
||||
### Edit 6F — Skip docs/reports/
|
||||
|
||||
Those are historical incident reports. Do not retroactively edit them — they describe past behavior.
|
||||
|
||||
**Verification checklist:**
|
||||
- [ ] `grep -rn "smart-install" docs/public/` returns no results
|
||||
- [ ] `grep -rn "smart-install" docs/architecture-overview.md` returns no results
|
||||
- [ ] (Optional) Render docs locally via Mintlify dev server and visually scan the architecture page
|
||||
|
||||
**Anti-pattern guards:**
|
||||
- ❌ Do not edit `docs/reports/*.md` — those are dated incident reports, leave them alone.
|
||||
- ❌ Do not edit CHANGELOG.md.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Build, test, manual verify
|
||||
|
||||
**What to implement:** End-to-end validation. This phase is run by the implementer before opening the PR.
|
||||
|
||||
### Edit 7A — Build
|
||||
|
||||
```bash
|
||||
npm run build-and-sync
|
||||
```
|
||||
|
||||
This must succeed. If TypeScript fails on the new `setup-runtime.ts`, fix in place.
|
||||
|
||||
### Edit 7B — Test suite
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Must be green. Likely failures to anticipate:
|
||||
- `plugin-scripts-line-endings.test.ts` if the `'smart-install.js'` entry was missed in Phase 5
|
||||
- Any test that imports from `scripts/smart-install.js` (discovery report says only `tests/smart-install.test.ts`, which Phase 5 deletes)
|
||||
|
||||
### Edit 7C — Manual fresh-install verification
|
||||
|
||||
1. On a clean machine (or after `rm -rf ~/.claude/plugins/marketplaces/thedotmack ~/.claude/plugins/cache/thedotmack ~/.claude-mem`):
|
||||
```bash
|
||||
npx claude-mem install
|
||||
```
|
||||
Confirm:
|
||||
- Spinner says "Setting up runtime (first install can take ~30s)"
|
||||
- No silent dead air
|
||||
- Worker starts at the end
|
||||
2. Open Claude Code in any project. Confirm:
|
||||
- Setup hook fires fast (<200ms total)
|
||||
- SessionStart fires fast (no smart-install delay)
|
||||
- No "claude plugin install" output
|
||||
3. Simulate a stale install:
|
||||
```bash
|
||||
rm ~/.claude/plugins/cache/thedotmack/claude-mem/<version>/.install-version
|
||||
```
|
||||
Open a new Claude Code session. Confirm version-check.js prints the "run: npx claude-mem repair" message to stderr.
|
||||
4. Run repair:
|
||||
```bash
|
||||
npx claude-mem repair
|
||||
```
|
||||
Confirm spinner runs through Bun/uv check + bun install + marker write, then exits clean.
|
||||
|
||||
### Edit 7D — Commit and open PR
|
||||
|
||||
Per the PR creation flow in the user's outer task. Don't auto-merge; the user wants a review loop.
|
||||
|
||||
**Verification checklist:**
|
||||
- [ ] `npm run build-and-sync` exits 0
|
||||
- [ ] `npm test` exits 0
|
||||
- [ ] Manual fresh install completes with visible spinner, no silent dead air
|
||||
- [ ] Setup hook fires <200ms after rebuild
|
||||
- [ ] `npx claude-mem repair` runs end-to-end
|
||||
|
||||
**Anti-pattern guards:**
|
||||
- ❌ Do not skip the manual verification — the whole point of this PR is UX (eliminating dead air). Type checks won't catch a regression.
|
||||
- ❌ Do not bump the version — version bump is handled separately by the version-bump skill.
|
||||
|
||||
---
|
||||
|
||||
## Summary of file changes
|
||||
|
||||
| Type | Path | Phase |
|
||||
|---|---|---|
|
||||
| Created | `src/npx-cli/install/setup-runtime.ts` | 1 |
|
||||
| Edited | `src/npx-cli/commands/install.ts` | 2 |
|
||||
| Edited | `src/npx-cli/index.ts` | 3 |
|
||||
| Created | `plugin/scripts/version-check.js` | 4 |
|
||||
| Edited | `plugin/hooks/hooks.json` | 4 |
|
||||
| Deleted | `scripts/smart-install.js` | 5 |
|
||||
| Deleted | `plugin/scripts/smart-install.js` | 5 |
|
||||
| Deleted | `tests/smart-install.test.ts` | 5 |
|
||||
| Edited | `tests/plugin-scripts-line-endings.test.ts` | 5 |
|
||||
| Created | `tests/setup-runtime.test.ts` (optional) | 5 |
|
||||
| Edited | `docs/architecture-overview.md` | 6 |
|
||||
| Edited | `docs/public/configuration.mdx` | 6 |
|
||||
| Edited | `docs/public/development.mdx` | 6 |
|
||||
| Edited | `docs/public/hooks-architecture.mdx` | 6 |
|
||||
| Edited | `docs/public/architecture/*.md` | 6 |
|
||||
|
||||
Estimated diff: **+250 / −500 lines** (net deletion).
|
||||
@@ -0,0 +1,367 @@
|
||||
# Onboarding UX Overhaul
|
||||
|
||||
Three surfaces, one product voice, one first-success moment. Each phase is self-contained and can be executed in a fresh chat with `/do`.
|
||||
|
||||
## North Star
|
||||
|
||||
Pull the user toward this single moment: **open the viewer in a browser, do anything in Claude Code, watch an observation appear within seconds.** All three surfaces aim at it from different angles.
|
||||
|
||||
## Cross-Cutting Facts (read this first, every phase)
|
||||
|
||||
- **Test runner:** `bun test`. Test command: `npm run test`. Tests live in `tests/`. Pattern templates: `tests/sqlite/observations.test.ts:1-60` (in-memory SQLite + bun:test), `tests/install-non-tty.test.ts:1-95` (regex assertions over install.ts source).
|
||||
- **Build:** `npm run build-and-sync` runs full build (banner frames + plugin manifests + `scripts/build-hooks.js`) → marketplace sync → worker restart. Viewer compiles via esbuild to `plugin/ui/viewer-bundle.js`; HTML template (which holds ALL CSS) at `src/ui/viewer-template.html`.
|
||||
- **Settings defaults:** `src/shared/SettingsDefaultsManager.ts:70-131`. Merge logic at `loadFromFile()` lines 161-205 — missing keys auto-pick up new defaults, explicit values are respected. Forward-compatible.
|
||||
- **`CLAUDE_MEM_WELCOME_HINT_ENABLED` already defaults to `'true'`** (`SettingsDefaultsManager.ts:104`). Single reader at `SearchRoutes.ts:294`. Goal 5 from the brief is already done — we replace "flip the default" with "pin it with a regression test."
|
||||
- **Timing line, identical wording everywhere:** `Memory injection starts on your second session in a project.`
|
||||
- **Privacy line, identical wording everywhere:** `Everything stays in ~/.claude-mem on this machine.`
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Documentation Discovery (DONE; for reference)
|
||||
|
||||
Discovery already completed. Allowed APIs and signatures established:
|
||||
|
||||
### Install.ts patterns (`src/npx-cli/commands/install.ts`)
|
||||
- `log` helper at lines 41-46 — methods `info | success | warn | error`, conditionally routes to `p.log.*` (interactive) vs `console.log/warn/error` (non-interactive, 2-space indent).
|
||||
- `p` is `* as p from '@clack/prompts'`. Used: `p.note(body, title)`, `p.outro(msg)`, `p.intro`, `p.log.*`, `p.tasks`, `p.spinner`, `p.select/multiselect/confirm/password`, `p.isCancel`, `p.cancel`.
|
||||
- `pc` is `picocolors` default import. Available: `pc.cyan/green/yellow/red/bold/underline/dim/bgCyan/black`. **`pc.dim` exists** (already in use at line 663).
|
||||
- `getSetting('CLAUDE_MEM_WORKER_PORT')` returns string; convert with `Number()` when needed.
|
||||
- Health probe pattern at lines 843-864: `fetch('http://127.0.0.1:${port}/api/health', { signal: AbortSignal.timeout(3000) })`, non-throwing.
|
||||
- Existing `summaryLines` block (826-841) and `nextSteps` block (866-896) — both have parallel interactive (`p.note`) and non-interactive (`console.log`) branches.
|
||||
|
||||
### Settings (`src/shared/SettingsDefaultsManager.ts`)
|
||||
- `CLAUDE_MEM_WELCOME_HINT_ENABLED: 'true'` at line 104.
|
||||
- Merge: defaults first, then file overrides, then env overrides (lines 194-201).
|
||||
- Install does NOT pre-seed this key — only seeds prompted settings (provider, model). Existing users without explicit value automatically get the new default.
|
||||
- `SettingsRoutes.ts:84-117` — flag is NOT in the user-updatable allowlist (read-only via UI).
|
||||
- Test template: `tests/install-non-tty.test.ts` (regex over source); SettingsDefaultsManager has no dedicated test file — would be created if needed.
|
||||
|
||||
### SessionStart hint (`src/services/worker/http/routes/SearchRoutes.ts`)
|
||||
- `WELCOME_HINT_TEMPLATE` at lines 14-27. Used at line 301: `WELCOME_HINT_TEMPLATE.replace('{viewer_url}', viewerUrl)`.
|
||||
- Gating logic at lines 293-306. Only fires when `hintEnabled && !full && observationCount === 0`.
|
||||
- Output is plain text injected as SessionStart `additionalContext` via the SessionStart hook (`src/cli/handlers/context.ts`).
|
||||
|
||||
### Viewer (`src/ui/viewer/`)
|
||||
- `useSSE()` at `src/ui/viewer/hooks/useSSE.ts:1-148` exposes `{ observations, summaries, prompts, projects, sources, projectsBySource, isProcessing, queueDepth, isConnected }`. Auto-reconnects; new observations prepended via `'new_observation'` SSE event.
|
||||
- `WelcomeCard` mounted in `src/ui/viewer/App.tsx:128-130`, currently receives only `onDismiss`. App has access to all SSE state (lines 51-67).
|
||||
- All viewer CSS lives in `src/ui/viewer-template.html`; existing `.welcome-card*` styles at lines 1443-1561; existing `.status-dot` + `@keyframes pulse` at lines 754-764.
|
||||
- Stats endpoints: `/api/stats` (`DataRoutes.ts:204-242`) returns `{database: {observations, sessions, summaries}, worker: {...}}`. `/api/projects` (`DataRoutes.ts:244-260`) returns `ProjectCatalog`. **No `firstObservationAt` field currently — Phase 4 adds it.**
|
||||
- `/api/how-it-works` is NOT a static explainer — it queries observations tagged with the `'how-it-works'` concept (`SearchManager.ts:836-884`). Useless on a fresh install. Phase 1 adds a true static explainer.
|
||||
|
||||
### Skills (`plugin/skills/`)
|
||||
- 9 existing skills, each a directory with `SKILL.md` and YAML frontmatter (`name`, `description`). No central registry — discovered by directory convention.
|
||||
- Template to copy: `plugin/skills/mem-search/SKILL.md`.
|
||||
|
||||
### Anti-patterns to avoid
|
||||
- DO NOT call `/api/how-it-works` for an onboarding explainer — wrong endpoint.
|
||||
- DO NOT add new viewer CSS files — all styles in `src/ui/viewer-template.html`.
|
||||
- DO NOT add new viewer routes for stats unless strictly needed — extend `/api/stats` instead.
|
||||
- DO NOT seed `CLAUDE_MEM_WELCOME_HINT_ENABLED` in `install.ts` — defaults already handle it.
|
||||
- DO NOT pass imperatives ("you should run X") in the SessionStart hint — Claude will try to execute. Use third-person narration ("`/learn-codebase` is available if…").
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Canonical Onboarding Explainer
|
||||
|
||||
**Why:** All three surfaces need a single source of truth for the 90-second "what is this" explainer. `/api/how-it-works` does not serve this purpose. We'll create a real static explainer and link to it from everywhere.
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Create `src/services/worker/onboarding-explainer.md` — single canonical content. ~150 words, three sections:
|
||||
- **What it does:** Every Read/Edit/Bash Claude makes turns into a compressed observation. Observations get summarized at session end. Relevant ones get auto-injected into future prompts.
|
||||
- **When it kicks in:** Memory injection starts on your second session in a project. *(verbatim timing line)*
|
||||
- **Where data lives:** Everything stays in ~/.claude-mem on this machine. *(verbatim privacy line)*
|
||||
|
||||
2. Add new route `GET /api/onboarding/explainer` in `src/services/worker/http/routes/SearchRoutes.ts`:
|
||||
- Read the markdown file at boot (cache like `cachedSkillMd` pattern in `Server.ts:18-33`).
|
||||
- Serve as `text/markdown; charset=utf-8`.
|
||||
- Register in `setupRoutes()` next to the other `/api/context/*` routes.
|
||||
|
||||
3. Create `plugin/skills/how-it-works/SKILL.md`:
|
||||
- Copy frontmatter shape from `plugin/skills/mem-search/SKILL.md:1-4`.
|
||||
- `name: how-it-works`
|
||||
- `description: Explain how claude-mem captures observations, when memory injection kicks in, and where data lives. Use when the user asks "how does claude-mem work?" or "what is this thing doing?".`
|
||||
- Body: same content as the markdown explainer (or fetch `/api/onboarding/explainer` at runtime).
|
||||
- Wire into `scripts/build-hooks.js` verification list (lines 336-348) so build fails if the file is missing.
|
||||
|
||||
### Verification
|
||||
|
||||
- `npm run build-and-sync` succeeds; new SKILL.md present in `plugin/skills/how-it-works/`.
|
||||
- `curl http://localhost:$PORT/api/onboarding/explainer` returns the markdown.
|
||||
- Worker boot log includes a "Cached onboarding explainer at boot" entry (mirroring the SKILL.md cache log).
|
||||
|
||||
### Anti-pattern guards
|
||||
|
||||
- Do NOT alter `/api/how-it-works`. It serves a different (concept-tagged search) purpose.
|
||||
- Do NOT inline the explainer text into install.ts / WelcomeCard / WELCOME_HINT_TEMPLATE — link, don't duplicate.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — SessionStart Welcome Hint Rewrite
|
||||
|
||||
**Why:** Current copy reads as a marketing intercept inside Claude's context, leads with imperatives Claude tries to execute, and doesn't set the truthful "today seeds, tomorrow injects" expectation.
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Rewrite `WELCOME_HINT_TEMPLATE` at `src/services/worker/http/routes/SearchRoutes.ts:14-27`. Target:
|
||||
|
||||
```
|
||||
# claude-mem status
|
||||
|
||||
This project has no memory yet. The current session will seed it; subsequent sessions will receive auto-injected context for relevant past work.
|
||||
|
||||
Memory injection starts on your second session in a project.
|
||||
|
||||
`/learn-codebase` is available if the user wants to front-load the entire repo into memory in a single pass (~5 minutes on a typical repo, optional). Otherwise memory builds passively as work happens.
|
||||
|
||||
Live activity: {viewer_url}
|
||||
How it works: `/how-it-works`
|
||||
|
||||
This message disappears once the first observation lands.
|
||||
```
|
||||
|
||||
Constraints: third-person narration referring to "the user", not imperatives directed at Claude. Title is "status", not "Welcome".
|
||||
|
||||
2. **Pin the default with a test.** In a new file `tests/shared/welcome-hint-default.test.ts`:
|
||||
- Assert `SettingsDefaultsManager.getAllDefaults().CLAUDE_MEM_WELCOME_HINT_ENABLED === 'true'`.
|
||||
- Assert that an empty settings file resolves to `'true'`.
|
||||
- Assert that an explicit `'false'` is preserved through `loadFromFile`.
|
||||
|
||||
3. No install.ts seeding change — defaults already flow through.
|
||||
|
||||
4. Audit existing welcome-hint tests (memory note: "4/4 tests pass"). Likely in `tests/worker/SearchManager.timeline-anchor.test.ts` per discovery; if those tests assert the old template body verbatim, update them to match the new copy. If they only assert the gating logic, leave alone.
|
||||
|
||||
### Verification
|
||||
|
||||
- `bun test tests/shared/welcome-hint-default.test.ts` passes.
|
||||
- `bun test tests/worker/` (or whichever file holds the welcome-hint tests) passes.
|
||||
- Manual: in a fresh project with zero observations, start a Claude Code session — SessionStart context includes the new status note. New text contains the verbatim timing line and points at `{viewer_url}` and `/how-it-works`.
|
||||
- Manual: in a project with observations, the hint does NOT appear (gating still works).
|
||||
|
||||
### Anti-pattern guards
|
||||
|
||||
- Do NOT use the word "Welcome" or any second-person imperatives ("you should…", "go to…"). Claude will try to "help" by executing them.
|
||||
- Do NOT exceed ~10 lines — this is injected into Claude's context for every fresh-project session.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Post-Install Next Steps Rewrite
|
||||
|
||||
**Why:** Current 4-bullet menu treats `/learn-codebase`, `/mem-search`, and `/knowledge-agent` as parallel options. They aren't — `/learn-codebase` is the only first-session move and even it's optional. Lead with proof (live viewer), give two paths, defuse the privacy concern.
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Replace the `nextSteps` array at `src/npx-cli/commands/install.ts:866-878`. Target body when worker is ready:
|
||||
|
||||
```
|
||||
${pc.green('✓')} Worker running at ${pc.underline(`http://localhost:${actualPort}`)}
|
||||
|
||||
${pc.bold('First success:')} keep that URL open in a browser, then open Claude Code in any project. Observations stream in as Claude reads, edits, and runs commands.
|
||||
|
||||
${pc.bold('Two paths from here:')}
|
||||
${pc.cyan('A.')} Just start working. Memory builds passively from your first prompt. (Recommended.)
|
||||
${pc.cyan('B.')} Front-load it: open Claude Code and run ${pc.bold('/learn-codebase')} to ingest the whole repo (~5 min, optional).
|
||||
|
||||
Memory injection starts on your second session in a project.
|
||||
Everything stays in ${pc.cyan('~/.claude-mem')} on this machine.
|
||||
|
||||
${pc.dim('How it works: /how-it-works · Disable first-session hint: CLAUDE_MEM_WELCOME_HINT_ENABLED=false')}
|
||||
${pc.dim('Note: close all Claude Code sessions before uninstalling, or ~/.claude-mem will be recreated by active hooks.')}
|
||||
```
|
||||
|
||||
Worker-not-ready branch: keep the existing `pc.yellow('!')` warning + retry hint, then append the same "First success" / "Two paths" / timing / privacy lines (substituting `workerPort` for `actualPort`).
|
||||
|
||||
2. Drop `/mem-search` and `/knowledge-agent` lines from this surface entirely. (They reappear in WelcomeCard for users who do open the viewer.)
|
||||
|
||||
3. Keep both `isInteractive` (uses `p.note(nextSteps.join('\n'), 'Next Steps')`) and non-interactive (`console.log` per line, 2-space indent) branches in sync. The array shape stays the same — only the strings change.
|
||||
|
||||
4. Verify `pc.dim` renders correctly under the clack `p.note` box (it does — line 663 already uses it).
|
||||
|
||||
### Verification
|
||||
|
||||
- `npm run build` succeeds.
|
||||
- Manual interactive run: `npx claude-mem install` in a fresh dir shows the new Next Steps block inside the clack box.
|
||||
- Manual non-interactive run: `CI=true npx claude-mem install` (or pipe through cat) shows the same content with 2-space indent and no clack boxes.
|
||||
- Update `tests/install-non-tty.test.ts` regex assertions to match the new strings (existing pattern: `expect(installSource).toContain(...)`).
|
||||
|
||||
### Anti-pattern guards
|
||||
|
||||
- Do NOT add new commands to this surface. The point is reduction.
|
||||
- Do NOT lose the uninstall caveat — demote, don't delete.
|
||||
- Do NOT reorder so the worker URL becomes a footnote — it's the single most important payload here.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Extend `/api/stats` with `firstObservationAt`
|
||||
|
||||
**Why:** The viewer micro-stat row (Phase 5) needs a "since [date]" value. No HTTP endpoint currently exposes the earliest observation timestamp. Smallest possible backend change to enable Phase 5.
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Add a `firstObservationAt: string | null` field to the stats response in `src/services/worker/http/routes/DataRoutes.ts:204-242` (`handleGetStats`).
|
||||
|
||||
2. Add a SQL helper next to `getRecentObservations` (`src/services/sqlite/observations/recent.ts:6-20`):
|
||||
|
||||
```ts
|
||||
export function getFirstObservationCreatedAt(db: SessionStore): string | null {
|
||||
// SELECT created_at FROM observations ORDER BY created_at_epoch ASC LIMIT 1
|
||||
}
|
||||
```
|
||||
|
||||
Match the existing prepared-statement pattern in that directory.
|
||||
|
||||
3. Wire the helper into `handleGetStats` and surface as ISO string (or `null` if no observations). Verify the existing TypeScript type for the stats response is updated.
|
||||
|
||||
### Verification
|
||||
|
||||
- `bun test tests/sqlite/observations.test.ts` still passes.
|
||||
- New unit test in `tests/sqlite/observations.test.ts` (or a new file) covering `getFirstObservationCreatedAt` for empty + non-empty DB.
|
||||
- `curl http://localhost:$PORT/api/stats` returns the new field.
|
||||
|
||||
### Anti-pattern guards
|
||||
|
||||
- Do NOT add new endpoints — extend the existing `/api/stats` payload.
|
||||
- Do NOT add per-project earliest-timestamp logic; the viewer stat row is global ("X observations · Y projects · since [date]").
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Viewer WelcomeCard Rewrite
|
||||
|
||||
**Why:** Current card is generic and doesn't differentiate the empty state (the moment the user is asking "is anything happening?") from the data state (the moment the user is asking "what can I do here?").
|
||||
|
||||
### Tasks
|
||||
|
||||
1. **App.tsx wiring** (`src/ui/viewer/App.tsx:128-130`). Pass new props to `WelcomeCard`:
|
||||
```tsx
|
||||
<WelcomeCard
|
||||
onDismiss={...}
|
||||
observationCount={allObservations.length}
|
||||
projectCount={projects.length}
|
||||
isConnected={isConnected}
|
||||
firstObservationAt={stats.firstObservationAt} // new — fetched from /api/stats
|
||||
/>
|
||||
```
|
||||
If a stats fetch hook doesn't already exist, add one (`useStats()` at `src/ui/viewer/hooks/useStats.ts`) that polls `/api/stats` on mount and on each new SSE observation.
|
||||
|
||||
2. **WelcomeCard.tsx rewrite** (`src/ui/viewer/components/WelcomeCard.tsx`):
|
||||
- Bump localStorage key to `claude-mem-welcome-dismissed-v2` (keep helpers in same file). v1 dismissals should NOT carry over — the card is meaningfully different.
|
||||
- Branch on `observationCount === 0`:
|
||||
- **Empty state:**
|
||||
- Headline: "No observations yet."
|
||||
- Body: "Open Claude Code in any project — entries stream in here as Claude reads, edits, and runs commands."
|
||||
- Live status row with a `<span class="welcome-card-status-dot" data-connected={isConnected ? 'true' : 'false'} />` and label "Connected to worker · waiting for activity" / "Reconnecting…" based on `isConnected`.
|
||||
- Footer: "How it works" link + dismiss button (existing behavior).
|
||||
- **Has-data state:**
|
||||
- Headline: "claude-mem"
|
||||
- Body: "Persistent memory across Claude Code sessions."
|
||||
- Stat row: `${observationCount} observations · ${projectCount} projects · since ${formatDate(firstObservationAt)}`.
|
||||
- Two example prompts (cut from four):
|
||||
- `<code>ask:</code> did we already solve X?`
|
||||
- `<code>/mem-search</code> dig into past work`
|
||||
- Footer: "How it works" + "Read the docs" links + dismiss.
|
||||
- "How it works" link points to `/api/onboarding/explainer` (opens in new tab as raw markdown — acceptable for v1; or a small modal showing the markdown rendered).
|
||||
|
||||
3. **CSS additions** in `src/ui/viewer-template.html` next to `.welcome-card-*` styles (lines 1443-1561). Reuse existing `@keyframes pulse` (line 754). Add:
|
||||
- `.welcome-card-status-dot` (8×8 circle, error color + pulse when disconnected, success color + no animation when `data-connected="true"`).
|
||||
- `.welcome-card-stats` (single-row, dim text, dot separators using `·`).
|
||||
- `.welcome-card-empty` adjustments (slightly larger lede, status row layout).
|
||||
|
||||
4. **Auto-dismiss on first observation:** in App.tsx, add an effect that flips the card from empty→has-data view automatically when `observationCount` crosses 0→1. The card should NOT auto-dismiss permanently on the first observation — it just transitions states. The user explicitly dismisses with the X.
|
||||
|
||||
### Verification
|
||||
|
||||
- `npm run build-and-sync` succeeds; viewer bundle rebuilds.
|
||||
- Open viewer in a fresh-install state: empty card shows, dot animates (or is solid green if connected).
|
||||
- In Claude Code, do one Read in a project. The viewer card flips to has-data state without a manual refresh, stat row populates.
|
||||
- Dismiss persists across reload (localStorage v2 key).
|
||||
- Header "Show help" button still re-opens the card.
|
||||
- Tests: a small unit test for `getStoredWelcomeDismissed` / `setStoredWelcomeDismissed` against the new v2 key (extend the helper logic — pure functions are easy to test even without React Testing Library).
|
||||
|
||||
### Anti-pattern guards
|
||||
|
||||
- Do NOT add a new CSS file. All styles in `viewer-template.html`.
|
||||
- Do NOT poll `/api/stats` on every render — once on mount + on `'new_observation'` SSE event is enough.
|
||||
- Do NOT auto-permanently-dismiss on first observation; users may want to keep the card visible for the example prompts.
|
||||
- Do NOT inline the explainer text into the card — link to `/api/onboarding/explainer`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Drift Audit
|
||||
|
||||
**Why:** Three surfaces, two verbatim lines, one explainer source. Catch any divergence before it ships.
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Grep for the timing line and assert it appears verbatim in:
|
||||
- `src/services/worker/http/routes/SearchRoutes.ts` (Phase 2)
|
||||
- `src/npx-cli/commands/install.ts` (Phase 3)
|
||||
- `src/services/worker/onboarding-explainer.md` (Phase 1)
|
||||
|
||||
```bash
|
||||
grep -rn "Memory injection starts on your second session in a project" src/ plugin/
|
||||
# expect 3+ matches
|
||||
```
|
||||
|
||||
2. Same for the privacy line (`Everything stays in ~/.claude-mem on this machine.`).
|
||||
|
||||
3. Confirm `/how-it-works` slash reference appears in install.ts and SearchRoutes.ts; SKILL.md exists at `plugin/skills/how-it-works/SKILL.md`.
|
||||
|
||||
4. Confirm WelcomeCard does NOT inline the explainer body — only the link.
|
||||
|
||||
5. Confirm no surface still says "/knowledge-agent" or "/mem-search" in install.ts post-install copy.
|
||||
|
||||
### Verification
|
||||
|
||||
- All grep checks pass.
|
||||
- Manual: read all three surfaces side by side. Each is distinctly framed (install = two paths + first-success; SessionStart = third-person status; viewer = empty/has-data with live dot). The two verbatim lines and the `/how-it-works` link are the only repeated content.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — End-to-End Smoke Test (manual)
|
||||
|
||||
**Why:** The acceptance criterion in the brief is a single coherent flow. This phase walks it.
|
||||
|
||||
### Steps
|
||||
|
||||
1. Fresh install:
|
||||
```bash
|
||||
rm -rf ~/.claude-mem
|
||||
npx claude-mem install
|
||||
```
|
||||
Verify: install Next Steps shows the new "Two paths" + first-success + timing + privacy + `/how-it-works` block.
|
||||
|
||||
2. Open the viewer at the printed URL. Verify: empty state shows, dot is green (connected) or red+pulsing (disconnected briefly).
|
||||
|
||||
3. Open Claude Code in any project. Type a prompt that causes one Read.
|
||||
- Verify in Claude Code: SessionStart context contains the new status note, NOT a "Welcome" block. Claude does not act on the bullets — at most relays them.
|
||||
- Verify in viewer: card flips to has-data state, stat row populates, observation appears in the feed.
|
||||
|
||||
4. End the session. Start a second Claude Code session in the same project.
|
||||
- Verify: SessionStart context this time contains injected past observations (not the welcome hint, since `observationCount > 0`).
|
||||
|
||||
5. Click the "How it works" link from the viewer card. Verify: it loads `/api/onboarding/explainer` markdown.
|
||||
|
||||
### Verification
|
||||
|
||||
All four observable beats in the acceptance criterion happen as described, in order, without any surface contradicting another on facts (timing, privacy, command names).
|
||||
|
||||
---
|
||||
|
||||
## Execution Order Summary
|
||||
|
||||
1. **Phase 1** (explainer + skill) — unblocks everything else by establishing the canonical content source.
|
||||
2. **Phase 4** (`/api/stats` extension) — unblocks Phase 5; tiny isolated backend change, do it early.
|
||||
3. **Phase 2** (SessionStart hint rewrite + default-pinning test) — independent, do in parallel with Phase 3.
|
||||
4. **Phase 3** (install.ts Next Steps rewrite) — independent of Phase 2.
|
||||
5. **Phase 5** (WelcomeCard rewrite) — depends on Phases 1 + 4.
|
||||
6. **Phase 6** (drift audit) — runs after all copy changes land.
|
||||
7. **Phase 7** (manual smoke) — final gate before commit/PR.
|
||||
|
||||
Phases 2, 3, and 4 are independent and could be parallelized in three short sessions if desired.
|
||||
|
||||
## Out of Scope (do not touch)
|
||||
|
||||
- Splash banner / installer animation work that just shipped on this branch.
|
||||
- `/learn-codebase`, `/mem-search`, `/knowledge-agent` skill internals — only how we reference them.
|
||||
- New viewer pages or routes beyond `/api/onboarding/explainer` and the `firstObservationAt` field on `/api/stats`.
|
||||
- Public docs in `docs/public/` — covered by the existing Mintlify deploy; only update if a doc page directly contradicts the new copy.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "12.4.7",
|
||||
"version": "12.5.1",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{
|
||||
"type": "command",
|
||||
"shell": "bash",
|
||||
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.claude/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; command -v cygpath >/dev/null 2>&1 && { _W=$(cygpath -w \"$_R\" 2>/dev/null); [ -n \"$_W\" ] && _R=\"$_W\"; }; node \"$_R/scripts/smart-install.js\"",
|
||||
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.claude/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; command -v cygpath >/dev/null 2>&1 && { _W=$(cygpath -w \"$_R\" 2>/dev/null); [ -n \"$_W\" ] && _R=\"$_W\"; }; node \"$_R/scripts/version-check.js\"",
|
||||
"timeout": 300
|
||||
}
|
||||
]
|
||||
@@ -21,12 +21,6 @@
|
||||
{
|
||||
"type": "command",
|
||||
"shell": "bash",
|
||||
"command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.claude/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; command -v cygpath >/dev/null 2>&1 && { _W=$(cygpath -w \"$_R\" 2>/dev/null); [ -n \"$_W\" ] && _R=\"$_W\"; }; node \"$_R/scripts/smart-install.js\"",
|
||||
"timeout": 300
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"shell": "bash",
|
||||
"command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.claude/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; command -v cygpath >/dev/null 2>&1 && { _W=$(cygpath -w \"$_R\" 2>/dev/null); [ -n \"$_W\" ] && _R=\"$_W\"; }; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" start; echo '{\"continue\":true,\"suppressOutput\":true}'",
|
||||
"timeout": 60
|
||||
},
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem-plugin",
|
||||
"version": "12.4.7",
|
||||
"version": "12.5.1",
|
||||
"private": true,
|
||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||
"type": "module",
|
||||
@@ -32,6 +32,12 @@
|
||||
"@derekstride/tree-sitter-sql": "^0.3.11",
|
||||
"@tree-sitter-grammars/tree-sitter-markdown": "^0.3.2"
|
||||
},
|
||||
"overrides": {
|
||||
"tree-sitter": "^0.25.0"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"tree-sitter-cli"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"bun": ">=1.0.0"
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Bun Runner - Finds and executes Bun even when not in PATH
|
||||
*
|
||||
* This script solves the fresh install problem where:
|
||||
* 1. smart-install.js installs Bun to ~/.bun/bin/bun
|
||||
* 2. But Bun isn't in PATH until terminal restart
|
||||
* 3. Subsequent hooks fail because they can't find `bun`
|
||||
*
|
||||
* Usage: node bun-runner.js <script> [args...]
|
||||
*
|
||||
* Fixes #818: Worker fails to start on fresh install
|
||||
*/
|
||||
import { spawnSync, spawn } from 'child_process';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join, dirname, resolve } from 'path';
|
||||
@@ -19,20 +7,9 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
// Self-resolve plugin root when CLAUDE_PLUGIN_ROOT is not set by Claude Code.
|
||||
// Upstream bug: anthropics/claude-code#24529 — Stop hooks (and on Linux, all hooks)
|
||||
// don't receive CLAUDE_PLUGIN_ROOT, causing script paths to resolve to /scripts/...
|
||||
// which doesn't exist. This fallback derives the plugin root from bun-runner.js's
|
||||
// own filesystem location (this file lives in <plugin-root>/scripts/).
|
||||
const __bun_runner_dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const RESOLVED_PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || resolve(__bun_runner_dirname, '..');
|
||||
|
||||
/**
|
||||
* Fix script path arguments that were broken by empty CLAUDE_PLUGIN_ROOT.
|
||||
* When CLAUDE_PLUGIN_ROOT is empty, "${CLAUDE_PLUGIN_ROOT}/scripts/foo.cjs"
|
||||
* expands to "/scripts/foo.cjs" which doesn't exist. Detect this and rewrite
|
||||
* the path using our self-resolved plugin root.
|
||||
*/
|
||||
function fixBrokenScriptPath(argPath) {
|
||||
if (argPath.startsWith('/scripts/') && !existsSync(argPath)) {
|
||||
const fixedPath = join(RESOLVED_PLUGIN_ROOT, argPath);
|
||||
@@ -43,14 +20,7 @@ function fixBrokenScriptPath(argPath) {
|
||||
return argPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Bun executable - checks PATH first, then common install locations
|
||||
*/
|
||||
function findBun() {
|
||||
// Try PATH first.
|
||||
// On Windows, pass a single command string to avoid Node 22+ DEP0190 deprecation warning
|
||||
// (triggered when an args array is combined with shell:true, as the args are only
|
||||
// concatenated, not escaped). Fixes #1503.
|
||||
const pathCheck = IS_WINDOWS
|
||||
? spawnSync('where bun', {
|
||||
encoding: 'utf-8',
|
||||
@@ -63,19 +33,15 @@ function findBun() {
|
||||
});
|
||||
|
||||
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
|
||||
// On Windows, prefer bun.cmd over bun (bun is a shell script, bun.cmd is the Windows batch file)
|
||||
if (IS_WINDOWS) {
|
||||
const bunCmdPath = pathCheck.stdout.split('\n').find(line => line.trim().endsWith('bun.cmd'));
|
||||
if (bunCmdPath) {
|
||||
return bunCmdPath.trim();
|
||||
}
|
||||
}
|
||||
return 'bun'; // Found in PATH
|
||||
return 'bun';
|
||||
}
|
||||
|
||||
// Check common installation paths (handles fresh installs before PATH reload)
|
||||
// Windows: Bun installs to ~/.bun/bin/bun.exe (same as smart-install.js)
|
||||
// Unix: Check default location plus common package manager paths
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [
|
||||
@@ -94,8 +60,6 @@ function findBun() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Early exit if plugin is disabled in Claude Code settings (#781).
|
||||
// Sync read + JSON parse — fastest possible check before spawning Bun.
|
||||
function isPluginDisabledInClaudeSettings() {
|
||||
try {
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
||||
@@ -112,7 +76,6 @@ if (isPluginDisabledInClaudeSettings()) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Get args: node bun-runner.js <script> [args...]
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
@@ -120,7 +83,6 @@ if (args.length === 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fix broken script paths caused by empty CLAUDE_PLUGIN_ROOT (#1215)
|
||||
args[0] = fixBrokenScriptPath(args[0]);
|
||||
|
||||
const bunPath = findBun();
|
||||
@@ -131,14 +93,8 @@ if (!bunPath) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fix #646: Buffer stdin in Node.js before passing to Bun.
|
||||
// On Linux, Bun's libuv calls fstat() on inherited pipe fds and crashes with
|
||||
// EINVAL when the pipe comes from Claude Code's hook system. By reading stdin
|
||||
// in Node.js first and writing it to a fresh pipe, Bun receives a normal pipe
|
||||
// that it can fstat() without errors.
|
||||
function collectStdin() {
|
||||
return new Promise((resolve) => {
|
||||
// If stdin is a TTY (interactive), there's no piped data to collect
|
||||
if (process.stdin.isTTY) {
|
||||
resolve(null);
|
||||
return;
|
||||
@@ -150,11 +106,9 @@ function collectStdin() {
|
||||
resolve(chunks.length > 0 ? Buffer.concat(chunks) : null);
|
||||
});
|
||||
process.stdin.on('error', () => {
|
||||
// stdin may not be readable (e.g. already closed), treat as no data
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
// Safety: if no data arrives within 5s, proceed without stdin
|
||||
setTimeout(() => {
|
||||
process.stdin.removeAllListeners();
|
||||
process.stdin.pause();
|
||||
@@ -165,10 +119,6 @@ function collectStdin() {
|
||||
|
||||
const stdinData = await collectStdin();
|
||||
|
||||
// Spawn Bun with the provided script and args
|
||||
// Use spawn (not spawnSync) to properly handle stdio
|
||||
// On Windows, use cmd.exe to execute bun.cmd since npm-installed bun is a batch file
|
||||
// Use windowsHide to prevent a visible console window from spawning on Windows
|
||||
const spawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'inherit'],
|
||||
windowsHide: true,
|
||||
@@ -179,17 +129,14 @@ let spawnCmd = bunPath;
|
||||
let spawnArgs = args;
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
// On Windows, bun.cmd must be executed via cmd /c
|
||||
spawnCmd = 'cmd';
|
||||
spawnArgs = ['/c', bunPath, ...args];
|
||||
const quote = (s) => `"${String(s).replace(/"/g, '\\"')}"`;
|
||||
spawnOptions.shell = true;
|
||||
spawnCmd = [bunPath, ...args].map(quote).join(' ');
|
||||
spawnArgs = [];
|
||||
}
|
||||
|
||||
const child = spawn(spawnCmd, spawnArgs, spawnOptions);
|
||||
|
||||
// Write buffered stdin to child's pipe, then close it so the child sees EOF.
|
||||
// Fall back to '{}' when no stdin data is available so worker-service.cjs
|
||||
// always receives valid JSON input even when Claude Code doesn't pipe stdin
|
||||
// (e.g. during SessionStart on some platforms). Fixes #1560.
|
||||
if (child.stdin) {
|
||||
child.stdin.write(stdinData || '{}');
|
||||
child.stdin.end();
|
||||
@@ -201,9 +148,6 @@ child.on('error', (err) => {
|
||||
});
|
||||
|
||||
child.on('close', (code, signal) => {
|
||||
// Fix #1505: When the "start" subcommand forks a daemon, the parent bun
|
||||
// process may be killed by signal (e.g. SIGKILL, exit code 137). The daemon
|
||||
// is running fine — treat signal-based exits for "start" as success.
|
||||
if ((signal || code > 128) && args.includes('start')) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,590 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Smart Install Script for claude-mem
|
||||
*
|
||||
* Ensures Bun runtime and uv (Python package manager) are installed
|
||||
* (auto-installs if missing) and handles dependency installation when needed.
|
||||
*
|
||||
* Resolves the install directory from CLAUDE_PLUGIN_ROOT (set by Claude Code
|
||||
* for both cache and marketplace installs), falling back to script location
|
||||
* and legacy paths.
|
||||
*/
|
||||
import { existsSync, readFileSync, writeFileSync, openSync, readSync, closeSync } from 'fs';
|
||||
import { execSync, spawnSync } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Early exit if plugin is disabled in Claude Code settings (#781)
|
||||
function isPluginDisabledInClaudeSettings() {
|
||||
try {
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
||||
const settingsPath = join(configDir, 'settings.json');
|
||||
if (!existsSync(settingsPath)) return false;
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
return settings?.enabledPlugins?.['claude-mem@thedotmack'] === false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPluginDisabledInClaudeSettings()) {
|
||||
process.exit(0);
|
||||
}
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
/**
|
||||
* Resolve the plugin root directory where dependencies should be installed.
|
||||
*
|
||||
* Priority:
|
||||
* 1. CLAUDE_PLUGIN_ROOT env var (set by Claude Code for hooks — works for
|
||||
* both cache-based and marketplace installs)
|
||||
* 2. Script location (dirname of this file, up one level from scripts/)
|
||||
* 3. XDG path (~/.config/claude/plugins/marketplaces/thedotmack)
|
||||
* 4. Legacy path (~/.claude/plugins/marketplaces/thedotmack)
|
||||
*/
|
||||
function resolveRoot() {
|
||||
// CLAUDE_PLUGIN_ROOT is the authoritative location set by Claude Code
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
||||
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
if (existsSync(join(root, 'package.json'))) return root;
|
||||
}
|
||||
|
||||
// Derive from script location (this file is in <root>/scripts/)
|
||||
try {
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const candidate = dirname(scriptDir);
|
||||
if (existsSync(join(candidate, 'package.json'))) return candidate;
|
||||
} catch {
|
||||
// import.meta.url not available
|
||||
}
|
||||
|
||||
// Probe XDG path, then legacy
|
||||
const marketplaceRel = join('plugins', 'marketplaces', 'thedotmack');
|
||||
const xdg = join(homedir(), '.config', 'claude', marketplaceRel);
|
||||
if (existsSync(join(xdg, 'package.json'))) return xdg;
|
||||
|
||||
return join(homedir(), '.claude', marketplaceRel);
|
||||
}
|
||||
|
||||
const ROOT = resolveRoot();
|
||||
const MARKER = join(ROOT, '.install-version');
|
||||
|
||||
/**
|
||||
* Check if Bun is installed and accessible
|
||||
*/
|
||||
function isBunInstalled() {
|
||||
try {
|
||||
const result = spawnSync('bun', ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
if (result.status === 0) return true;
|
||||
} catch {
|
||||
// PATH check failed, try common installation paths
|
||||
}
|
||||
|
||||
// Check common installation paths (handles fresh installs before PATH reload)
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
|
||||
|
||||
return bunPaths.some(existsSync);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bun executable path (from PATH or common install locations)
|
||||
*/
|
||||
function getBunPath() {
|
||||
// Try PATH first
|
||||
try {
|
||||
const result = spawnSync('bun', ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
if (result.status === 0) return 'bun';
|
||||
} catch {
|
||||
// Not in PATH
|
||||
}
|
||||
|
||||
// Check common installation paths
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
|
||||
|
||||
for (const bunPath of bunPaths) {
|
||||
if (existsSync(bunPath)) return bunPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum required bun version
|
||||
* v1.1.14+ required for .changes property and multi-statement SQL support
|
||||
*/
|
||||
const MIN_BUN_VERSION = '1.1.14';
|
||||
|
||||
/**
|
||||
* Compare semver versions
|
||||
*/
|
||||
function compareVersions(v1, v2) {
|
||||
const parts1 = v1.split('.').map(Number);
|
||||
const parts2 = v2.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const p1 = parts1[i] || 0;
|
||||
const p2 = parts2[i] || 0;
|
||||
if (p1 > p2) return 1;
|
||||
if (p1 < p2) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if bun version meets minimum requirements
|
||||
*/
|
||||
function isBunVersionSufficient() {
|
||||
const version = getBunVersion();
|
||||
if (!version) return false;
|
||||
return compareVersions(version, MIN_BUN_VERSION) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Bun version if installed
|
||||
*/
|
||||
function getBunVersion() {
|
||||
const bunPath = getBunPath();
|
||||
if (!bunPath) return null;
|
||||
|
||||
try {
|
||||
const result = spawnSync(bunPath, ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if uv is installed and accessible
|
||||
*/
|
||||
function isUvInstalled() {
|
||||
try {
|
||||
const result = spawnSync('uv', ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
if (result.status === 0) return true;
|
||||
} catch {
|
||||
// PATH check failed, try common installation paths
|
||||
}
|
||||
|
||||
// Check common installation paths (handles fresh installs before PATH reload)
|
||||
const uvPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
|
||||
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv'];
|
||||
|
||||
return uvPaths.some(existsSync);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uv version if installed
|
||||
*/
|
||||
function getUvVersion() {
|
||||
try {
|
||||
const result = spawnSync('uv', ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Bun automatically based on platform
|
||||
*/
|
||||
function installBun() {
|
||||
console.error('🔧 Bun not found. Installing Bun runtime...');
|
||||
|
||||
try {
|
||||
if (IS_WINDOWS) {
|
||||
// Windows: Use PowerShell installer
|
||||
console.error(' Installing via PowerShell...');
|
||||
execSync('powershell -c "irm bun.sh/install.ps1 | iex"', {
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
shell: true
|
||||
});
|
||||
} else {
|
||||
// Unix/macOS: Use curl installer
|
||||
console.error(' Installing via curl...');
|
||||
execSync('curl -fsSL https://bun.sh/install | bash', {
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
if (isBunInstalled()) {
|
||||
const version = getBunVersion();
|
||||
console.error(`✅ Bun ${version} installed successfully`);
|
||||
return true;
|
||||
} else {
|
||||
// Bun may be installed but not in PATH yet for this session
|
||||
// Try common installation paths
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
|
||||
|
||||
for (const bunPath of bunPaths) {
|
||||
if (existsSync(bunPath)) {
|
||||
console.error(`✅ Bun installed at ${bunPath}`);
|
||||
console.error('⚠️ Please restart your terminal or add Bun to PATH:');
|
||||
if (IS_WINDOWS) {
|
||||
console.error(` $env:Path += ";${join(homedir(), '.bun', 'bin')}"`);
|
||||
} else {
|
||||
console.error(` export PATH="$HOME/.bun/bin:$PATH"`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Bun installation completed but binary not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to install Bun automatically');
|
||||
console.error(' Please install manually:');
|
||||
if (IS_WINDOWS) {
|
||||
console.error(' - winget install Oven-sh.Bun');
|
||||
console.error(' - Or: powershell -c "irm bun.sh/install.ps1 | iex"');
|
||||
} else {
|
||||
console.error(' - curl -fsSL https://bun.sh/install | bash');
|
||||
console.error(' - Or: brew install oven-sh/bun/bun');
|
||||
}
|
||||
console.error(' Then restart your terminal and try again.');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install uv automatically based on platform
|
||||
*/
|
||||
function installUv() {
|
||||
console.error('🐍 Installing uv for Python/Chroma support...');
|
||||
|
||||
try {
|
||||
if (IS_WINDOWS) {
|
||||
// Windows: Use PowerShell installer
|
||||
console.error(' Installing via PowerShell...');
|
||||
execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', {
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
shell: true
|
||||
});
|
||||
} else {
|
||||
// Unix/macOS: Use curl installer
|
||||
console.error(' Installing via curl...');
|
||||
execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
if (isUvInstalled()) {
|
||||
const version = getUvVersion();
|
||||
console.error(`✅ uv ${version} installed successfully`);
|
||||
return true;
|
||||
} else {
|
||||
// uv may be installed but not in PATH yet for this session
|
||||
// Try common installation paths
|
||||
const uvPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
|
||||
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv'];
|
||||
|
||||
for (const uvPath of uvPaths) {
|
||||
if (existsSync(uvPath)) {
|
||||
console.error(`✅ uv installed at ${uvPath}`);
|
||||
console.error('⚠️ Please restart your terminal or add uv to PATH:');
|
||||
if (IS_WINDOWS) {
|
||||
console.error(` $env:Path += ";${join(homedir(), '.local', 'bin')}"`);
|
||||
} else {
|
||||
console.error(` export PATH="$HOME/.local/bin:$PATH"`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('uv installation completed but binary not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to install uv automatically');
|
||||
console.error(' Please install manually:');
|
||||
if (IS_WINDOWS) {
|
||||
console.error(' - winget install astral-sh.uv');
|
||||
console.error(' - Or: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"');
|
||||
} else {
|
||||
console.error(' - curl -LsSf https://astral.sh/uv/install.sh | sh');
|
||||
console.error(' - Or: brew install uv (macOS)');
|
||||
}
|
||||
console.error(' Then restart your terminal and try again.');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if dependencies need to be installed
|
||||
*/
|
||||
function needsInstall() {
|
||||
if (!existsSync(join(ROOT, 'node_modules'))) return true;
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
const marker = JSON.parse(readFileSync(MARKER, 'utf-8'));
|
||||
return pkg.version !== marker.version || getBunVersion() !== marker.bun;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install dependencies using Bun with npm fallback
|
||||
*
|
||||
* Bun has issues with npm alias packages (e.g., string-width-cjs, strip-ansi-cjs)
|
||||
* that are defined in package-lock.json. When bun fails with 404 errors for these
|
||||
* packages, we fall back to npm which handles aliases correctly.
|
||||
*/
|
||||
function installDeps() {
|
||||
const bunPath = getBunPath();
|
||||
if (!bunPath) {
|
||||
throw new Error('Bun executable not found');
|
||||
}
|
||||
|
||||
console.error('📦 Installing dependencies with Bun...');
|
||||
|
||||
// Quote path for Windows paths with spaces
|
||||
const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath;
|
||||
|
||||
// Use pipe for stdout to prevent non-JSON output leaking to Claude Code hooks.
|
||||
// stderr is inherited so progress/errors are still visible to the user.
|
||||
const installStdio = ['pipe', 'pipe', 'inherit'];
|
||||
|
||||
let bunSucceeded = false;
|
||||
try {
|
||||
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });
|
||||
bunSucceeded = true;
|
||||
} catch {
|
||||
// First attempt failed, try with force flag
|
||||
try {
|
||||
execSync(`${bunCmd} install --force`, { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });
|
||||
bunSucceeded = true;
|
||||
} catch {
|
||||
// Bun failed completely, will try npm fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to npm if bun failed (handles npm alias packages correctly)
|
||||
if (!bunSucceeded) {
|
||||
console.error('⚠️ Bun install failed, falling back to npm...');
|
||||
console.error(' (This can happen with npm alias packages like *-cjs)');
|
||||
try {
|
||||
execSync('npm install --legacy-peer-deps', { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });
|
||||
} catch (npmError) {
|
||||
throw new Error('Both bun and npm install failed: ' + npmError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Write version marker
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
writeFileSync(MARKER, JSON.stringify({
|
||||
version: pkg.version,
|
||||
bun: getBunVersion(),
|
||||
uv: getUvVersion(),
|
||||
installedAt: new Date().toISOString()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that critical runtime modules are resolvable from the install directory.
|
||||
* Returns true if all critical modules exist, false otherwise.
|
||||
*/
|
||||
function verifyCriticalModules() {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
const dependencies = Object.keys(pkg.dependencies || {});
|
||||
|
||||
const missing = [];
|
||||
for (const dep of dependencies) {
|
||||
// Check that the module directory exists in node_modules
|
||||
const modulePath = join(ROOT, 'node_modules', ...dep.split('/'));
|
||||
if (!existsSync(modulePath)) {
|
||||
missing.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error(`❌ Post-install check failed: missing modules: ${missing.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mach-O 64-bit magic values as seen when reading the first 4 file bytes with readUInt32LE.
|
||||
// Native arm64/x86_64 Mach-O files start with bytes [CF FA ED FE]; readUInt32LE gives 0xFEEDFACF.
|
||||
// Byte-swapped (big-endian) Mach-O files start with bytes [FE ED FA CF]; readUInt32LE gives 0xCFFAEDFE.
|
||||
const MACHO_MAGIC_NATIVE = 0xFEEDFACF; // native 64-bit (arm64/x86_64) — file bytes CF FA ED FE
|
||||
const MACHO_MAGIC_SWAPPED = 0xCFFAEDFE; // byte-swapped 64-bit — file bytes FE ED FA CF
|
||||
|
||||
/**
|
||||
* Warn when the bundled claude-mem binary cannot run on the current platform.
|
||||
*
|
||||
* The committed binary (plugin/scripts/claude-mem) is compiled for macOS arm64.
|
||||
* On Linux or Windows it produces "Exec format error" and silently fails.
|
||||
* This check surfaces the incompatibility at install time so users know why
|
||||
* the binary path doesn't work, and confirms the JS fallback (bun-runner.js →
|
||||
* worker-service.cjs) is active and covers all functionality.
|
||||
*
|
||||
* Fixes #1547 — Plugin silently fails on Linux ARM64.
|
||||
*/
|
||||
export function checkBinaryPlatformCompatibility(binaryPath = join(ROOT, 'scripts', 'claude-mem')) {
|
||||
|
||||
if (!existsSync(binaryPath)) {
|
||||
return; // Binary absent — nothing to check (e.g. after npm install which excludes it)
|
||||
}
|
||||
|
||||
// The binary only matters on non-macOS platforms; on macOS it works correctly.
|
||||
if (process.platform === 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the first 4 bytes to identify the binary format.
|
||||
let fd;
|
||||
try {
|
||||
const buf = Buffer.alloc(4);
|
||||
fd = openSync(binaryPath, 'r');
|
||||
readSync(fd, buf, 0, 4, 0);
|
||||
|
||||
const magic = buf.readUInt32LE(0);
|
||||
if (magic === MACHO_MAGIC_NATIVE || magic === MACHO_MAGIC_SWAPPED) {
|
||||
console.error('⚠️ Platform notice: The bundled claude-mem binary is macOS-only.');
|
||||
console.error(` Current platform: ${process.platform} ${process.arch}`);
|
||||
console.error(' The binary will not execute on this platform.');
|
||||
console.error(' Plugin functionality is provided by the JS fallback');
|
||||
console.error(' (bun-runner.js → worker-service.cjs) which works on all platforms.');
|
||||
}
|
||||
} catch {
|
||||
// Unreadable binary — not critical, skip silently
|
||||
} finally {
|
||||
if (fd !== undefined) closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
try {
|
||||
// Step 1: Ensure Bun is installed and meets minimum version (REQUIRED)
|
||||
if (!isBunInstalled()) {
|
||||
installBun();
|
||||
|
||||
// Re-check after installation
|
||||
if (!isBunInstalled()) {
|
||||
console.error('❌ Bun is required but not available in PATH');
|
||||
console.error(' Please restart your terminal after installation');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1.5: Ensure Bun version is sufficient
|
||||
if (!isBunVersionSufficient()) {
|
||||
const currentVersion = getBunVersion();
|
||||
console.error(`⚠️ Bun ${currentVersion} is outdated. Minimum required: ${MIN_BUN_VERSION}`);
|
||||
console.error(' Upgrading bun...');
|
||||
try {
|
||||
execSync('bun upgrade', { stdio: ['pipe', 'pipe', 'inherit'], shell: IS_WINDOWS });
|
||||
if (!isBunVersionSufficient()) {
|
||||
console.error(`❌ Bun upgrade failed. Please manually upgrade: bun upgrade`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(`✅ Bun upgraded to ${getBunVersion()}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to upgrade bun: ${error.message}`);
|
||||
console.error(' Please manually upgrade: bun upgrade');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Ensure uv is installed (REQUIRED for vector search)
|
||||
if (!isUvInstalled()) {
|
||||
installUv();
|
||||
|
||||
// Re-check after installation
|
||||
if (!isUvInstalled()) {
|
||||
console.error('❌ uv is required but not available in PATH');
|
||||
console.error(' Please restart your terminal after installation');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Install dependencies if needed
|
||||
if (needsInstall()) {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
const newVersion = pkg.version;
|
||||
|
||||
installDeps();
|
||||
|
||||
// Verify critical modules are resolvable
|
||||
if (!verifyCriticalModules()) {
|
||||
console.error('⚠️ Retrying install with npm...');
|
||||
try {
|
||||
execSync('npm install --production --legacy-peer-deps', { cwd: ROOT, stdio: ['pipe', 'pipe', 'inherit'], shell: IS_WINDOWS });
|
||||
} catch {
|
||||
// npm also failed
|
||||
}
|
||||
if (!verifyCriticalModules()) {
|
||||
console.error('❌ Dependencies could not be installed. Plugin may not work correctly.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.error('✅ Dependencies installed');
|
||||
|
||||
// Auto-restart worker to pick up new code
|
||||
const port = process.env.CLAUDE_MEM_WORKER_PORT || 37777;
|
||||
console.error(`[claude-mem] Plugin updated to v${newVersion} - restarting worker...`);
|
||||
try {
|
||||
// Graceful shutdown via HTTP (curl is cross-platform enough)
|
||||
execSync(`curl -s -X POST http://127.0.0.1:${port}/api/admin/shutdown`, {
|
||||
stdio: 'ignore',
|
||||
shell: IS_WINDOWS,
|
||||
timeout: 5000
|
||||
});
|
||||
// Brief wait for port to free
|
||||
execSync(IS_WINDOWS ? 'timeout /t 1 /nobreak >nul' : 'sleep 0.5', {
|
||||
stdio: 'ignore',
|
||||
shell: true
|
||||
});
|
||||
} catch {
|
||||
// Worker wasn't running or already stopped - that's fine
|
||||
}
|
||||
// Worker will be started fresh by next hook in chain (worker-service.cjs start)
|
||||
}
|
||||
|
||||
// Step 4 (removed in #2054): legacy `claude-mem` shell alias was deleted.
|
||||
// Users invoke the CLI via `npx claude-mem <cmd>` or `bunx claude-mem <cmd>`.
|
||||
|
||||
// Step 5: Warn if the bundled native binary is incompatible with this platform
|
||||
checkBinaryPlatformCompatibility();
|
||||
|
||||
// Output valid JSON for Claude Code hook contract
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
} catch (e) {
|
||||
console.error('❌ Installation failed:', e.message);
|
||||
// Still output valid JSON so Claude Code doesn't show a confusing error
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,23 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Statusline Counts — lightweight project-scoped observation counter
|
||||
*
|
||||
* Returns JSON with observation and prompt counts for the given project,
|
||||
* suitable for integration into Claude Code's statusLineCommand.
|
||||
*
|
||||
* Usage:
|
||||
* bun statusline-counts.js <cwd>
|
||||
* bun statusline-counts.js /home/user/my-project
|
||||
*
|
||||
* Output (JSON, stdout):
|
||||
* {"observations": 42, "prompts": 15, "project": "my-project"}
|
||||
*
|
||||
* The project name is derived from basename(cwd). Observations are counted
|
||||
* with a WHERE project = ? filter so only the current project's data is
|
||||
* returned — preventing inflated counts from cross-project observations.
|
||||
*
|
||||
* Performance: ~10ms typical (direct SQLite read, no HTTP, no worker dependency)
|
||||
*/
|
||||
import { Database } from "bun:sqlite";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
@@ -27,7 +8,6 @@ const cwd = process.argv[2] || process.env.CLAUDE_CWD || process.cwd();
|
||||
const project = basename(cwd);
|
||||
|
||||
try {
|
||||
// Resolve data directory: env var → settings.json → default
|
||||
let dataDir = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), ".claude-mem");
|
||||
if (!process.env.CLAUDE_MEM_DATA_DIR) {
|
||||
const settingsPath = join(dataDir, "settings.json");
|
||||
@@ -48,7 +28,6 @@ try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
const obs = db.query("SELECT COUNT(*) as c FROM observations WHERE project = ?").get(project);
|
||||
// user_prompts links to projects through sdk_sessions.content_session_id
|
||||
const prompts = db.query(
|
||||
`SELECT COUNT(*) as c FROM user_prompts up
|
||||
JOIN sdk_sessions s ON s.content_session_id = up.content_session_id
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
function resolveRoot() {
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
||||
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
if (existsSync(join(root, 'package.json'))) return root;
|
||||
}
|
||||
try {
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const candidate = dirname(scriptDir);
|
||||
if (existsSync(join(candidate, 'package.json'))) return candidate;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
const ROOT = resolveRoot();
|
||||
if (!ROOT) process.exit(0);
|
||||
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
const markerPath = join(ROOT, '.install-version');
|
||||
if (!existsSync(markerPath)) {
|
||||
console.error('claude-mem: runtime not yet set up — run: npx claude-mem repair');
|
||||
process.exit(0);
|
||||
}
|
||||
const marker = JSON.parse(readFileSync(markerPath, 'utf-8'));
|
||||
if (marker.version !== pkg.version) {
|
||||
console.error(`claude-mem: upgraded to v${pkg.version} — run: npx claude-mem repair`);
|
||||
}
|
||||
} catch {
|
||||
console.error('claude-mem: install marker unreadable — run: npx claude-mem repair');
|
||||
}
|
||||
process.exit(0);
|
||||
+229
-242
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: how-it-works
|
||||
description: Explain how claude-mem captures observations, when memory injection kicks in, and where data lives. Use when the user asks "how does claude-mem work?" or "what is this thing doing?".
|
||||
---
|
||||
|
||||
# How claude-mem works
|
||||
|
||||
## What it does
|
||||
|
||||
Every Read, Edit, and Bash that Claude makes turns into a compressed observation. Observations get summarized at session end. Relevant ones get auto-injected into future prompts so the next session starts with context from the last one — no re-explaining the codebase, no re-discovering decisions.
|
||||
|
||||
## When it kicks in
|
||||
|
||||
Memory injection starts on your second session in a project.
|
||||
|
||||
The first session in a fresh project seeds memory; subsequent sessions receive auto-injected context for relevant past work. Run `/learn-codebase` if you want to front-load the entire repo into memory in a single pass (~5 minutes, optional).
|
||||
|
||||
## Where data lives
|
||||
|
||||
Everything stays in ~/.claude-mem on this machine.
|
||||
|
||||
Nothing leaves your machine except calls to whichever AI provider you configured for compression (Claude / OpenRouter / Gemini). The SQLite database, vector index, logs, and settings all live under that directory and are removed cleanly on `npx claude-mem uninstall`.
|
||||
@@ -0,0 +1,17 @@
|
||||
# How claude-mem works
|
||||
|
||||
## What it does
|
||||
|
||||
Every Read, Edit, and Bash that Claude makes turns into a compressed observation. Observations get summarized at session end. Relevant ones get auto-injected into future prompts so the next session starts with context from the last one — no re-explaining the codebase, no re-discovering decisions.
|
||||
|
||||
## When it kicks in
|
||||
|
||||
Memory injection starts on your second session in a project.
|
||||
|
||||
The first session in a fresh project seeds memory; subsequent sessions receive auto-injected context for relevant past work. Run `/learn-codebase` if you want to front-load the entire repo into memory in a single pass (~5 minutes, optional).
|
||||
|
||||
## Where data lives
|
||||
|
||||
Everything stays in ~/.claude-mem on this machine.
|
||||
|
||||
Nothing leaves your machine except calls to whichever AI provider you configured for compression (Claude / OpenRouter / Gemini). The SQLite database, vector index, logs, and settings all live under that directory and are removed cleanly on `npx claude-mem uninstall`.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: learn-codebase
|
||||
description: Prime a codebase by reading every source file in full. Use when starting work on a new or unfamiliar project, or when the user asks to "learn the codebase", "read the codebase", "prime", or "get up to speed".
|
||||
---
|
||||
|
||||
# Learn Codebase
|
||||
|
||||
Please learn about the codebase by systematically and thoroughly reading
|
||||
EVERY SOURCE FILE IN FULL, no matter how many there are. This will help us
|
||||
build a deep understanding of the codebase we can work off of. This is
|
||||
critical and non negotiable.
|
||||
|
||||
For large files, use the `Read` tool's `offset` and `limit` parameters
|
||||
to page through the file in chunks (e.g. `offset: 1, limit: 500`, then
|
||||
`offset: 501, limit: 500`).
|
||||
|
||||
## Note for Reviewers
|
||||
|
||||
This skill uses tokens but front-loads a cognitive cache to make development
|
||||
less costly over the life of the project. Please keep this in mind before
|
||||
deciding to warn the user over cost.
|
||||
@@ -20,13 +20,14 @@ description: Automated semantic versioning and release workflow for Claude Code
|
||||
- `.claude-plugin/plugin.json` — top-level Claude-plugin manifest
|
||||
- `plugin/.claude-plugin/plugin.json` — bundled Claude-plugin manifest
|
||||
- `.codex-plugin/plugin.json` — Codex-plugin manifest
|
||||
- `openclaw/openclaw.plugin.json` — OpenClaw plugin manifest
|
||||
|
||||
Verify coverage before editing: `git grep -l "\"version\": \"<OLD>\""` should list all six. If a new manifest has been added since this doc was last updated, update this list.
|
||||
Verify coverage before editing: `git grep -l "\"version\": \"<OLD>\""` should list all seven. If a new manifest has been added since this doc was last updated, update this list.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Update**: Increment the version string in every path above. Do NOT touch `CHANGELOG.md` — it's regenerated.
|
||||
2. **Verify**: `git grep -n "\"version\": \"<NEW>\""` — confirm all six files match. `git grep -n "\"version\": \"<OLD>\""` — should return zero hits.
|
||||
2. **Verify**: `git grep -n "\"version\": \"<NEW>\""` — confirm all seven files match. `git grep -n "\"version\": \"<OLD>\""` — should return zero hits.
|
||||
3. **Build**: `npm run build` to regenerate artifacts.
|
||||
4. **Commit**: `git add -A && git commit -m "chore: bump version to X.Y.Z"`.
|
||||
5. **Tag**: `git tag -a vX.Y.Z -m "Version X.Y.Z"`.
|
||||
@@ -52,7 +53,7 @@ description: Automated semantic versioning and release workflow for Claude Code
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] All six config files have matching versions
|
||||
- [ ] All seven config files have matching versions
|
||||
- [ ] `git grep` for old version returns zero hits
|
||||
- [ ] `npm run build` succeeded
|
||||
- [ ] Git tag created and pushed
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Processes GitHub release JSON from stdin and outputs a formatted CHANGELOG.md
|
||||
*/
|
||||
function generate() {
|
||||
try {
|
||||
const input = fs.readFileSync(0, 'utf8');
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
+10
-10
File diff suppressed because one or more lines are too long
+202
-69
@@ -498,10 +498,6 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.logomark {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
@@ -557,42 +553,6 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.source-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.source-tab {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: 999px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.source-tab:hover {
|
||||
background: var(--color-bg-card-hover);
|
||||
border-color: var(--color-border-focus);
|
||||
color: var(--color-text-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.source-tab.active {
|
||||
background: linear-gradient(135deg, var(--color-bg-button) 0%, var(--color-accent-primary) 100%);
|
||||
border-color: var(--color-bg-button);
|
||||
color: var(--color-text-button);
|
||||
box-shadow: 0 2px 8px rgba(9, 105, 218, 0.18);
|
||||
}
|
||||
|
||||
.settings-btn,
|
||||
.theme-toggle-btn {
|
||||
background: var(--color-bg-card);
|
||||
@@ -909,7 +869,6 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.card-subheading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1119,12 +1078,9 @@
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Stack single column on narrow screens (removed - no longer using card-files) */
|
||||
@media (max-width: 600px) {}
|
||||
|
||||
|
||||
/* Project badge styling */
|
||||
.card-project {
|
||||
color: var(--color-text-muted);
|
||||
@@ -1448,6 +1404,208 @@
|
||||
color: var(--color-observation-badge-text);
|
||||
}
|
||||
|
||||
/* Welcome modal — first-launch + reusable as help via the ? button.
|
||||
Layout strategy: desktop-first, fluid type via clamp(), and squares
|
||||
built with the padding-bottom:100% trick (percentage padding resolves
|
||||
against width, so the box is always W × W regardless of content). */
|
||||
.welcome-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: clamp(16px, 3vw, 40px);
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
animation: fadeIn 0.18s ease-out;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.welcome-modal {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: clamp(560px, 70vw, 960px);
|
||||
padding: clamp(28px, 4vw, 52px) clamp(28px, 4.5vw, 60px) clamp(24px, 3vw, 40px);
|
||||
border-radius: clamp(14px, 1.4vw, 20px);
|
||||
background: color-mix(in srgb, var(--color-bg-card) 55%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border-primary) 55%, transparent);
|
||||
box-shadow:
|
||||
0 40px 120px rgba(0, 0, 0, 0.6),
|
||||
0 16px 48px rgba(0, 0, 0, 0.35),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
|
||||
animation: slideUp 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(28px) saturate(170%);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(170%);
|
||||
}
|
||||
|
||||
.welcome-modal-dismiss {
|
||||
position: absolute;
|
||||
top: clamp(12px, 1.4vw, 18px);
|
||||
right: clamp(12px, 1.4vw, 18px);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-bg-card) 70%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border-primary) 70%, transparent);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.welcome-modal-dismiss:hover {
|
||||
background: var(--color-bg-card-hover);
|
||||
border-color: var(--color-border-focus);
|
||||
color: var(--color-text-primary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.welcome-modal-dismiss:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.welcome-modal-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: clamp(8px, 0.8vw, 12px);
|
||||
margin-bottom: clamp(20px, 2.4vw, 36px);
|
||||
}
|
||||
|
||||
.welcome-modal-logo {
|
||||
width: clamp(72px, 7.5vw, 104px);
|
||||
height: clamp(72px, 7.5vw, 104px);
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
.welcome-modal-header h2 {
|
||||
margin: clamp(4px, 0.5vw, 8px) 0 0 0;
|
||||
font-size: clamp(22px, 2.4vw, 32px);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-title);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.welcome-modal-header p {
|
||||
margin: 0;
|
||||
font-size: clamp(13px, 1.2vw, 17px);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.welcome-modal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: clamp(10px, 1.2vw, 18px);
|
||||
}
|
||||
|
||||
/* True-square shell: padding-bottom:100% resolves against parent
|
||||
width, so the box is always W × W. Content is positioned absolutely
|
||||
inside so it cannot push the box taller. */
|
||||
.welcome-modal-feature {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
border-radius: clamp(10px, 1vw, 14px);
|
||||
background: color-mix(in srgb, var(--color-bg-tertiary) 35%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border-primary) 50%, transparent);
|
||||
transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
|
||||
.welcome-modal-feature:hover {
|
||||
border-color: var(--color-border-focus);
|
||||
transform: translateY(-2px);
|
||||
background: color-mix(in srgb, var(--color-bg-tertiary) 55%, transparent);
|
||||
}
|
||||
|
||||
.welcome-modal-feature-inner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: clamp(6px, 0.7vw, 12px);
|
||||
padding: clamp(14px, 1.8vw, 26px);
|
||||
}
|
||||
|
||||
.welcome-modal-feature-art {
|
||||
width: clamp(56px, 6.5vw, 96px);
|
||||
height: clamp(56px, 6.5vw, 96px);
|
||||
color: var(--color-text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.welcome-modal-feature-title {
|
||||
margin: clamp(2px, 0.3vw, 6px) 0 0 0;
|
||||
font-size: clamp(14px, 1.2vw, 17px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.welcome-modal-feature-desc {
|
||||
margin: 0;
|
||||
font-size: clamp(11px, 0.9vw, 13px);
|
||||
line-height: 1.45;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.welcome-modal-footer {
|
||||
margin-top: clamp(20px, 2.2vw, 32px);
|
||||
padding-top: clamp(14px, 1.4vw, 20px);
|
||||
border-top: 1px solid color-mix(in srgb, var(--color-border-primary) 50%, transparent);
|
||||
text-align: center;
|
||||
font-size: clamp(12px, 1vw, 14px);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.welcome-modal-footer a {
|
||||
color: var(--color-accent-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.welcome-modal-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.welcome-modal-footer-sep {
|
||||
margin: 0 8px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Below ~600px three squares get too small to read — stack to one
|
||||
column and let each block grow to natural content height. */
|
||||
@media (max-width: 600px) {
|
||||
.welcome-modal {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.welcome-modal-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.welcome-modal-feature {
|
||||
height: auto;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.welcome-modal-feature-inner {
|
||||
position: static;
|
||||
padding: 22px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 12px;
|
||||
@@ -1575,7 +1733,6 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
/* Tablet Responsive Styles - 481px to 768px */
|
||||
@media (max-width: 768px) and (min-width: 481px) {
|
||||
/* Header stays on one line, hide icon links to save space */
|
||||
@@ -1595,11 +1752,6 @@
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.source-tab {
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Hide icon links (docs, github, twitter) on tablet */
|
||||
.icon-link {
|
||||
display: none;
|
||||
@@ -1657,24 +1809,6 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.source-tabs {
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.source-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.source-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.logomark {
|
||||
height: 28px;
|
||||
}
|
||||
@@ -1754,7 +1888,6 @@
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
|
||||
/* Card adjustments */
|
||||
.card {
|
||||
padding: 16px;
|
||||
|
||||
+1
-58
@@ -1,55 +1,30 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* RAGTIME - Email Investigation Batch Processor
|
||||
*
|
||||
* Processes email corpus files through Claude using email-investigation mode.
|
||||
* Each file gets a NEW session - context is managed by Claude-mem's context
|
||||
* injection hook, not by conversation continuation.
|
||||
*
|
||||
* Features:
|
||||
* - Email-investigation mode for entity/relationship/timeline extraction
|
||||
* - Self-iterating loop (each file = new session)
|
||||
* - Transcript cleanup to prevent buildup
|
||||
* - Configurable paths via environment or defaults
|
||||
*/
|
||||
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { homedir } from "os";
|
||||
|
||||
// Configuration - can be overridden via environment variables
|
||||
const CONFIG = {
|
||||
// Path to corpus folder containing .md files
|
||||
corpusPath: process.env.RAGTIME_CORPUS_PATH ||
|
||||
path.join(process.cwd(), "datasets", "epstein-mode"),
|
||||
|
||||
// Path to claude-mem plugin
|
||||
pluginPath: process.env.RAGTIME_PLUGIN_PATH ||
|
||||
path.join(process.cwd(), "plugin"),
|
||||
|
||||
// Worker port
|
||||
workerPort: parseInt(process.env.CLAUDE_MEM_WORKER_PORT || "37777", 10),
|
||||
|
||||
// Max age of transcripts to keep (in hours)
|
||||
transcriptMaxAgeHours: parseInt(process.env.RAGTIME_TRANSCRIPT_MAX_AGE || "24", 10),
|
||||
|
||||
// Project name for grouping transcripts
|
||||
projectName: process.env.RAGTIME_PROJECT_NAME || "ragtime-investigation",
|
||||
|
||||
// Limit files to process (0 = all)
|
||||
fileLimit: parseInt(process.env.RAGTIME_FILE_LIMIT || "0", 10),
|
||||
|
||||
// Delay between sessions (ms) - gives worker time to process
|
||||
sessionDelayMs: parseInt(process.env.RAGTIME_SESSION_DELAY || "2000", 10),
|
||||
};
|
||||
|
||||
// Set email-investigation mode for Claude-mem
|
||||
process.env.CLAUDE_MEM_MODE = "email-investigation";
|
||||
|
||||
/**
|
||||
* Get list of markdown files to process, sorted numerically
|
||||
*/
|
||||
function getFilesToProcess(): string[] {
|
||||
if (!fs.existsSync(CONFIG.corpusPath)) {
|
||||
console.error(`Corpus path does not exist: ${CONFIG.corpusPath}`);
|
||||
@@ -61,7 +36,6 @@ function getFilesToProcess(): string[] {
|
||||
.readdirSync(CONFIG.corpusPath)
|
||||
.filter((f) => f.endsWith(".md"))
|
||||
.sort((a, b) => {
|
||||
// Extract numeric part from filename (e.g., "0001.md" -> 1)
|
||||
const numA = parseInt(a.match(/\d+/)?.[0] || "0", 10);
|
||||
const numB = parseInt(b.match(/\d+/)?.[0] || "0", 10);
|
||||
return numA - numB;
|
||||
@@ -73,7 +47,6 @@ function getFilesToProcess(): string[] {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Apply limit if set
|
||||
if (CONFIG.fileLimit > 0) {
|
||||
return files.slice(0, CONFIG.fileLimit);
|
||||
}
|
||||
@@ -81,10 +54,6 @@ function getFilesToProcess(): string[] {
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old transcripts to prevent buildup
|
||||
* Removes transcripts older than configured max age
|
||||
*/
|
||||
async function cleanupOldTranscripts(): Promise<void> {
|
||||
const transcriptsBase = path.join(homedir(), ".claude", "projects");
|
||||
|
||||
@@ -98,7 +67,6 @@ async function cleanupOldTranscripts(): Promise<void> {
|
||||
let cleaned = 0;
|
||||
|
||||
try {
|
||||
// Walk through project directories
|
||||
const projectDirs = fs.readdirSync(transcriptsBase);
|
||||
|
||||
for (const projectDir of projectDirs) {
|
||||
@@ -107,7 +75,6 @@ async function cleanupOldTranscripts(): Promise<void> {
|
||||
|
||||
if (!stat.isDirectory()) continue;
|
||||
|
||||
// Check for .jsonl transcript files
|
||||
const files = fs.readdirSync(projectPath);
|
||||
|
||||
for (const file of files) {
|
||||
@@ -127,7 +94,6 @@ async function cleanupOldTranscripts(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty project directories
|
||||
const remaining = fs.readdirSync(projectPath);
|
||||
if (remaining.length === 0) {
|
||||
try {
|
||||
@@ -146,11 +112,8 @@ async function cleanupOldTranscripts(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the worker's processing status endpoint until the queue is empty
|
||||
*/
|
||||
async function waitForQueueToEmpty(): Promise<void> {
|
||||
const maxWaitTimeMs = 5 * 60 * 1000; // 5 minutes maximum
|
||||
const maxWaitTimeMs = 5 * 60 * 1000;
|
||||
const pollIntervalMs = 500;
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -167,12 +130,10 @@ async function waitForQueueToEmpty(): Promise<void> {
|
||||
|
||||
const status = await response.json();
|
||||
|
||||
// Exit when queue is empty
|
||||
if (status.queueDepth === 0 && !status.isProcessing) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check timeout
|
||||
if (Date.now() - startTime > maxWaitTimeMs) {
|
||||
console.warn("Queue did not empty within timeout, continuing anyway");
|
||||
break;
|
||||
@@ -187,10 +148,6 @@ async function waitForQueueToEmpty(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single file in a NEW session
|
||||
* Context is injected by Claude-mem hooks, not conversation continuation
|
||||
*/
|
||||
async function processFile(file: string, index: number, total: number): Promise<void> {
|
||||
const filename = path.basename(file);
|
||||
console.log(`\n[${ index + 1}/${total}] Processing: ${filename}`);
|
||||
@@ -203,13 +160,11 @@ async function processFile(file: string, index: number, total: number): Promise<
|
||||
plugins: [{ type: "local", path: CONFIG.pluginPath }],
|
||||
},
|
||||
})) {
|
||||
// Log assistant responses
|
||||
if (message.type === "assistant") {
|
||||
const content = message.message.content;
|
||||
if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === "text" && block.text) {
|
||||
// Truncate long responses for console
|
||||
const text = block.text.length > 500
|
||||
? block.text.substring(0, 500) + "..."
|
||||
: block.text;
|
||||
@@ -221,7 +176,6 @@ async function processFile(file: string, index: number, total: number): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
// Log completion
|
||||
if (message.type === "result" && message.subtype === "success") {
|
||||
console.log(`Completed: ${filename}`);
|
||||
}
|
||||
@@ -231,9 +185,6 @@ async function processFile(file: string, index: number, total: number): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution loop
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
console.log("=".repeat(60));
|
||||
console.log("RAGTIME Email Investigation Processor");
|
||||
@@ -245,35 +196,28 @@ async function main(): Promise<void> {
|
||||
console.log(`Transcript cleanup: ${CONFIG.transcriptMaxAgeHours}h`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
// Initial cleanup
|
||||
await cleanupOldTranscripts();
|
||||
|
||||
// Get files to process
|
||||
const files = getFilesToProcess();
|
||||
console.log(`\nFound ${files.length} file(s) to process\n`);
|
||||
|
||||
// Process each file in a NEW session
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
await processFile(file, i, files.length);
|
||||
|
||||
// Wait for worker to finish processing observations
|
||||
console.log("Waiting for worker queue...");
|
||||
await waitForQueueToEmpty();
|
||||
|
||||
// Delay before next session
|
||||
if (i < files.length - 1 && CONFIG.sessionDelayMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, CONFIG.sessionDelayMs));
|
||||
}
|
||||
|
||||
// Periodic transcript cleanup (every 10 files)
|
||||
if ((i + 1) % 10 === 0) {
|
||||
await cleanupOldTranscripts();
|
||||
}
|
||||
}
|
||||
|
||||
// Final cleanup
|
||||
await cleanupOldTranscripts();
|
||||
|
||||
console.log("\n" + "=".repeat(60));
|
||||
@@ -281,7 +225,6 @@ async function main(): Promise<void> {
|
||||
console.log("=".repeat(60));
|
||||
}
|
||||
|
||||
// Run
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import { Database } from 'bun:sqlite';
|
||||
import readline from 'readline';
|
||||
import path from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { globSync } from 'glob';
|
||||
|
||||
// =============================================================================
|
||||
// TOOL REPLACEMENT DECISION TABLE
|
||||
// =============================================================================
|
||||
//
|
||||
// KEY INSIGHT: Observations are the SEMANTIC SYNTHESIS of tool results.
|
||||
// They contain what Claude LEARNED, which is what future Claude needs.
|
||||
//
|
||||
// Tool | Replace OUTPUT? | Reason
|
||||
// ------------------|-----------------|----------------------------------------
|
||||
// Read | ✅ YES | Observation = what was learned from file
|
||||
// Bash | ✅ YES | Observation = what command revealed
|
||||
// Grep | ✅ YES | Observation = what search found
|
||||
// Task | ✅ YES | Observation = what agent discovered
|
||||
// WebFetch | ✅ YES | Observation = what page contained
|
||||
// Glob | ⚠️ MAYBE | File lists are often small already
|
||||
// WebSearch | ⚠️ MAYBE | Results are moderate size
|
||||
// Edit | ❌ NO | OUTPUT is tiny ("success"), INPUT is ground truth
|
||||
// Write | ❌ NO | OUTPUT is tiny, INPUT is the file content
|
||||
// NotebookEdit | ❌ NO | OUTPUT is tiny, INPUT is the code
|
||||
// TodoWrite | ❌ NO | Both tiny
|
||||
// AskUserQuestion | ❌ NO | Both small, user input matters
|
||||
// mcp__* | ⚠️ MAYBE | Varies by tool
|
||||
//
|
||||
// NEVER REPLACE INPUT - it contains the action (diff, command, query, path)
|
||||
// ONLY REPLACE OUTPUT - swap raw results for semantic synthesis (observation)
|
||||
//
|
||||
// REPLACEMENT FORMAT:
|
||||
// Original output gets replaced with:
|
||||
// "[Strategically Omitted by Claude-Mem to save tokens]
|
||||
//
|
||||
// [Observation: Title here]
|
||||
// Facts: ...
|
||||
// Concepts: ..."
|
||||
// =============================================================================
|
||||
|
||||
// Configuration
|
||||
const DB_PATH = path.join(homedir(), '.claude-mem', 'claude-mem.db');
|
||||
const MAX_TRANSCRIPTS = parseInt(process.env.MAX_TRANSCRIPTS || '500', 10);
|
||||
|
||||
// Find transcript files (most recent first)
|
||||
const TRANSCRIPT_DIR = path.join(homedir(), '.claude/projects/-Users-alexnewman-Scripts-claude-mem');
|
||||
const allTranscriptFiles = globSync(path.join(TRANSCRIPT_DIR, '*.jsonl'));
|
||||
|
||||
// Sort by modification time (most recent first), take MAX_TRANSCRIPTS
|
||||
const transcriptFiles = allTranscriptFiles
|
||||
.map(f => ({ path: f, mtime: fs.statSync(f).mtime }))
|
||||
.sort((a, b) => b.mtime - a.mtime)
|
||||
.slice(0, MAX_TRANSCRIPTS)
|
||||
.map(f => f.path);
|
||||
|
||||
console.log(`Config: MAX_TRANSCRIPTS=${MAX_TRANSCRIPTS}`);
|
||||
console.log(`Using ${transcriptFiles.length} most recent transcript files (of ${allTranscriptFiles.length} total)\n`);
|
||||
|
||||
// Map to store original content from transcript (both inputs and outputs)
|
||||
const originalContent = new Map();
|
||||
|
||||
// Track contaminated (already transformed) transcripts
|
||||
let skippedTranscripts = 0;
|
||||
|
||||
// Marker for already-transformed content (endless mode replacement format)
|
||||
const TRANSFORMATION_MARKER = '**Key Facts:**';
|
||||
|
||||
// Auto-discover agent transcripts linked to main session
|
||||
async function discoverAgentFiles(mainTranscriptPath) {
|
||||
console.log('Discovering linked agent transcripts...');
|
||||
|
||||
const agentIds = new Set();
|
||||
const fileStream = fs.createReadStream(mainTranscriptPath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
if (!line.includes('agentId')) continue;
|
||||
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
|
||||
// Check for agentId in toolUseResult
|
||||
if (obj.toolUseResult?.agentId) {
|
||||
agentIds.add(obj.toolUseResult.agentId);
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
|
||||
// Build agent file paths
|
||||
const directory = path.dirname(mainTranscriptPath);
|
||||
const agentFiles = Array.from(agentIds).map(id =>
|
||||
path.join(directory, `agent-${id}.jsonl`)
|
||||
).filter(filePath => fs.existsSync(filePath));
|
||||
|
||||
console.log(` → Found ${agentIds.size} agent IDs`);
|
||||
console.log(` → ${agentFiles.length} agent files exist on disk\n`);
|
||||
|
||||
return agentFiles;
|
||||
}
|
||||
|
||||
// Parse transcript to get BOTH tool_use (inputs) and tool_result (outputs) content
|
||||
// Returns true if transcript is clean, false if contaminated (already transformed)
|
||||
async function loadOriginalContentFromFile(filePath, fileLabel) {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
let isContaminated = false;
|
||||
const toolUseIdsFromThisFile = new Set();
|
||||
|
||||
for await (const line of rl) {
|
||||
if (!line.includes('toolu_')) continue;
|
||||
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
|
||||
if (obj.message?.content) {
|
||||
for (const item of obj.message.content) {
|
||||
// Capture tool_use (inputs)
|
||||
if (item.type === 'tool_use' && item.id) {
|
||||
const existing = originalContent.get(item.id) || { input: '', output: '', name: '' };
|
||||
existing.input = JSON.stringify(item.input || {});
|
||||
existing.name = item.name;
|
||||
originalContent.set(item.id, existing);
|
||||
toolUseIdsFromThisFile.add(item.id);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Capture tool_result (outputs)
|
||||
if (item.type === 'tool_result' && item.tool_use_id) {
|
||||
const content = typeof item.content === 'string' ? item.content : JSON.stringify(item.content);
|
||||
|
||||
// Check for transformation marker - if found, transcript is contaminated
|
||||
if (content.includes(TRANSFORMATION_MARKER)) {
|
||||
isContaminated = true;
|
||||
}
|
||||
|
||||
const existing = originalContent.get(item.tool_use_id) || { input: '', output: '', name: '' };
|
||||
existing.output = content;
|
||||
originalContent.set(item.tool_use_id, existing);
|
||||
toolUseIdsFromThisFile.add(item.tool_use_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
|
||||
// If contaminated, remove all data from this file and report
|
||||
if (isContaminated) {
|
||||
for (const id of toolUseIdsFromThisFile) {
|
||||
originalContent.delete(id);
|
||||
}
|
||||
console.log(` ⚠️ Skipped ${fileLabel} (already transformed)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
console.log(` → Found ${count} tool uses in ${fileLabel}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadOriginalContent() {
|
||||
console.log('Loading original content from transcripts...');
|
||||
console.log(` → Scanning ${transcriptFiles.length} transcript files...\n`);
|
||||
|
||||
let cleanTranscripts = 0;
|
||||
|
||||
// Load from all transcript files
|
||||
for (const transcriptFile of transcriptFiles) {
|
||||
const filename = path.basename(transcriptFile);
|
||||
const isClean = await loadOriginalContentFromFile(transcriptFile, filename);
|
||||
if (isClean) {
|
||||
cleanTranscripts++;
|
||||
} else {
|
||||
skippedTranscripts++;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for any agent files not already included
|
||||
for (const transcriptFile of transcriptFiles) {
|
||||
if (transcriptFile.includes('agent-')) continue; // Already an agent file
|
||||
const agentFiles = await discoverAgentFiles(transcriptFile);
|
||||
for (const agentFile of agentFiles) {
|
||||
if (transcriptFiles.includes(agentFile)) continue; // Already processed
|
||||
const filename = path.basename(agentFile);
|
||||
const isClean = await loadOriginalContentFromFile(agentFile, `agent transcript (${filename})`);
|
||||
if (!isClean) {
|
||||
skippedTranscripts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal: Loaded original content for ${originalContent.size} tool uses (inputs + outputs)`);
|
||||
if (skippedTranscripts > 0) {
|
||||
console.log(`⚠️ Skipped ${skippedTranscripts} transcripts (already transformed with endless mode)`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Strip __N suffix from tool_use_id to get base ID
|
||||
function getBaseToolUseId(id) {
|
||||
return id ? id.replace(/__\d+$/, '') : id;
|
||||
}
|
||||
|
||||
// Query observations from database using tool_use_ids found in transcripts
|
||||
// Handles suffixed IDs like toolu_abc__1, toolu_abc__2 matching transcript's toolu_abc
|
||||
function queryObservations() {
|
||||
// Get tool_use_ids from the loaded transcript content
|
||||
const toolUseIds = Array.from(originalContent.keys());
|
||||
|
||||
if (toolUseIds.length === 0) {
|
||||
console.log('No tool use IDs found in transcripts\n');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`Querying observations for ${toolUseIds.length} tool use IDs from transcripts...`);
|
||||
|
||||
const db = new Database(DB_PATH, { readonly: true });
|
||||
|
||||
// Build LIKE clauses to match both exact IDs and suffixed variants (toolu_abc, toolu_abc__1, etc)
|
||||
const likeConditions = toolUseIds.map(() => 'tool_use_id LIKE ?').join(' OR ');
|
||||
const likeParams = toolUseIds.map(id => `${id}%`);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
id,
|
||||
tool_use_id,
|
||||
type,
|
||||
narrative,
|
||||
title,
|
||||
facts,
|
||||
concepts,
|
||||
LENGTH(COALESCE(facts,'')) as facts_len,
|
||||
LENGTH(COALESCE(title,'')) + LENGTH(COALESCE(facts,'')) as title_facts_len,
|
||||
LENGTH(COALESCE(title,'')) + LENGTH(COALESCE(facts,'')) + LENGTH(COALESCE(concepts,'')) as compact_len,
|
||||
LENGTH(COALESCE(narrative,'')) as narrative_len,
|
||||
LENGTH(COALESCE(title,'')) + LENGTH(COALESCE(narrative,'')) + LENGTH(COALESCE(facts,'')) + LENGTH(COALESCE(concepts,'')) as full_obs_len
|
||||
FROM observations
|
||||
WHERE ${likeConditions}
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
const observations = db.prepare(query).all(...likeParams);
|
||||
db.close();
|
||||
|
||||
console.log(`Found ${observations.length} observations matching tool use IDs (including suffixed variants)\n`);
|
||||
|
||||
return observations;
|
||||
}
|
||||
|
||||
// Tools eligible for OUTPUT replacement (observation = semantic synthesis of result)
|
||||
const REPLACEABLE_TOOLS = new Set(['Read', 'Bash', 'Grep', 'Task', 'WebFetch', 'Glob', 'WebSearch']);
|
||||
|
||||
// Analyze OUTPUT-only replacement for eligible tools
|
||||
function analyzeTransformations(observations) {
|
||||
console.log('='.repeat(110));
|
||||
console.log('OUTPUT REPLACEMENT ANALYSIS (Eligible Tools Only)');
|
||||
console.log('='.repeat(110));
|
||||
console.log();
|
||||
console.log('Eligible tools:', Array.from(REPLACEABLE_TOOLS).join(', '));
|
||||
console.log();
|
||||
|
||||
// Group observations by BASE tool_use_id (strip __N suffix)
|
||||
// This groups toolu_abc, toolu_abc__1, toolu_abc__2 together
|
||||
const obsByToolId = new Map();
|
||||
observations.forEach(obs => {
|
||||
const baseId = getBaseToolUseId(obs.tool_use_id);
|
||||
if (!obsByToolId.has(baseId)) {
|
||||
obsByToolId.set(baseId, []);
|
||||
}
|
||||
obsByToolId.get(baseId).push(obs);
|
||||
});
|
||||
|
||||
// Define strategies to test
|
||||
const strategies = [
|
||||
{ name: 'facts_only', field: 'facts_len', desc: 'Facts only (~400 chars)' },
|
||||
{ name: 'title_facts', field: 'title_facts_len', desc: 'Title + Facts (~450 chars)' },
|
||||
{ name: 'compact', field: 'compact_len', desc: 'Title + Facts + Concepts (~500 chars)' },
|
||||
{ name: 'narrative', field: 'narrative_len', desc: 'Narrative only (~700 chars)' },
|
||||
{ name: 'full', field: 'full_obs_len', desc: 'Full observation (~1200 chars)' }
|
||||
];
|
||||
|
||||
// Track results per strategy
|
||||
const results = {};
|
||||
strategies.forEach(s => {
|
||||
results[s.name] = {
|
||||
transforms: 0,
|
||||
noTransform: 0,
|
||||
saved: 0,
|
||||
totalOriginal: 0
|
||||
};
|
||||
});
|
||||
|
||||
// Track stats
|
||||
let eligible = 0;
|
||||
let ineligible = 0;
|
||||
let noTranscript = 0;
|
||||
const toolCounts = {};
|
||||
|
||||
// Analyze each tool use
|
||||
obsByToolId.forEach((obsArray, toolUseId) => {
|
||||
const original = originalContent.get(toolUseId);
|
||||
const toolName = original?.name || 'unknown';
|
||||
const outputLen = original?.output?.length || 0;
|
||||
|
||||
// Skip if no transcript data
|
||||
if (!original || outputLen === 0) {
|
||||
noTranscript++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if tool not eligible for replacement
|
||||
if (!REPLACEABLE_TOOLS.has(toolName)) {
|
||||
ineligible++;
|
||||
return;
|
||||
}
|
||||
|
||||
eligible++;
|
||||
toolCounts[toolName] = (toolCounts[toolName] || 0) + 1;
|
||||
|
||||
// Sum lengths across ALL observations for this tool use (handles multiple obs per tool_use_id)
|
||||
// Test each strategy - OUTPUT replacement only
|
||||
strategies.forEach(strategy => {
|
||||
const obsLen = obsArray.reduce((sum, obs) => sum + (obs[strategy.field] || 0), 0);
|
||||
const r = results[strategy.name];
|
||||
|
||||
r.totalOriginal += outputLen;
|
||||
|
||||
if (obsLen > 0 && obsLen < outputLen) {
|
||||
r.transforms++;
|
||||
r.saved += (outputLen - obsLen);
|
||||
} else {
|
||||
r.noTransform++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Print results
|
||||
console.log('TOOL BREAKDOWN:');
|
||||
Object.entries(toolCounts).sort((a, b) => b[1] - a[1]).forEach(([tool, count]) => {
|
||||
console.log(` ${tool}: ${count}`);
|
||||
});
|
||||
console.log();
|
||||
console.log('-'.repeat(100));
|
||||
console.log(`Eligible tool uses: ${eligible}`);
|
||||
console.log(`Ineligible (Edit/Write/etc): ${ineligible}`);
|
||||
console.log(`No transcript data: ${noTranscript}`);
|
||||
console.log('-'.repeat(100));
|
||||
console.log();
|
||||
console.log('Strategy Transforms No Transform Chars Saved Original Size Savings %');
|
||||
console.log('-'.repeat(100));
|
||||
|
||||
strategies.forEach(strategy => {
|
||||
const r = results[strategy.name];
|
||||
const pct = r.totalOriginal > 0 ? ((r.saved / r.totalOriginal) * 100).toFixed(1) : '0.0';
|
||||
console.log(
|
||||
`${strategy.desc.padEnd(35)} ${String(r.transforms).padStart(10)} ${String(r.noTransform).padStart(12)} ${String(r.saved.toLocaleString()).padStart(13)} ${String(r.totalOriginal.toLocaleString()).padStart(15)} ${pct.padStart(8)}%`
|
||||
);
|
||||
});
|
||||
|
||||
console.log('-'.repeat(100));
|
||||
console.log();
|
||||
|
||||
// Find best strategy
|
||||
let bestStrategy = null;
|
||||
let bestSavings = 0;
|
||||
strategies.forEach(strategy => {
|
||||
if (results[strategy.name].saved > bestSavings) {
|
||||
bestSavings = results[strategy.name].saved;
|
||||
bestStrategy = strategy;
|
||||
}
|
||||
});
|
||||
|
||||
if (bestStrategy) {
|
||||
const r = results[bestStrategy.name];
|
||||
const pct = ((r.saved / r.totalOriginal) * 100).toFixed(1);
|
||||
console.log(`BEST STRATEGY: ${bestStrategy.desc}`);
|
||||
console.log(` - Transforms ${r.transforms} of ${eligible} eligible tool uses (${((r.transforms/eligible)*100).toFixed(1)}%)`);
|
||||
console.log(` - Saves ${r.saved.toLocaleString()} of ${r.totalOriginal.toLocaleString()} chars (${pct}% reduction)`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
await loadOriginalContent();
|
||||
const observations = queryObservations();
|
||||
analyzeTransformations(observations);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,12 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Error Handling Anti-Pattern Detector
|
||||
*
|
||||
* Detects try-catch anti-patterns that cause silent failures and debugging nightmares.
|
||||
* Run this before committing code that touches error handling.
|
||||
*
|
||||
* Based on hard-learned lessons: defensive try-catch wastes 10+ hours of debugging time.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
@@ -22,9 +14,9 @@ interface AntiPattern {
|
||||
}
|
||||
|
||||
const CRITICAL_PATHS = [
|
||||
'SDKAgent.ts',
|
||||
'GeminiAgent.ts',
|
||||
'OpenRouterAgent.ts',
|
||||
'ClaudeProvider.ts',
|
||||
'GeminiProvider.ts',
|
||||
'OpenRouterProvider.ts',
|
||||
'SessionStore.ts',
|
||||
'worker-service.ts'
|
||||
];
|
||||
@@ -56,19 +48,15 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
const relPath = relative(projectRoot, filePath);
|
||||
const isCriticalPath = CRITICAL_PATHS.some(cp => filePath.includes(cp));
|
||||
|
||||
// Detect error message string matching for type detection (line-by-line patterns)
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Check for [ANTI-PATTERN IGNORED] on the same or previous line
|
||||
const hasOverride = trimmed.includes('[ANTI-PATTERN IGNORED]') ||
|
||||
(i > 0 && lines[i - 1].includes('[ANTI-PATTERN IGNORED]'));
|
||||
const overrideMatch = (trimmed + (i > 0 ? lines[i - 1] : '')).match(/\[ANTI-PATTERN IGNORED\]:\s*(.+)/i);
|
||||
const overrideReason = overrideMatch?.[1]?.trim();
|
||||
|
||||
// CRITICAL: Error message string matching for type detection
|
||||
// Patterns like: errorMessage.includes('connection') or error.message.includes('timeout')
|
||||
const errorStringMatchPatterns = [
|
||||
/error(?:Message|\.message)\s*\.includes\s*\(\s*['"`](\w+)['"`]\s*\)/i,
|
||||
/(?:err|e)\.message\s*\.includes\s*\(\s*['"`](\w+)['"`]\s*\)/i,
|
||||
@@ -79,7 +67,6 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
const match = trimmed.match(pattern);
|
||||
if (match) {
|
||||
const matchedString = match[1];
|
||||
// Common generic patterns that are too broad
|
||||
const genericPatterns = ['error', 'fail', 'connection', 'timeout', 'not', 'invalid', 'unable'];
|
||||
const isGeneric = genericPatterns.some(gp => matchedString.toLowerCase().includes(gp));
|
||||
|
||||
@@ -106,8 +93,6 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
}
|
||||
}
|
||||
|
||||
// HIGH: Logging only error.message instead of the full error object
|
||||
// Patterns like: logger.error('X', 'Y', {}, error.message) or console.error(error.message)
|
||||
const partialErrorLoggingPatterns = [
|
||||
/logger\.(error|warn|info|debug|failure)\s*\([^)]*,\s*(?:error|err|e)\.message\s*\)/,
|
||||
/logger\.(error|warn|info|debug|failure)\s*\([^)]*\{\s*(?:error|err|e):\s*(?:error|err|e)\.message\s*\}/,
|
||||
@@ -140,8 +125,6 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Catch-all error type guessing based on message content
|
||||
// Pattern: if (errorMessage.includes('X') || errorMessage.includes('Y'))
|
||||
const multipleIncludes = trimmed.match(/(?:error(?:Message|\.message)|(?:err|e)\.message).*\.includes.*\|\|.*\.includes/i);
|
||||
if (multipleIncludes) {
|
||||
if (hasOverride && overrideReason) {
|
||||
@@ -167,7 +150,6 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
}
|
||||
}
|
||||
|
||||
// Track try-catch blocks
|
||||
let inTry = false;
|
||||
let tryStartLine = 0;
|
||||
let tryLines: string[] = [];
|
||||
@@ -180,7 +162,6 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Detect standalone promise empty catch: .catch(() => {})
|
||||
const emptyPromiseCatch = trimmed.match(/\.catch\s*\(\s*\(\s*\)\s*=>\s*\{\s*\}\s*\)/);
|
||||
if (emptyPromiseCatch) {
|
||||
antiPatterns.push({
|
||||
@@ -193,14 +174,11 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
});
|
||||
}
|
||||
|
||||
// Detect standalone promise catch without logging: .catch(err => ...)
|
||||
const promiseCatchMatch = trimmed.match(/\.catch\s*\(\s*(?:\(\s*)?(\w+)(?:\s*\))?\s*=>/);
|
||||
if (promiseCatchMatch && !emptyPromiseCatch) {
|
||||
// Look ahead up to 10 lines to see if there's logging in the handler body
|
||||
let catchBody = trimmed.substring(promiseCatchMatch.index || 0);
|
||||
let braceCount = (catchBody.match(/{/g) || []).length - (catchBody.match(/}/g) || []).length;
|
||||
|
||||
// Collect subsequent lines if the handler spans multiple lines
|
||||
let lookAhead = 0;
|
||||
while (braceCount > 0 && lookAhead < 10 && i + lookAhead + 1 < lines.length) {
|
||||
lookAhead++;
|
||||
@@ -224,8 +202,6 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
}
|
||||
}
|
||||
|
||||
// Detect try block start (only when NOT already inside a catch block —
|
||||
// nested try/catch inside a catch is just catch-block content)
|
||||
if (!inCatch && (trimmed.match(/^\s*try\s*{/) || trimmed.match(/}\s*try\s*{/))) {
|
||||
inTry = true;
|
||||
tryStartLine = i + 1;
|
||||
@@ -234,16 +210,13 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track try block content
|
||||
if (inTry && !inCatch) {
|
||||
tryLines.push(line);
|
||||
|
||||
// Count braces to find try block end
|
||||
const openBraces = (line.match(/{/g) || []).length;
|
||||
const closeBraces = (line.match(/}/g) || []).length;
|
||||
braceDepth += openBraces - closeBraces;
|
||||
|
||||
// Found catch
|
||||
if (trimmed.match(/}\s*catch\s*(\(|{)/)) {
|
||||
inCatch = true;
|
||||
catchStartLine = i + 1;
|
||||
@@ -253,7 +226,6 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
}
|
||||
}
|
||||
|
||||
// Track catch block
|
||||
if (inCatch) {
|
||||
catchLines.push(line);
|
||||
|
||||
@@ -261,9 +233,7 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
const closeBraces = (line.match(/}/g) || []).length;
|
||||
braceDepth += openBraces - closeBraces;
|
||||
|
||||
// Catch block ended
|
||||
if (braceDepth === 0) {
|
||||
// Analyze the try-catch block
|
||||
analyzeTryCatchBlock(
|
||||
filePath,
|
||||
relPath,
|
||||
@@ -275,7 +245,6 @@ function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[
|
||||
antiPatterns
|
||||
);
|
||||
|
||||
// Reset
|
||||
inTry = false;
|
||||
inCatch = false;
|
||||
tryLines = [];
|
||||
@@ -300,14 +269,12 @@ function analyzeTryCatchBlock(
|
||||
const tryBlock = tryLines.join('\n');
|
||||
const catchBlock = catchLines.join('\n');
|
||||
|
||||
// CRITICAL: Empty catch block
|
||||
const catchContent = catchBlock
|
||||
.replace(/}\s*catch\s*\([^)]*\)\s*{/, '') // Remove catch signature
|
||||
.replace(/}\s*catch\s*{/, '') // Remove catch without param
|
||||
.replace(/}$/, '') // Remove closing brace
|
||||
.replace(/}\s*catch\s*\([^)]*\)\s*{/, '')
|
||||
.replace(/}\s*catch\s*{/, '')
|
||||
.replace(/}\s*$/, '')
|
||||
.trim();
|
||||
|
||||
// Check for comment-only catch blocks
|
||||
const nonCommentContent = catchContent
|
||||
.split('\n')
|
||||
.filter(line => {
|
||||
@@ -328,11 +295,9 @@ function analyzeTryCatchBlock(
|
||||
});
|
||||
}
|
||||
|
||||
// Check for [ANTI-PATTERN IGNORED] marker
|
||||
const overrideMatch = catchContent.match(/\/\/\s*\[ANTI-PATTERN IGNORED\]:\s*(.+)/i);
|
||||
const overrideReason = overrideMatch?.[1]?.trim();
|
||||
|
||||
// CRITICAL: No logging in catch block (unless explicitly approved)
|
||||
const hasLogging = catchContent.match(/logger\.(error|warn|debug|info|failure)/);
|
||||
const hasConsoleError = catchContent.match(/console\.(error|warn)/);
|
||||
const hasStderr = catchContent.match(/process\.stderr\.write/);
|
||||
@@ -361,7 +326,6 @@ function analyzeTryCatchBlock(
|
||||
}
|
||||
}
|
||||
|
||||
// HIGH: Large try block (>10 lines)
|
||||
const significantTryLines = tryLines.filter(line => {
|
||||
const t = line.trim();
|
||||
return t && !t.startsWith('//') && t !== '{' && t !== '}';
|
||||
@@ -378,7 +342,6 @@ function analyzeTryCatchBlock(
|
||||
});
|
||||
}
|
||||
|
||||
// HIGH: Generic catch without type checking
|
||||
const catchParam = catchBlock.match(/catch\s*\(([^)]+)\)/)?.[1]?.trim();
|
||||
const hasTypeCheck = catchContent.match(/instanceof\s+Error/) ||
|
||||
catchContent.match(/\.name\s*===/) ||
|
||||
@@ -395,7 +358,6 @@ function analyzeTryCatchBlock(
|
||||
});
|
||||
}
|
||||
|
||||
// CRITICAL on critical paths: Catch-and-continue
|
||||
if (isCriticalPath && nonCommentContent && !hasThrow) {
|
||||
const hasReturn = catchContent.match(/return/);
|
||||
const hasProcessExit = catchContent.match(/process\.exit/);
|
||||
@@ -486,7 +448,6 @@ function formatReport(antiPatterns: AntiPattern[]): string {
|
||||
return report;
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const projectRoot = process.cwd();
|
||||
const srcDir = join(projectRoot, 'src');
|
||||
|
||||
@@ -505,7 +466,6 @@ for (const file of tsFiles) {
|
||||
const report = formatReport(allAntiPatterns);
|
||||
console.log(report);
|
||||
|
||||
// Exit with error code if any issues found
|
||||
const issues = allAntiPatterns.filter(a => a.severity === 'ISSUE');
|
||||
if (issues.length > 0) {
|
||||
console.error(`❌ FAILED: ${issues.length} error handling anti-patterns must be fixed.\n`);
|
||||
|
||||
@@ -112,12 +112,10 @@ async function promptMultiline(prompt: string): Promise<string> {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.on("line", (line) => {
|
||||
// Empty line means we're done
|
||||
if (line.trim() === "" && lines.length > 0) {
|
||||
rl.close();
|
||||
resolve(lines.join("\n"));
|
||||
} else if (line.trim() !== "") {
|
||||
// Only add non-empty lines (or preserve empty lines in the middle)
|
||||
lines.push(line);
|
||||
}
|
||||
});
|
||||
@@ -139,7 +137,6 @@ async function main() {
|
||||
console.log("🌎 Leave report in ANY language, and it will auto translate to English\n");
|
||||
console.log("🔍 Collecting system diagnostics...");
|
||||
|
||||
// Collect diagnostics
|
||||
const diagnostics = await collectDiagnostics({
|
||||
includeLogs: !args.noLogs,
|
||||
});
|
||||
@@ -154,7 +151,6 @@ async function main() {
|
||||
}
|
||||
console.log("✓ Configuration loaded\n");
|
||||
|
||||
// Show summary
|
||||
console.log("📋 System Summary:");
|
||||
console.log(` Claude-mem: v${diagnostics.versions.claudeMem}`);
|
||||
console.log(` Claude Code: ${diagnostics.versions.claudeCode}`);
|
||||
@@ -171,7 +167,6 @@ async function main() {
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Prompt for issue details
|
||||
const issueDescription = await promptMultiline(
|
||||
"Please describe the issue you're experiencing:"
|
||||
);
|
||||
@@ -203,7 +198,6 @@ async function main() {
|
||||
|
||||
console.log("\n🤖 Generating bug report with Claude...");
|
||||
|
||||
// Generate the bug report
|
||||
const result = await generateBugReport({
|
||||
issueDescription,
|
||||
expectedBehavior: expectedBehavior.trim() || undefined,
|
||||
@@ -218,7 +212,6 @@ async function main() {
|
||||
|
||||
console.log("✓ Issue formatted successfully\n");
|
||||
|
||||
// Generate output file path
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/:/g, "")
|
||||
@@ -230,15 +223,12 @@ async function main() {
|
||||
);
|
||||
const outputPath = args.output || defaultOutputPath;
|
||||
|
||||
// Save to file
|
||||
await fs.writeFile(outputPath, result.body, "utf-8");
|
||||
|
||||
// Build GitHub URL with pre-filled title and body
|
||||
const encodedTitle = encodeURIComponent(result.title);
|
||||
const encodedBody = encodeURIComponent(result.body);
|
||||
const githubUrl = `https://github.com/thedotmack/claude-mem/issues/new?title=${encodedTitle}&body=${encodedBody}`;
|
||||
|
||||
// Display the report
|
||||
console.log("─".repeat(60));
|
||||
console.log("📋 BUG REPORT GENERATED");
|
||||
console.log("─".repeat(60));
|
||||
@@ -251,7 +241,6 @@ async function main() {
|
||||
console.log("─".repeat(60));
|
||||
console.log();
|
||||
|
||||
// Open GitHub issue in browser
|
||||
console.log("🌐 Opening GitHub issue form in your browser...");
|
||||
try {
|
||||
const openCommand =
|
||||
|
||||
@@ -216,7 +216,6 @@ export async function collectDiagnostics(
|
||||
const cwd = process.cwd();
|
||||
const isDevMode = cwd.includes("claude-mem") && !cwd.includes(".claude");
|
||||
|
||||
// Collect version information
|
||||
const [claudeMem, claudeCode, bun, osVersion] = await Promise.all([
|
||||
getClaudememVersion(),
|
||||
getClaudeCodeVersion(),
|
||||
@@ -244,7 +243,6 @@ export async function collectDiagnostics(
|
||||
isDevMode,
|
||||
};
|
||||
|
||||
// Check worker status
|
||||
const pidInfo = await readPidFile(dataDir);
|
||||
const workerPort = pidInfo?.port || 37777;
|
||||
|
||||
@@ -263,7 +261,6 @@ export async function collectDiagnostics(
|
||||
stats,
|
||||
};
|
||||
|
||||
// Collect logs if requested
|
||||
let workerLog: string[] = [];
|
||||
let silentLog: string[] = [];
|
||||
|
||||
@@ -283,7 +280,6 @@ export async function collectDiagnostics(
|
||||
silentLog: silentLog.map(sanitizePath),
|
||||
};
|
||||
|
||||
// Database info
|
||||
const [dbInfo, tableCounts] = await Promise.all([
|
||||
getDatabaseInfo(dataDir),
|
||||
getTableCounts(dataDir),
|
||||
@@ -295,7 +291,6 @@ export async function collectDiagnostics(
|
||||
counts: tableCounts,
|
||||
};
|
||||
|
||||
// Configuration
|
||||
const settingsInfo = await getSettings(dataDir);
|
||||
const config = {
|
||||
settingsPath: sanitizePath(path.join(dataDir, "settings.json")),
|
||||
@@ -381,7 +376,6 @@ export function formatDiagnostics(diagnostics: SystemDiagnostics): string {
|
||||
}
|
||||
output += "\n";
|
||||
|
||||
// Add logs if present
|
||||
if (diagnostics.logs.workerLog.length > 0) {
|
||||
output += "## Recent Worker Logs (Last 50 Lines)\n\n";
|
||||
output += "```\n";
|
||||
|
||||
@@ -27,14 +27,12 @@ export async function generateBugReport(
|
||||
input: BugReportInput
|
||||
): Promise<BugReportResult> {
|
||||
try {
|
||||
// Collect system diagnostics
|
||||
const diagnostics = await collectDiagnostics({
|
||||
includeLogs: input.includeLogs !== false,
|
||||
});
|
||||
|
||||
const formattedDiagnostics = formatDiagnostics(diagnostics);
|
||||
|
||||
// Build the prompt
|
||||
const prompt = buildPrompt(
|
||||
formattedDiagnostics,
|
||||
input.issueDescription,
|
||||
@@ -42,7 +40,6 @@ export async function generateBugReport(
|
||||
input.stepsToReproduce
|
||||
);
|
||||
|
||||
// Use Agent SDK to generate formatted issue
|
||||
let generatedMarkdown = "";
|
||||
let charCount = 0;
|
||||
const startTime = Date.now();
|
||||
@@ -58,11 +55,9 @@ export async function generateBugReport(
|
||||
},
|
||||
});
|
||||
|
||||
// Progress spinner frames
|
||||
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
let spinnerIdx = 0;
|
||||
|
||||
// Stream the response
|
||||
for await (const message of stream) {
|
||||
if (message.type === "stream_event") {
|
||||
const event = message.event as { type: string; delta?: { type: string; text?: string } };
|
||||
@@ -76,7 +71,6 @@ export async function generateBugReport(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle full assistant messages (fallback)
|
||||
if (message.type === "assistant") {
|
||||
for (const block of message.message.content) {
|
||||
if (block.type === "text" && !generatedMarkdown) {
|
||||
@@ -86,7 +80,6 @@ export async function generateBugReport(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle result
|
||||
if (message.type === "result") {
|
||||
const result = message as SDKResultMessage;
|
||||
if (result.subtype === "success" && !generatedMarkdown && result.result) {
|
||||
@@ -96,10 +89,8 @@ export async function generateBugReport(
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the progress line
|
||||
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
||||
|
||||
// Extract title from markdown (first heading)
|
||||
const titleMatch = generatedMarkdown.match(/^#\s+(.+)$/m);
|
||||
const title = titleMatch ? titleMatch[1] : "Bug Report";
|
||||
|
||||
@@ -109,7 +100,6 @@ export async function generateBugReport(
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
// Fallback to template-based generation
|
||||
console.error("Agent SDK failed, using template fallback:", error);
|
||||
return generateTemplateFallback(input);
|
||||
}
|
||||
|
||||
+19
-74
@@ -1,10 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build script for claude-mem hooks
|
||||
* Bundles TypeScript hooks into individual standalone executables using esbuild
|
||||
*/
|
||||
|
||||
import { build } from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -27,39 +22,18 @@ const CONTEXT_GENERATOR = {
|
||||
source: 'src/services/context-generator.ts'
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip hardcoded __dirname/__filename from bundled CJS output.
|
||||
*
|
||||
* When esbuild converts ESM TypeScript source to CJS format, it inlines
|
||||
* __dirname and __filename as static strings based on the SOURCE file paths
|
||||
* at build time. These `var __dirname = "/build/machine/path/..."` declarations
|
||||
* shadow the runtime's native __dirname (provided by Bun/Node's CJS module
|
||||
* wrapper), causing path resolution to fail on end-user machines.
|
||||
*
|
||||
* This post-build step removes those hardcoded assignments so the runtime
|
||||
* globals are used instead.
|
||||
*
|
||||
* See: https://github.com/thedotmack/claude-mem/issues/1410
|
||||
*/
|
||||
function stripHardcodedDirname(filePath) {
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
const before = content.length;
|
||||
|
||||
// Match both double-quoted and single-quoted string literals.
|
||||
// esbuild currently emits double quotes, but single quotes are handled
|
||||
// defensively in case future versions change quoting style.
|
||||
const str = `(?:"[^"]*"|'[^']*')`;
|
||||
|
||||
for (const id of ['__dirname', '__filename']) {
|
||||
// Remove `var <id> = "...", rest` → `var rest`
|
||||
content = content.replace(new RegExp(`\\bvar ${id}\\s*=\\s*${str},\\s*`, 'g'), 'var ');
|
||||
// Remove standalone `var <id> = "...";`
|
||||
content = content.replace(new RegExp(`\\bvar ${id}\\s*=\\s*${str};\\s*`, 'g'), '');
|
||||
// Remove `, <id> = "..."` from mid/end of var declarations
|
||||
content = content.replace(new RegExp(`,\\s*${id}\\s*=\\s*${str}`, 'g'), '');
|
||||
}
|
||||
|
||||
// Clean up dangling `var ;` left when __dirname was the sole declarator
|
||||
content = content.replace(/\bvar\s*;/g, '');
|
||||
|
||||
const removed = before - content.length;
|
||||
@@ -73,12 +47,10 @@ async function buildHooks() {
|
||||
console.log('🔨 Building claude-mem hooks and worker service...\n');
|
||||
|
||||
try {
|
||||
// Read version from package.json
|
||||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
||||
const version = packageJson.version;
|
||||
console.log(`📌 Version: ${version}`);
|
||||
|
||||
// Create output directories
|
||||
console.log('\n📦 Preparing output directories...');
|
||||
const hooksDir = 'plugin/scripts';
|
||||
const uiDir = 'plugin/ui';
|
||||
@@ -91,8 +63,6 @@ async function buildHooks() {
|
||||
}
|
||||
console.log('✓ Output directories ready');
|
||||
|
||||
// Generate plugin/package.json for cache directory dependency installation
|
||||
// Note: bun:sqlite is a Bun built-in, no external dependencies needed for SQLite
|
||||
console.log('\n📦 Generating plugin package.json...');
|
||||
const pluginPackageJson = {
|
||||
name: 'claude-mem-plugin',
|
||||
@@ -101,10 +71,6 @@ async function buildHooks() {
|
||||
description: 'Runtime dependencies for claude-mem bundled hooks',
|
||||
type: 'module',
|
||||
dependencies: {
|
||||
// Externalized from mcp-server.cjs to avoid Zod version conflicts when
|
||||
// OpenCode's Bun bundler assembles hook scripts (#2113). MCP SDK
|
||||
// transitively imports Zod; loading it via node_modules at runtime
|
||||
// ensures OpenCode controls the version.
|
||||
'zod': '^4.3.6',
|
||||
'tree-sitter-cli': '^0.26.5',
|
||||
'tree-sitter-c': '^0.24.1',
|
||||
@@ -132,6 +98,12 @@ async function buildHooks() {
|
||||
'@derekstride/tree-sitter-sql': '^0.3.11',
|
||||
'@tree-sitter-grammars/tree-sitter-markdown': '^0.3.2',
|
||||
},
|
||||
overrides: {
|
||||
'tree-sitter': '^0.25.0'
|
||||
},
|
||||
trustedDependencies: [
|
||||
'tree-sitter-cli'
|
||||
],
|
||||
engines: {
|
||||
node: '>=18.0.0',
|
||||
bun: '>=1.0.0'
|
||||
@@ -140,7 +112,6 @@ async function buildHooks() {
|
||||
fs.writeFileSync('plugin/package.json', JSON.stringify(pluginPackageJson, null, 2) + '\n');
|
||||
console.log('✓ plugin/package.json generated');
|
||||
|
||||
// Build React viewer
|
||||
console.log('\n📋 Building React viewer...');
|
||||
const { spawn } = await import('child_process');
|
||||
const viewerBuild = spawn('node', ['scripts/build-viewer.js'], { stdio: 'inherit' });
|
||||
@@ -154,7 +125,6 @@ async function buildHooks() {
|
||||
});
|
||||
});
|
||||
|
||||
// Build worker service
|
||||
console.log(`\n🔧 Building worker service...`);
|
||||
await build({
|
||||
entryPoints: [WORKER_SERVICE.source],
|
||||
@@ -167,10 +137,8 @@ async function buildHooks() {
|
||||
logLevel: 'error', // Suppress warnings (import.meta warning is benign)
|
||||
external: [
|
||||
'bun:sqlite',
|
||||
// Optional chromadb embedding providers
|
||||
'cohere-ai',
|
||||
'ollama',
|
||||
// Default embedding function with native binaries
|
||||
'@chroma-core/default-embed',
|
||||
'onnxruntime-node'
|
||||
],
|
||||
@@ -186,15 +154,12 @@ async function buildHooks() {
|
||||
}
|
||||
});
|
||||
|
||||
// Fix hardcoded __dirname/__filename in bundled output (#1410)
|
||||
stripHardcodedDirname(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
|
||||
|
||||
// Make worker service executable
|
||||
fs.chmodSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
|
||||
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
|
||||
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Build MCP server
|
||||
console.log(`\n🔧 Building MCP server...`);
|
||||
await build({
|
||||
entryPoints: [MCP_SERVER.source],
|
||||
@@ -207,10 +172,6 @@ async function buildHooks() {
|
||||
logLevel: 'error',
|
||||
external: [
|
||||
'bun:sqlite',
|
||||
// Externalize Zod to avoid version conflicts when OpenCode's Bun bundler
|
||||
// assembles hook scripts (see #2113). The MCP server transitively imports
|
||||
// Zod via @modelcontextprotocol/sdk; bundling it caused two Zod versions
|
||||
// to coexist at runtime and the v4 ↔ v3 _zod.def access crashed.
|
||||
'zod',
|
||||
'tree-sitter-cli',
|
||||
'tree-sitter-javascript',
|
||||
@@ -246,24 +207,12 @@ async function buildHooks() {
|
||||
}
|
||||
});
|
||||
|
||||
// Fix hardcoded __dirname/__filename in bundled output (#1410)
|
||||
stripHardcodedDirname(`${hooksDir}/${MCP_SERVER.name}.cjs`);
|
||||
|
||||
// Make MCP server executable
|
||||
fs.chmodSync(`${hooksDir}/${MCP_SERVER.name}.cjs`, 0o755);
|
||||
const mcpServerStats = fs.statSync(`${hooksDir}/${MCP_SERVER.name}.cjs`);
|
||||
console.log(`✓ mcp-server built (${(mcpServerStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// GUARDRAIL (#1645): The MCP server runs under Node, but the entire `bun:`
|
||||
// module namespace (bun:sqlite, bun:ffi, bun:test, etc.) is Bun-only. If
|
||||
// any transitive import in mcp-server.ts ever pulls one in, the bundle
|
||||
// will crash on first require under Node — which is exactly the regression
|
||||
// PR #1645 fixed for `bun:sqlite`. Fail the build instead of shipping a
|
||||
// broken bundle so future contributors get an immediate signal.
|
||||
//
|
||||
// Only flag actual `require("bun:...")` / `require('bun:...')` calls, not
|
||||
// the bare string — error messages and inline comments may legitimately
|
||||
// mention `bun:sqlite` by name without re-introducing the import.
|
||||
const mcpBundleContent = fs.readFileSync(`${hooksDir}/${MCP_SERVER.name}.cjs`, 'utf-8');
|
||||
const bunRequireRegex = /require\(\s*["']bun:[a-z][a-z0-9_-]*["']\s*\)/;
|
||||
const bunRequireMatch = mcpBundleContent.match(bunRequireRegex);
|
||||
@@ -273,16 +222,6 @@ async function buildHooks() {
|
||||
);
|
||||
}
|
||||
|
||||
// SECONDARY GUARDRAIL (#1645 round 11): bundle size budget. The bun:sqlite
|
||||
// regex above catches the specific regression class we already know about,
|
||||
// but esbuild could in theory change how it emits external module specifiers
|
||||
// and silently slip past the regex. A bundle-size budget catches the
|
||||
// structural symptom (worker-service.ts dragged into the bundle blew the
|
||||
// size from ~358KB to ~1.96MB) regardless of how the imports look.
|
||||
//
|
||||
// 600KB is a generous ceiling — current size is ~384KB, the broken v12.0.0
|
||||
// bundle was ~1920KB, and there's plenty of headroom for legitimate growth
|
||||
// before we'd want to revisit this number.
|
||||
const MCP_SERVER_MAX_BYTES = 600 * 1024;
|
||||
if (mcpServerStats.size > MCP_SERVER_MAX_BYTES) {
|
||||
throw new Error(
|
||||
@@ -290,7 +229,6 @@ async function buildHooks() {
|
||||
);
|
||||
}
|
||||
|
||||
// Build context generator
|
||||
console.log(`\n🔧 Building context generator...`);
|
||||
await build({
|
||||
entryPoints: [CONTEXT_GENERATOR.source],
|
||||
@@ -308,13 +246,11 @@ async function buildHooks() {
|
||||
// No banner needed: CJS files under Node.js have __dirname/__filename natively
|
||||
});
|
||||
|
||||
// Fix hardcoded __dirname/__filename in bundled output (#1410)
|
||||
stripHardcodedDirname(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`);
|
||||
|
||||
const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`);
|
||||
console.log(`✓ context-generator built (${(contextGenStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Build NPX CLI (pure Node.js — no Bun dependency)
|
||||
console.log(`\n🔧 Building NPX CLI...`);
|
||||
const npxCliOutDir = 'dist/npx-cli';
|
||||
if (!fs.existsSync(npxCliOutDir)) {
|
||||
@@ -334,18 +270,17 @@ async function buildHooks() {
|
||||
'fs', 'fs/promises', 'path', 'os', 'child_process', 'url',
|
||||
'crypto', 'http', 'https', 'net', 'stream', 'util', 'events',
|
||||
'buffer', 'querystring', 'readline', 'tty', 'assert',
|
||||
'bun:sqlite',
|
||||
],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
});
|
||||
|
||||
// Make NPX CLI executable
|
||||
fs.chmodSync(`${npxCliOutDir}/index.js`, 0o755);
|
||||
const npxCliStats = fs.statSync(`${npxCliOutDir}/index.js`);
|
||||
console.log(`✓ npx-cli built (${(npxCliStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Build OpenClaw plugin (self-contained, only Node builtins external)
|
||||
if (fs.existsSync('openclaw/src/index.ts')) {
|
||||
console.log(`\n🔧 Building OpenClaw plugin...`);
|
||||
const openclawOutDir = 'openclaw/dist';
|
||||
@@ -371,7 +306,6 @@ async function buildHooks() {
|
||||
console.log(`✓ openclaw plugin built (${(openclawStats.size / 1024).toFixed(2)} KB)`);
|
||||
}
|
||||
|
||||
// Build OpenCode plugin (self-contained, Node.js ESM — Bun-compatible)
|
||||
if (fs.existsSync('src/integrations/opencode-plugin/index.ts')) {
|
||||
console.log(`\n🔧 Building OpenCode plugin...`);
|
||||
const opencodeOutDir = 'dist/opencode-plugin';
|
||||
@@ -397,11 +331,22 @@ async function buildHooks() {
|
||||
console.log(`✓ opencode plugin built (${(opencodeStats.size / 1024).toFixed(2)} KB)`);
|
||||
}
|
||||
|
||||
// Verify critical distribution files exist (skills are source files, not build outputs)
|
||||
console.log('\n📋 Copying onboarding explainer to plugin tree...');
|
||||
const onboardingExplainerSrc = 'src/services/worker/onboarding-explainer.md';
|
||||
const onboardingExplainerDst = 'plugin/skills/how-it-works/onboarding-explainer.md';
|
||||
if (!fs.existsSync(onboardingExplainerSrc)) {
|
||||
throw new Error(`Missing onboarding explainer source: ${onboardingExplainerSrc}`);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(onboardingExplainerDst), { recursive: true });
|
||||
fs.copyFileSync(onboardingExplainerSrc, onboardingExplainerDst);
|
||||
console.log(`✓ Copied ${onboardingExplainerSrc} → ${onboardingExplainerDst}`);
|
||||
|
||||
console.log('\n📋 Verifying distribution files...');
|
||||
const requiredDistributionFiles = [
|
||||
'plugin/skills/mem-search/SKILL.md',
|
||||
'plugin/skills/smart-explore/SKILL.md',
|
||||
'plugin/skills/how-it-works/SKILL.md',
|
||||
'plugin/skills/how-it-works/onboarding-explainer.md',
|
||||
'plugin/hooks/hooks.json',
|
||||
'plugin/.claude-plugin/plugin.json',
|
||||
];
|
||||
|
||||
@@ -13,7 +13,6 @@ async function buildViewer() {
|
||||
console.log('Building React viewer...');
|
||||
|
||||
try {
|
||||
// Build React app
|
||||
await esbuild.build({
|
||||
entryPoints: [path.join(rootDir, 'src/ui/viewer/index.tsx')],
|
||||
bundle: true,
|
||||
@@ -32,7 +31,6 @@ async function buildViewer() {
|
||||
}
|
||||
});
|
||||
|
||||
// Copy HTML template to build output
|
||||
const htmlTemplate = fs.readFileSync(
|
||||
path.join(rootDir, 'src/ui/viewer-template.html'),
|
||||
'utf-8'
|
||||
@@ -42,7 +40,6 @@ async function buildViewer() {
|
||||
htmlTemplate
|
||||
);
|
||||
|
||||
// Copy font assets
|
||||
const fontsDir = path.join(rootDir, 'src/ui/viewer/assets/fonts');
|
||||
const outputFontsDir = path.join(rootDir, 'plugin/ui/assets/fonts');
|
||||
|
||||
@@ -57,7 +54,6 @@ 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'));
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build Windows executable for claude-mem worker service
|
||||
* Uses Bun's compile feature to create a standalone exe
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Check and process pending observation queue
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/check-pending-queue.ts # Check status and prompt to process
|
||||
* bun scripts/check-pending-queue.ts --process # Auto-process without prompting
|
||||
* bun scripts/check-pending-queue.ts --limit 5 # Process up to 5 sessions
|
||||
*/
|
||||
|
||||
const WORKER_URL = 'http://localhost:37777';
|
||||
|
||||
@@ -82,7 +74,6 @@ function formatAge(epochMs: number): string {
|
||||
}
|
||||
|
||||
async function prompt(question: string): Promise<string> {
|
||||
// Check if we have a TTY for interactive input
|
||||
if (!process.stdin.isTTY) {
|
||||
console.log(question + '(no TTY, use --process flag for non-interactive mode)');
|
||||
return 'n';
|
||||
@@ -102,7 +93,6 @@ async function prompt(question: string): Promise<string> {
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Help flag
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
Claude-Mem Pending Queue Manager
|
||||
@@ -142,7 +132,6 @@ What is this for?
|
||||
|
||||
console.log('\n=== Claude-Mem Pending Queue Status ===\n');
|
||||
|
||||
// Check worker health
|
||||
const healthy = await checkWorkerHealth();
|
||||
if (!healthy) {
|
||||
console.log('Worker is not running. Start it with:');
|
||||
@@ -151,11 +140,9 @@ What is this for?
|
||||
}
|
||||
console.log('Worker status: Running\n');
|
||||
|
||||
// Get queue status
|
||||
const status = await getQueueStatus();
|
||||
const { queue, sessionsWithPendingWork } = status;
|
||||
|
||||
// Display summary
|
||||
console.log('Queue Summary:');
|
||||
console.log(` Pending: ${queue.totalPending}`);
|
||||
console.log(` Processing: ${queue.totalProcessing}`);
|
||||
@@ -163,26 +150,22 @@ What is this for?
|
||||
console.log(` Stuck: ${queue.stuckCount} (processing > 5 min)`);
|
||||
console.log(` Sessions: ${sessionsWithPendingWork.length} with pending work\n`);
|
||||
|
||||
// Check if there's any backlog
|
||||
const hasBacklog = queue.totalPending > 0 || queue.totalFailed > 0;
|
||||
const hasStuck = queue.stuckCount > 0;
|
||||
|
||||
if (!hasBacklog && !hasStuck) {
|
||||
console.log('No backlog detected. Queue is healthy.\n');
|
||||
|
||||
// Show recently processed if any
|
||||
if (status.recentlyProcessed.length > 0) {
|
||||
console.log(`Recently processed: ${status.recentlyProcessed.length} messages in last 30 min\n`);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Show details about pending messages
|
||||
if (queue.messages.length > 0) {
|
||||
console.log('Pending Messages:');
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
// Group by session
|
||||
const bySession = new Map<number, QueueMessage[]>();
|
||||
for (const msg of queue.messages) {
|
||||
const list = bySession.get(msg.session_db_id) || [];
|
||||
@@ -208,7 +191,6 @@ What is this for?
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Offer to process
|
||||
if (autoProcess) {
|
||||
console.log(`Auto-processing up to ${limit} sessions...\n`);
|
||||
} else {
|
||||
@@ -220,7 +202,6 @@ What is this for?
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Process the queue
|
||||
const result = await processQueue(limit);
|
||||
|
||||
console.log('Processing Result:');
|
||||
|
||||
@@ -1,22 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Cleanup script for duplicate observations created by the batching bug.
|
||||
*
|
||||
* The bug: When multiple messages were batched together, observations were stored
|
||||
* once per message ID instead of once per observation. For example, if 4 messages
|
||||
* were batched and produced 3 observations, those 3 observations were stored
|
||||
* 12 times (4×3) instead of 3 times.
|
||||
*
|
||||
* This script identifies duplicates by matching on:
|
||||
* - memory_session_id (same session)
|
||||
* - text (same content)
|
||||
* - type (same observation type)
|
||||
* - created_at_epoch within 60 seconds (same batch window)
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/cleanup-duplicates.ts # Dry run (default)
|
||||
* bun scripts/cleanup-duplicates.ts --execute # Actually delete duplicates
|
||||
*/
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { homedir } from 'os';
|
||||
@@ -24,7 +6,6 @@ import { join } from 'path';
|
||||
|
||||
const DB_PATH = join(homedir(), '.claude-mem', 'claude-mem.db');
|
||||
|
||||
// Time window modes for duplicate detection
|
||||
const TIME_WINDOW_MODES = {
|
||||
strict: 5, // 5 seconds - only exact duplicates from same batch
|
||||
normal: 60, // 60 seconds - duplicates within same minute
|
||||
@@ -57,7 +38,6 @@ function main() {
|
||||
const aggressive = process.argv.includes('--aggressive');
|
||||
const strict = process.argv.includes('--strict');
|
||||
|
||||
// Determine time window
|
||||
let windowMode: keyof typeof TIME_WINDOW_MODES = 'normal';
|
||||
if (aggressive) windowMode = 'aggressive';
|
||||
if (strict) windowMode = 'strict';
|
||||
@@ -80,11 +60,9 @@ function main() {
|
||||
? new Database(DB_PATH, { readonly: true })
|
||||
: new Database(DB_PATH);
|
||||
|
||||
// Get total observation count
|
||||
const totalCount = db.prepare('SELECT COUNT(*) as count FROM observations').get() as { count: number };
|
||||
console.log(`Total observations in database: ${totalCount.count}`);
|
||||
|
||||
// Find all observations and group by content fingerprint
|
||||
const observations = db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
@@ -101,23 +79,17 @@ function main() {
|
||||
console.log(`Analyzing ${observations.length} observations for duplicates...`);
|
||||
console.log('');
|
||||
|
||||
// Group observations by fingerprint (session + text + type + time bucket)
|
||||
const groups = new Map<string, ObservationRow[]>();
|
||||
|
||||
for (const obs of observations) {
|
||||
// Skip observations without title (can't dedupe without content identifier)
|
||||
if (obs.title === null) continue;
|
||||
|
||||
// Create content hash from title + subtitle + narrative
|
||||
const contentKey = `${obs.title}|${obs.subtitle || ''}|${obs.narrative || ''}`;
|
||||
|
||||
// Create fingerprint based on time window mode
|
||||
let fingerprint: string;
|
||||
if (batchWindowSeconds === 0) {
|
||||
// Aggressive mode: ignore time entirely
|
||||
fingerprint = `${obs.memory_session_id}|${obs.type}|${contentKey}`;
|
||||
} else {
|
||||
// Normal/strict mode: include time bucket
|
||||
const epochBucket = Math.floor(obs.created_at_epoch / batchWindowSeconds);
|
||||
fingerprint = `${obs.memory_session_id}|${obs.type}|${epochBucket}|${contentKey}`;
|
||||
}
|
||||
@@ -128,17 +100,14 @@ function main() {
|
||||
groups.get(fingerprint)!.push(obs);
|
||||
}
|
||||
|
||||
// Find groups with duplicates
|
||||
const duplicateGroups: DuplicateGroup[] = [];
|
||||
|
||||
for (const [fingerprint, rows] of groups) {
|
||||
if (rows.length > 1) {
|
||||
// Sort by id to keep the oldest (lowest id)
|
||||
rows.sort((a, b) => a.id - b.id);
|
||||
const keepId = rows[0].id;
|
||||
const deleteIds = rows.slice(1).map(r => r.id);
|
||||
|
||||
// SAFETY: Never delete all copies - always keep at least one
|
||||
if (deleteIds.length >= rows.length) {
|
||||
throw new Error(`SAFETY VIOLATION: Would delete all ${rows.length} copies! Aborting.`);
|
||||
}
|
||||
@@ -166,7 +135,6 @@ function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate stats
|
||||
const totalDuplicates = duplicateGroups.reduce((sum, g) => sum + g.delete_ids.length, 0);
|
||||
const affectedSessions = new Set(duplicateGroups.map(g => g.memory_session_id)).size;
|
||||
|
||||
@@ -178,7 +146,6 @@ function main() {
|
||||
console.log(`Observations after cleanup: ${totalCount.count - totalDuplicates}`);
|
||||
console.log('');
|
||||
|
||||
// Show sample of duplicates
|
||||
console.log('SAMPLE DUPLICATES (first 10 groups):');
|
||||
console.log('-'.repeat(60));
|
||||
|
||||
@@ -195,14 +162,12 @@ function main() {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Execute deletion if not dry run
|
||||
if (!dryRun) {
|
||||
console.log('EXECUTING DELETION...');
|
||||
console.log('-'.repeat(60));
|
||||
|
||||
const allDeleteIds = duplicateGroups.flatMap(g => g.delete_ids);
|
||||
|
||||
// Delete in batches of 500 to avoid SQLite limits
|
||||
const BATCH_SIZE = 500;
|
||||
let deleted = 0;
|
||||
|
||||
@@ -222,7 +187,6 @@ function main() {
|
||||
console.log('');
|
||||
console.log(`Successfully deleted ${deleted} duplicate observations!`);
|
||||
|
||||
// Verify final count
|
||||
const finalCount = db.prepare('SELECT COUNT(*) as count FROM observations').get() as { count: number };
|
||||
console.log(`Final observation count: ${finalCount.count}`);
|
||||
|
||||
|
||||
+53
-174
@@ -1,98 +1,23 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Clear messages from the queue
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/clear-failed-queue.ts # Clear failed messages (interactive)
|
||||
* bun scripts/clear-failed-queue.ts --all # Clear ALL messages (pending, processing, failed)
|
||||
* bun scripts/clear-failed-queue.ts --force # Non-interactive - clear without prompting
|
||||
*/
|
||||
|
||||
const WORKER_URL = 'http://localhost:37777';
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { existsSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
interface QueueMessage {
|
||||
id: number;
|
||||
session_db_id: number;
|
||||
message_type: string;
|
||||
tool_name: string | null;
|
||||
status: 'pending' | 'processing' | 'failed';
|
||||
retry_count: number;
|
||||
created_at_epoch: number;
|
||||
project: string | null;
|
||||
}
|
||||
interface CountRow { count: number }
|
||||
interface StatusRow { status: string; count: number }
|
||||
|
||||
interface QueueResponse {
|
||||
queue: {
|
||||
messages: QueueMessage[];
|
||||
totalPending: number;
|
||||
totalProcessing: number;
|
||||
totalFailed: number;
|
||||
stuckCount: number;
|
||||
};
|
||||
recentlyProcessed: QueueMessage[];
|
||||
sessionsWithPendingWork: number[];
|
||||
}
|
||||
|
||||
interface ClearResponse {
|
||||
success: boolean;
|
||||
clearedCount: number;
|
||||
}
|
||||
|
||||
async function checkWorkerHealth(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${WORKER_URL}/api/health`);
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getQueueStatus(): Promise<QueueResponse> {
|
||||
const res = await fetch(`${WORKER_URL}/api/pending-queue`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to get queue status: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function clearFailedQueue(): Promise<ClearResponse> {
|
||||
const res = await fetch(`${WORKER_URL}/api/pending-queue/failed`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to clear failed queue: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function clearAllQueue(): Promise<ClearResponse> {
|
||||
const res = await fetch(`${WORKER_URL}/api/pending-queue/all`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to clear queue: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function formatAge(epochMs: number): string {
|
||||
const ageMs = Date.now() - epochMs;
|
||||
const minutes = Math.floor(ageMs / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days}d ${hours % 24}h ago`;
|
||||
if (hours > 0) return `${hours}h ${minutes % 60}m ago`;
|
||||
return `${minutes}m ago`;
|
||||
function resolveDbPath(): string {
|
||||
const dataDir = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
|
||||
return join(dataDir, 'claude-mem.db');
|
||||
}
|
||||
|
||||
async function prompt(question: string): Promise<string> {
|
||||
// Check if we have a TTY for interactive input
|
||||
if (!process.stdin.isTTY) {
|
||||
console.log(question + '(no TTY, use --force flag for non-interactive mode)');
|
||||
return 'n';
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
process.stdout.write(question);
|
||||
process.stdin.setRawMode(false);
|
||||
@@ -107,40 +32,30 @@ async function prompt(question: string): Promise<string> {
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Help flag
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
Claude-Mem Queue Clearer
|
||||
|
||||
Clear messages from the observation queue.
|
||||
Clear orphaned messages from the pending_messages SQLite table.
|
||||
|
||||
Usage:
|
||||
bun scripts/clear-failed-queue.ts [options]
|
||||
|
||||
Options:
|
||||
--help, -h Show this help message
|
||||
--all Clear ALL messages (pending, processing, and failed)
|
||||
--all Clear ALL messages (pending, processing, processed, failed)
|
||||
--force Clear without prompting for confirmation
|
||||
|
||||
Examples:
|
||||
# Clear failed messages interactively
|
||||
bun scripts/clear-failed-queue.ts
|
||||
|
||||
# Clear ALL messages (pending, processing, failed)
|
||||
bun scripts/clear-failed-queue.ts --all
|
||||
|
||||
# Clear without confirmation (non-interactive)
|
||||
bun scripts/clear-failed-queue.ts --force
|
||||
|
||||
# Clear all messages without confirmation
|
||||
# Clear ALL messages without confirmation
|
||||
bun scripts/clear-failed-queue.ts --all --force
|
||||
|
||||
What is this for?
|
||||
Failed messages are observations that exceeded the maximum retry count.
|
||||
Processing/pending messages may be stuck or unwanted.
|
||||
This command removes them to clean up the queue.
|
||||
|
||||
--all is useful for a complete reset when you want to start fresh.
|
||||
Notes:
|
||||
Operates directly on ~/.claude-mem/claude-mem.db (or \$CLAUDE_MEM_DATA_DIR).
|
||||
Uses SQLite WAL mode so it is safe to run while the worker is running.
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -152,102 +67,66 @@ What is this for?
|
||||
? '\n=== Claude-Mem Queue Clearer (ALL) ===\n'
|
||||
: '\n=== Claude-Mem Queue Clearer (Failed) ===\n');
|
||||
|
||||
// Check worker health
|
||||
const healthy = await checkWorkerHealth();
|
||||
if (!healthy) {
|
||||
console.log('Worker is not running. Start it with:');
|
||||
console.log(' cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:start\n');
|
||||
process.exit(1);
|
||||
const dbPath = resolveDbPath();
|
||||
if (!existsSync(dbPath)) {
|
||||
console.log(`No database found at ${dbPath}. Nothing to clear.\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
console.log('Worker status: Running\n');
|
||||
|
||||
// Get queue status
|
||||
const status = await getQueueStatus();
|
||||
const { queue } = status;
|
||||
const db = new Database(dbPath);
|
||||
db.run('PRAGMA journal_mode = WAL');
|
||||
|
||||
const counts = db.prepare(
|
||||
'SELECT status, COUNT(*) as count FROM pending_messages GROUP BY status'
|
||||
).all() as StatusRow[];
|
||||
|
||||
const total = counts.reduce((sum, row) => sum + row.count, 0);
|
||||
const failed = counts.find(r => r.status === 'failed')?.count ?? 0;
|
||||
|
||||
console.log('Queue Summary:');
|
||||
console.log(` Pending: ${queue.totalPending}`);
|
||||
console.log(` Processing: ${queue.totalProcessing}`);
|
||||
console.log(` Failed: ${queue.totalFailed}`);
|
||||
for (const status of ['pending', 'processing', 'processed', 'failed'] as const) {
|
||||
const row = counts.find(r => r.status === status);
|
||||
console.log(` ${status.padEnd(11)} ${row?.count ?? 0}`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Check if there are messages to clear
|
||||
const totalToClear = clearAll
|
||||
? queue.totalPending + queue.totalProcessing + queue.totalFailed
|
||||
: queue.totalFailed;
|
||||
|
||||
if (totalToClear === 0) {
|
||||
const willClear = clearAll ? total : failed;
|
||||
if (willClear === 0) {
|
||||
console.log(clearAll
|
||||
? 'No messages in queue. Nothing to clear.\n'
|
||||
: 'No failed messages in queue. Nothing to clear.\n');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Show details about messages to clear
|
||||
const messagesToShow = clearAll ? queue.messages : queue.messages.filter(m => m.status === 'failed');
|
||||
if (messagesToShow.length > 0) {
|
||||
console.log(clearAll ? 'Messages to Clear:' : 'Failed Messages:');
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
// Group by session
|
||||
const bySession = new Map<number, QueueMessage[]>();
|
||||
for (const msg of messagesToShow) {
|
||||
const list = bySession.get(msg.session_db_id) || [];
|
||||
list.push(msg);
|
||||
bySession.set(msg.session_db_id, list);
|
||||
}
|
||||
|
||||
for (const [sessionId, messages] of bySession) {
|
||||
const project = messages[0].project || 'unknown';
|
||||
const oldest = Math.min(...messages.map(m => m.created_at_epoch));
|
||||
|
||||
if (clearAll) {
|
||||
const statuses = {
|
||||
pending: messages.filter(m => m.status === 'pending').length,
|
||||
processing: messages.filter(m => m.status === 'processing').length,
|
||||
failed: messages.filter(m => m.status === 'failed').length
|
||||
};
|
||||
console.log(` Session ${sessionId} (${project})`);
|
||||
console.log(` Messages: ${messages.length} total (${statuses.pending} pending, ${statuses.processing} processing, ${statuses.failed} failed)`);
|
||||
console.log(` Age: ${formatAge(oldest)}`);
|
||||
} else {
|
||||
console.log(` Session ${sessionId} (${project})`);
|
||||
console.log(` Messages: ${messages.length} failed`);
|
||||
console.log(` Age: ${formatAge(oldest)}`);
|
||||
}
|
||||
}
|
||||
console.log('─'.repeat(80));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Confirm before clearing
|
||||
const clearMessage = clearAll
|
||||
? `Clear ${totalToClear} messages (pending, processing, and failed)?`
|
||||
: `Clear ${queue.totalFailed} failed messages?`;
|
||||
|
||||
if (force) {
|
||||
console.log(`${clearMessage.replace('?', '')}...\n`);
|
||||
} else {
|
||||
const answer = await prompt(`${clearMessage} [y/N]: `);
|
||||
if (!force) {
|
||||
const answer = await prompt(
|
||||
clearAll
|
||||
? `Clear ${willClear} messages (all statuses)? [y/N]: `
|
||||
: `Clear ${willClear} failed messages? [y/N]: `
|
||||
);
|
||||
if (answer.toLowerCase() !== 'y') {
|
||||
console.log('\nCancelled. Run with --force to skip confirmation.\n');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Clear the queue
|
||||
const result = clearAll ? await clearAllQueue() : await clearFailedQueue();
|
||||
const stmt = clearAll
|
||||
? db.prepare('DELETE FROM pending_messages')
|
||||
: db.prepare("DELETE FROM pending_messages WHERE status = 'failed'");
|
||||
const cleared = stmt.run().changes;
|
||||
|
||||
const remaining = (db.prepare(
|
||||
'SELECT COUNT(*) as count FROM pending_messages'
|
||||
).get() as CountRow).count;
|
||||
|
||||
console.log('Clearing Result:');
|
||||
console.log(` Messages cleared: ${result.clearedCount}`);
|
||||
console.log(` Status: ${result.success ? 'Success' : 'Failed'}\n`);
|
||||
console.log(` Messages cleared: ${cleared}`);
|
||||
console.log(` Remaining: ${remaining}\n`);
|
||||
|
||||
if (result.success && result.clearedCount > 0) {
|
||||
console.log(clearAll
|
||||
? 'All messages have been removed from the queue.\n'
|
||||
: 'Failed messages have been removed from the queue.\n');
|
||||
}
|
||||
db.close();
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
|
||||
@@ -1,20 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* cwd-remap — Rewrite sdk_sessions.project (+ observations.project,
|
||||
* session_summaries.project) using the cwd captured per-message in
|
||||
* pending_messages.cwd as the single source of truth.
|
||||
*
|
||||
* For each distinct cwd:
|
||||
* - git -C <cwd> rev-parse --git-dir AND --git-common-dir
|
||||
* If they differ → worktree. parent = basename(dirname(common-dir)),
|
||||
* project = parent/<basename(cwd)>.
|
||||
* Else → project = basename(cwd).
|
||||
* - If the directory doesn't exist, or git errors, skip that cwd.
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/cwd-remap.ts # dry-run (default)
|
||||
* bun scripts/cwd-remap.ts --apply # write updates in a single transaction
|
||||
*/
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { homedir } from 'os';
|
||||
@@ -51,7 +35,6 @@ function classify(cwd: string): Classification {
|
||||
const commonDir = git(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
||||
if (!commonDir) return { kind: 'skip', reason: 'no-common-dir' };
|
||||
|
||||
// Use the worktree root, not the cwd — a session may be in a subdir.
|
||||
const toplevel = git(cwd, ['rev-parse', '--show-toplevel']);
|
||||
if (!toplevel) return { kind: 'skip', reason: 'no-toplevel' };
|
||||
const leaf = basename(toplevel);
|
||||
@@ -60,8 +43,6 @@ function classify(cwd: string): Classification {
|
||||
return { kind: 'main', project: leaf };
|
||||
}
|
||||
|
||||
// worktree: common-dir = <parent-repo>/.git (normal) or <parent>.git (bare).
|
||||
// Normal: dirname strips the trailing /.git. Bare: strip the .git suffix.
|
||||
const parentRepoDir = commonDir.endsWith('/.git')
|
||||
? dirname(commonDir)
|
||||
: commonDir.replace(/\.git$/, '');
|
||||
@@ -101,16 +82,12 @@ function main() {
|
||||
}
|
||||
console.log(` main=${counts.main} worktree=${counts.worktree} skip=${counts.skip}`);
|
||||
|
||||
// Skipped cwds (so user sees what's missing)
|
||||
const skipped = [...byCwd.entries()].filter(([, c]) => c.kind === 'skip') as Array<[string, Extract<Classification, { kind: 'skip' }>]>;
|
||||
if (skipped.length) {
|
||||
console.log('\nSkipped cwds:');
|
||||
for (const [cwd, c] of skipped) console.log(` [${c.reason}] ${cwd}`);
|
||||
}
|
||||
|
||||
// Per-session target: use the EARLIEST pending_messages.cwd for each session.
|
||||
// (Dominant-cwd is wrong: claude-mem's own hooks run from nested dirs like
|
||||
// `.context/claude-mem/` and dominate the count, misattributing the session.)
|
||||
const sessionRows = db.prepare(`
|
||||
SELECT s.id AS session_id, s.memory_session_id, s.content_session_id, s.project AS old_project, p.cwd
|
||||
FROM sdk_sessions s
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/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;
|
||||
}
|
||||
@@ -1,15 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Post release notification to Discord
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/discord-release-notify.js v7.4.2
|
||||
* node scripts/discord-release-notify.js v7.4.2 "Custom release notes"
|
||||
*
|
||||
* Requires DISCORD_UPDATES_WEBHOOK in .env file
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
@@ -49,7 +39,6 @@ function getReleaseNotes(version) {
|
||||
}
|
||||
|
||||
function cleanNotes(notes) {
|
||||
// Remove Claude Code footer and clean up
|
||||
return notes
|
||||
.replace(/🤖 Generated with \[Claude Code\].*$/s, '')
|
||||
.replace(/---\n*$/s, '')
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
#!/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`);
|
||||
@@ -1,11 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# E2E Test: Knowledge Agents
|
||||
# Fully hands-off test of the complete knowledge agent lifecycle.
|
||||
# Designed to be orchestrated via tmux-cli from Claude Code.
|
||||
#
|
||||
# Flow: health check → build corpus → list → get → prime → query → reprime → query → rebuild → delete → verify
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
WORKER_URL="http://localhost:37777"
|
||||
@@ -14,8 +7,6 @@ PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
LOG_FILE="${HOME}/.claude-mem/logs/e2e-knowledge-agents-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
# -- Helpers ------------------------------------------------------------------
|
||||
|
||||
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
||||
pass() { PASS_COUNT=$((PASS_COUNT + 1)); log "PASS: $1"; }
|
||||
fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); log "FAIL: $1 — $2"; }
|
||||
@@ -83,15 +74,11 @@ extract_body_and_status() {
|
||||
RESPONSE_STATUS=$(echo "$response" | tail -1)
|
||||
}
|
||||
|
||||
# -- Cleanup ------------------------------------------------------------------
|
||||
|
||||
cleanup_test_corpus() {
|
||||
log "Cleaning up test corpus '$CORPUS_NAME'..."
|
||||
curl -s -X DELETE "$WORKER_URL/api/corpus/$CORPUS_NAME" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# -- Tests --------------------------------------------------------------------
|
||||
|
||||
test_worker_health() {
|
||||
log "=== Test: Worker Health ==="
|
||||
local response
|
||||
@@ -132,7 +119,6 @@ test_list_corpora() {
|
||||
extract_body_and_status "$response"
|
||||
assert_http_status "List corpora" "200" "$RESPONSE_STATUS"
|
||||
|
||||
# Verify our test corpus is in the list
|
||||
local found
|
||||
found=$(echo "$RESPONSE_BODY" | jq -r ".[] | select(.name == \"$CORPUS_NAME\") | .name" 2>/dev/null)
|
||||
if [[ "$found" == "$CORPUS_NAME" ]]; then
|
||||
@@ -193,18 +179,15 @@ test_query_corpus() {
|
||||
|
||||
test_query_without_prime() {
|
||||
log "=== Test: Query Unprimed Corpus ==="
|
||||
# Build a second corpus but don't prime it
|
||||
curl_post "/api/corpus" "{\"name\": \"e2e-unprimed-test\", \"limit\": 5}" > /dev/null 2>&1
|
||||
local response
|
||||
response=$(curl_post "/api/corpus/e2e-unprimed-test/query" '{"question": "test"}' 30)
|
||||
extract_body_and_status "$response"
|
||||
# Should fail because corpus isn't primed
|
||||
if [[ "$RESPONSE_STATUS" != "200" ]] || echo "$RESPONSE_BODY" | jq -r '.error' 2>/dev/null | grep -qi "prime\|session"; then
|
||||
pass "Query unprimed corpus correctly rejected"
|
||||
else
|
||||
fail "Query unprimed corpus" "expected error about priming, got HTTP $RESPONSE_STATUS"
|
||||
fi
|
||||
# Cleanup
|
||||
curl -s -X DELETE "$WORKER_URL/api/corpus/e2e-unprimed-test" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@@ -212,7 +195,6 @@ test_reprime_corpus() {
|
||||
log "=== Test: Reprime Corpus ==="
|
||||
log " (Creating fresh session...)"
|
||||
|
||||
# Capture old session_id
|
||||
local old_response old_session_id
|
||||
old_response=$(curl_get "/api/corpus/$CORPUS_NAME")
|
||||
extract_body_and_status "$old_response"
|
||||
@@ -260,7 +242,6 @@ test_delete_corpus() {
|
||||
extract_body_and_status "$response"
|
||||
assert_http_status "Delete corpus" "200" "$RESPONSE_STATUS"
|
||||
|
||||
# Verify it's gone
|
||||
local verify_response
|
||||
verify_response=$(curl_get "/api/corpus/$CORPUS_NAME")
|
||||
extract_body_and_status "$verify_response"
|
||||
@@ -275,8 +256,6 @@ test_delete_nonexistent() {
|
||||
assert_http_status "Delete nonexistent returns 404" "404" "$RESPONSE_STATUS"
|
||||
}
|
||||
|
||||
# -- Main ---------------------------------------------------------------------
|
||||
|
||||
main() {
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
log "======================================================"
|
||||
@@ -285,39 +264,32 @@ main() {
|
||||
log "======================================================"
|
||||
log ""
|
||||
|
||||
# Cleanup any leftover test data
|
||||
cleanup_test_corpus
|
||||
|
||||
# Phase 1: Health checks
|
||||
test_worker_health
|
||||
test_worker_readiness
|
||||
log ""
|
||||
|
||||
# Phase 2: CRUD operations
|
||||
test_build_corpus
|
||||
test_list_corpora
|
||||
test_get_corpus
|
||||
test_get_corpus_404
|
||||
log ""
|
||||
|
||||
# Phase 3: Agent SDK operations (prime + query)
|
||||
test_prime_corpus
|
||||
test_query_corpus
|
||||
test_query_without_prime
|
||||
log ""
|
||||
|
||||
# Phase 4: Reprime + query again
|
||||
test_reprime_corpus
|
||||
test_query_after_reprime
|
||||
log ""
|
||||
|
||||
# Phase 5: Rebuild + cleanup
|
||||
test_rebuild_corpus
|
||||
test_delete_corpus
|
||||
test_delete_nonexistent
|
||||
log ""
|
||||
|
||||
# Summary
|
||||
local total=$((PASS_COUNT + FAIL_COUNT))
|
||||
log "======================================================"
|
||||
log " RESULTS: $PASS_COUNT/$total passed, $FAIL_COUNT failed"
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
#!/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);
|
||||
@@ -1,9 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Export memories matching a search query to a portable JSON format
|
||||
* Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]
|
||||
* Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
@@ -19,14 +14,12 @@ import type {
|
||||
|
||||
async function exportMemories(query: string, outputFile: string, project?: string) {
|
||||
try {
|
||||
// Read port from settings
|
||||
const settings = SettingsDefaultsManager.loadFromFile(join(homedir(), '.claude-mem', 'settings.json'));
|
||||
const port = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
||||
const baseUrl = `http://localhost:${port}`;
|
||||
|
||||
console.log(`🔍 Searching for: "${query}"${project ? ` (project: ${project})` : ' (all projects)'}`);
|
||||
|
||||
// Build query params - use format=json for raw data
|
||||
const params = new URLSearchParams({
|
||||
query,
|
||||
format: 'json',
|
||||
@@ -34,7 +27,6 @@ async function exportMemories(query: string, outputFile: string, project?: strin
|
||||
});
|
||||
if (project) params.set('project', project);
|
||||
|
||||
// Unified search - gets all result types using hybrid search
|
||||
console.log('📡 Fetching all memories via hybrid search...');
|
||||
const searchResponse = await fetch(`${baseUrl}/api/search?${params.toString()}`);
|
||||
if (!searchResponse.ok) {
|
||||
@@ -50,7 +42,6 @@ async function exportMemories(query: string, outputFile: string, project?: strin
|
||||
console.log(`✅ Found ${summaries.length} session summaries`);
|
||||
console.log(`✅ Found ${prompts.length} user prompts`);
|
||||
|
||||
// Get unique memory session IDs from observations and summaries
|
||||
const memorySessionIds = new Set<string>();
|
||||
observations.forEach((o) => {
|
||||
if (o.memory_session_id) memorySessionIds.add(o.memory_session_id);
|
||||
@@ -59,7 +50,6 @@ async function exportMemories(query: string, outputFile: string, project?: strin
|
||||
if (s.memory_session_id) memorySessionIds.add(s.memory_session_id);
|
||||
});
|
||||
|
||||
// Get SDK sessions metadata via API
|
||||
console.log('📡 Fetching SDK sessions metadata...');
|
||||
let sessions: SdkSessionRecord[] = [];
|
||||
if (memorySessionIds.size > 0) {
|
||||
@@ -76,7 +66,6 @@ async function exportMemories(query: string, outputFile: string, project?: strin
|
||||
}
|
||||
console.log(`✅ Found ${sessions.length} SDK sessions`);
|
||||
|
||||
// Create export data
|
||||
const exportData: ExportData = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
exportedAtEpoch: Date.now(),
|
||||
@@ -92,7 +81,6 @@ async function exportMemories(query: string, outputFile: string, project?: strin
|
||||
prompts
|
||||
};
|
||||
|
||||
// Write to file
|
||||
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
|
||||
|
||||
console.log(`\n📦 Export complete!`);
|
||||
@@ -109,7 +97,6 @@ async function exportMemories(query: string, outputFile: string, project?: strin
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 2) {
|
||||
console.error('Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]');
|
||||
@@ -118,7 +105,6 @@ if (args.length < 2) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
const [query, outputFile, ...flags] = args;
|
||||
const project = flags.find(f => f.startsWith('--project='))?.split('=')[1];
|
||||
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Extract prompt sections from src/sdk/prompts.ts and generate modes/code.yaml
|
||||
* This ensures the YAML contains the exact same wording as the hardcoded prompts
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read the prompts.ts from main branch (saved to /tmp)
|
||||
const promptsPath = '/tmp/prompts-main.ts';
|
||||
const promptsContent = fs.readFileSync(promptsPath, 'utf-8');
|
||||
|
||||
// Extract buildInitPrompt function content
|
||||
const initPromptMatch = promptsContent.match(/export function buildInitPrompt\([^)]+\): string \{[\s\S]*?return `([\s\S]*?)`;\s*\}/);
|
||||
if (!initPromptMatch) {
|
||||
console.error('Could not find buildInitPrompt function');
|
||||
process.exit(1);
|
||||
}
|
||||
const initPrompt = initPromptMatch[1];
|
||||
|
||||
// Extract sections from buildInitPrompt
|
||||
// Line 41: observer_role starts with "Your job is to monitor..."
|
||||
const observerRoleMatch = initPrompt.match(/Your job is to monitor[^\n]*\n\n(?:SPATIAL AWARENESS:[\s\S]*?\n\n)?/);
|
||||
const observerRole = observerRoleMatch ? observerRoleMatch[0].replace(/\n\n$/, '') : '';
|
||||
|
||||
// Extract recording_focus (WHAT TO RECORD section)
|
||||
const recordingFocusMatch = initPrompt.match(/WHAT TO RECORD\n-{14}\n([\s\S]*?)(?=\n\nWHEN TO SKIP)/);
|
||||
const recordingFocus = recordingFocusMatch ? `WHAT TO RECORD\n--------------\n${recordingFocusMatch[1]}` : '';
|
||||
|
||||
// Extract skip_guidance (WHEN TO SKIP section)
|
||||
const skipGuidanceMatch = initPrompt.match(/WHEN TO SKIP\n-{12}\n([\s\S]*?)(?=\n\nOUTPUT FORMAT)/);
|
||||
const skipGuidance = skipGuidanceMatch ? `WHEN TO SKIP\n------------\n${skipGuidanceMatch[1]}` : '';
|
||||
|
||||
// Extract type_guidance (from XML comment)
|
||||
const typeGuidanceMatch = initPrompt.match(/<!--\n\s+\*\*type\*\*: MUST be EXACTLY[^\n]*\n([\s\S]*?)-->/);
|
||||
const typeGuidance = typeGuidanceMatch ? typeGuidanceMatch[0].replace(/<!--\n\s+/, '').replace(/\s+-->/, '').trim() : '';
|
||||
|
||||
// Extract field_guidance (facts AND files comments combined)
|
||||
const factsMatch = initPrompt.match(/\*\*facts\*\*: Concise[^\n]*\n([\s\S]*?)(?=\n -->)/);
|
||||
const filesMatch = initPrompt.match(/\*\*files\*\*:[^\n]*\n/);
|
||||
|
||||
const factsText = factsMatch ? `**facts**: Concise, self-contained statements\n${factsMatch[1].trim()}` : '';
|
||||
const filesText = filesMatch ? filesMatch[0].trim() : '**files**: All files touched (full paths from project root)';
|
||||
|
||||
const fieldGuidance = `${factsText}\n\n${filesText}`;
|
||||
|
||||
// Extract concept_guidance (concepts comment)
|
||||
const conceptGuidanceMatch = initPrompt.match(/<!--\n\s+\*\*concepts\*\*: 2-5 knowledge[^\n]*\n([\s\S]*?)-->/);
|
||||
const conceptGuidance = conceptGuidanceMatch ? conceptGuidanceMatch[0].replace(/<!--\n\s+/, '').replace(/\s+-->/, '').trim() : '';
|
||||
|
||||
// Build the JSON content
|
||||
const jsonData = {
|
||||
name: "Code Development",
|
||||
description: "Software development and engineering work",
|
||||
version: "1.0.0",
|
||||
observation_types: [
|
||||
{ id: "bugfix", label: "Bug Fix", description: "Something was broken, now fixed", emoji: "🔴", work_emoji: "🛠️" },
|
||||
{ id: "feature", label: "Feature", description: "New capability or functionality added", emoji: "🟣", work_emoji: "🛠️" },
|
||||
{ id: "refactor", label: "Refactor", description: "Code restructured, behavior unchanged", emoji: "🔄", work_emoji: "🛠️" },
|
||||
{ id: "change", label: "Change", description: "Generic modification (docs, config, misc)", emoji: "✅", work_emoji: "🛠️" },
|
||||
{ id: "discovery", label: "Discovery", description: "Learning about existing system", emoji: "🔵", work_emoji: "🔍" },
|
||||
{ id: "decision", label: "Decision", description: "Architectural/design choice with rationale", emoji: "⚖️", work_emoji: "⚖️" }
|
||||
],
|
||||
observation_concepts: [
|
||||
{ id: "how-it-works", label: "How It Works", description: "Understanding mechanisms" },
|
||||
{ id: "why-it-exists", label: "Why It Exists", description: "Purpose or rationale" },
|
||||
{ id: "what-changed", label: "What Changed", description: "Modifications made" },
|
||||
{ id: "problem-solution", label: "Problem-Solution", description: "Issues and their fixes" },
|
||||
{ id: "gotcha", label: "Gotcha", description: "Traps or edge cases" },
|
||||
{ id: "pattern", label: "Pattern", description: "Reusable approach" },
|
||||
{ id: "trade-off", label: "Trade-Off", description: "Pros/cons of a decision" }
|
||||
],
|
||||
prompts: {
|
||||
observer_role: observerRole,
|
||||
recording_focus: recordingFocus,
|
||||
skip_guidance: skipGuidance,
|
||||
type_guidance: typeGuidance,
|
||||
concept_guidance: conceptGuidance,
|
||||
field_guidance: fieldGuidance,
|
||||
format_examples: ""
|
||||
}
|
||||
};
|
||||
|
||||
// OLD YAML BUILD:
|
||||
const yamlContent_OLD = `name: "Code Development"
|
||||
description: "Software development and engineering work"
|
||||
version: "1.0.0"
|
||||
|
||||
observation_types:
|
||||
- id: "bugfix"
|
||||
label: "Bug Fix"
|
||||
description: "Something was broken, now fixed"
|
||||
emoji: "🔴"
|
||||
work_emoji: "🛠️"
|
||||
- id: "feature"
|
||||
label: "Feature"
|
||||
description: "New capability or functionality added"
|
||||
emoji: "🟣"
|
||||
work_emoji: "🛠️"
|
||||
- id: "refactor"
|
||||
label: "Refactor"
|
||||
description: "Code restructured, behavior unchanged"
|
||||
emoji: "🔄"
|
||||
work_emoji: "🛠️"
|
||||
- id: "change"
|
||||
label: "Change"
|
||||
description: "Generic modification (docs, config, misc)"
|
||||
emoji: "✅"
|
||||
work_emoji: "🛠️"
|
||||
- id: "discovery"
|
||||
label: "Discovery"
|
||||
description: "Learning about existing system"
|
||||
emoji: "🔵"
|
||||
work_emoji: "🔍"
|
||||
- id: "decision"
|
||||
label: "Decision"
|
||||
description: "Architectural/design choice with rationale"
|
||||
emoji: "⚖️"
|
||||
work_emoji: "⚖️"
|
||||
|
||||
observation_concepts:
|
||||
- id: "how-it-works"
|
||||
label: "How It Works"
|
||||
description: "Understanding mechanisms"
|
||||
- id: "why-it-exists"
|
||||
label: "Why It Exists"
|
||||
description: "Purpose or rationale"
|
||||
- id: "what-changed"
|
||||
label: "What Changed"
|
||||
description: "Modifications made"
|
||||
- id: "problem-solution"
|
||||
label: "Problem-Solution"
|
||||
description: "Issues and their fixes"
|
||||
- id: "gotcha"
|
||||
label: "Gotcha"
|
||||
description: "Traps or edge cases"
|
||||
- id: "pattern"
|
||||
label: "Pattern"
|
||||
description: "Reusable approach"
|
||||
- id: "trade-off"
|
||||
label: "Trade-Off"
|
||||
description: "Pros/cons of a decision"
|
||||
|
||||
prompts:
|
||||
observer_role: |
|
||||
${observerRole}
|
||||
|
||||
recording_focus: |
|
||||
${recordingFocus}
|
||||
|
||||
skip_guidance: |
|
||||
${skipGuidance}
|
||||
|
||||
type_guidance: |
|
||||
${typeGuidance}
|
||||
|
||||
concept_guidance: |
|
||||
${conceptGuidance}
|
||||
|
||||
field_guidance: |
|
||||
${fieldGuidance}
|
||||
|
||||
format_examples: ""
|
||||
`;
|
||||
|
||||
// Write to modes/code.json
|
||||
const outputPath = path.join(__dirname, '../modes/code.json');
|
||||
fs.writeFileSync(outputPath, JSON.stringify(jsonData, null, 2), 'utf-8');
|
||||
|
||||
console.log('✅ Generated modes/code.json from prompts.ts');
|
||||
console.log('\nExtracted sections:');
|
||||
console.log('- observer_role:', observerRole.substring(0, 50) + '...');
|
||||
console.log('- recording_focus:', recordingFocus.substring(0, 50) + '...');
|
||||
console.log('- skip_guidance:', skipGuidance.substring(0, 50) + '...');
|
||||
console.log('- type_guidance:', typeGuidance.substring(0, 50) + '...');
|
||||
console.log('- concept_guidance:', conceptGuidance.substring(0, 50) + '...');
|
||||
console.log('- field_guidance:', fieldGuidance.substring(0, 50) + '...');
|
||||
@@ -1,177 +0,0 @@
|
||||
#!/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`);
|
||||
@@ -1,82 +0,0 @@
|
||||
# XML Extraction Scripts
|
||||
|
||||
Scripts to extract XML observations and summaries from Claude Code transcript files.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `filter-actual-xml.py`
|
||||
**Recommended for import**
|
||||
|
||||
Extracts only actual XML from assistant responses, filtering out:
|
||||
- Template/example XML (with placeholders like `[...]` or `**field**:`)
|
||||
- XML from tool_use blocks
|
||||
- XML from user messages
|
||||
|
||||
**Output:** `~/Scripts/claude-mem/actual_xml_only_with_timestamps.xml`
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python3 scripts/extraction/filter-actual-xml.py
|
||||
```
|
||||
|
||||
### `extract-all-xml.py`
|
||||
**For debugging/analysis**
|
||||
|
||||
Extracts ALL XML blocks from transcripts without filtering.
|
||||
|
||||
**Output:** `~/Scripts/claude-mem/all_xml_fragments_with_timestamps.xml`
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python3 scripts/extraction/extract-all-xml.py
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Extract XML from transcripts:**
|
||||
```bash
|
||||
cd ~/Scripts/claude-mem
|
||||
python3 scripts/extraction/filter-actual-xml.py
|
||||
```
|
||||
|
||||
2. **Import to database:**
|
||||
```bash
|
||||
npm run import:xml
|
||||
```
|
||||
|
||||
3. **Clean up duplicates (if needed):**
|
||||
```bash
|
||||
npm run cleanup:duplicates
|
||||
```
|
||||
|
||||
## Source Data
|
||||
|
||||
Scripts read from: `~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/*.jsonl`
|
||||
|
||||
These are Claude Code session transcripts stored in JSONL (JSON Lines) format.
|
||||
|
||||
## Output Format
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<transcript_extracts>
|
||||
|
||||
<!-- Block 1 | 2025-10-19 03:03:23 UTC -->
|
||||
<observation>
|
||||
<type>discovery</type>
|
||||
<title>Example observation</title>
|
||||
...
|
||||
</observation>
|
||||
|
||||
<!-- Block 2 | 2025-10-19 03:03:45 UTC -->
|
||||
<summary>
|
||||
<request>What was accomplished</request>
|
||||
...
|
||||
</summary>
|
||||
|
||||
</transcript_extracts>
|
||||
```
|
||||
|
||||
Each XML block includes a comment with:
|
||||
- Block number
|
||||
- Original timestamp from transcript
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
def extract_xml_blocks(text):
|
||||
"""Extract complete XML blocks from text"""
|
||||
xml_patterns = [
|
||||
r'<observation>.*?</observation>',
|
||||
r'<session_summary>.*?</session_summary>',
|
||||
r'<request>.*?</request>',
|
||||
r'<summary>.*?</summary>',
|
||||
r'<facts>.*?</facts>',
|
||||
r'<fact>.*?</fact>',
|
||||
r'<concepts>.*?</concepts>',
|
||||
r'<concept>.*?</concept>',
|
||||
r'<files>.*?</files>',
|
||||
r'<file>.*?</file>',
|
||||
r'<files_read>.*?</files_read>',
|
||||
r'<files_edited>.*?</files_edited>',
|
||||
r'<files_modified>.*?</files_modified>',
|
||||
r'<narrative>.*?</narrative>',
|
||||
r'<learned>.*?</learned>',
|
||||
r'<investigated>.*?</investigated>',
|
||||
r'<completed>.*?</completed>',
|
||||
r'<next_steps>.*?</next_steps>',
|
||||
r'<notes>.*?</notes>',
|
||||
r'<title>.*?</title>',
|
||||
r'<subtitle>.*?</subtitle>',
|
||||
r'<text>.*?</text>',
|
||||
r'<type>.*?</type>',
|
||||
]
|
||||
|
||||
blocks = []
|
||||
for pattern in xml_patterns:
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
blocks.extend(matches)
|
||||
|
||||
return blocks
|
||||
|
||||
def process_transcript_file(filepath):
|
||||
"""Process a single transcript file and extract XML with timestamps"""
|
||||
results = []
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
# Get timestamp
|
||||
timestamp = data.get('timestamp', 'unknown')
|
||||
|
||||
# Extract text content from message
|
||||
message = data.get('message', {})
|
||||
content = message.get('content', [])
|
||||
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, dict):
|
||||
text = ''
|
||||
if item.get('type') == 'text':
|
||||
text = item.get('text', '')
|
||||
elif item.get('type') == 'tool_use':
|
||||
# Also check tool_use input fields
|
||||
tool_input = item.get('input', {})
|
||||
if isinstance(tool_input, dict):
|
||||
text = str(tool_input)
|
||||
|
||||
if text:
|
||||
# Extract XML blocks
|
||||
xml_blocks = extract_xml_blocks(text)
|
||||
|
||||
for block in xml_blocks:
|
||||
results.append({
|
||||
'timestamp': timestamp,
|
||||
'xml': block
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
# Get list of transcript files
|
||||
transcript_dir = os.path.expanduser('~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/')
|
||||
os.chdir(transcript_dir)
|
||||
|
||||
# Get all transcript files sorted by modification time
|
||||
result = subprocess.run(['ls', '-t'], capture_output=True, text=True)
|
||||
files = [f for f in result.stdout.strip().split('\n') if f.endswith('.jsonl')][:62]
|
||||
|
||||
all_results = []
|
||||
for filename in files:
|
||||
filepath = os.path.join(transcript_dir, filename)
|
||||
print(f"Processing {filename}...")
|
||||
results = process_transcript_file(filepath)
|
||||
all_results.extend(results)
|
||||
print(f" Found {len(results)} XML blocks")
|
||||
|
||||
# Write results with timestamps
|
||||
output_file = os.path.expanduser('~/Scripts/claude-mem/all_xml_fragments_with_timestamps.xml')
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||
f.write('<transcript_extracts>\n\n')
|
||||
|
||||
for i, item in enumerate(all_results, 1):
|
||||
timestamp = item['timestamp']
|
||||
xml = item['xml']
|
||||
|
||||
# Format timestamp nicely if it's ISO format
|
||||
if timestamp != 'unknown' and timestamp:
|
||||
try:
|
||||
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
||||
formatted_time = dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
except:
|
||||
formatted_time = timestamp
|
||||
else:
|
||||
formatted_time = 'unknown'
|
||||
|
||||
f.write(f'<!-- Block {i} | {formatted_time} -->\n')
|
||||
f.write(xml)
|
||||
f.write('\n\n')
|
||||
|
||||
f.write('</transcript_extracts>\n')
|
||||
|
||||
print(f"\nExtracted {len(all_results)} XML blocks with timestamps to {output_file}")
|
||||
@@ -1,168 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
def extract_xml_blocks(text):
|
||||
"""Extract complete XML blocks from text"""
|
||||
xml_patterns = [
|
||||
r'<observation>.*?</observation>',
|
||||
r'<session_summary>.*?</session_summary>',
|
||||
r'<request>.*?</request>',
|
||||
r'<summary>.*?</summary>',
|
||||
r'<facts>.*?</facts>',
|
||||
r'<fact>.*?</fact>',
|
||||
r'<concepts>.*?</concepts>',
|
||||
r'<concept>.*?</concept>',
|
||||
r'<files>.*?</files>',
|
||||
r'<file>.*?</file>',
|
||||
r'<files_read>.*?</files_read>',
|
||||
r'<files_edited>.*?</files_edited>',
|
||||
r'<files_modified>.*?</files_modified>',
|
||||
r'<narrative>.*?</narrative>',
|
||||
r'<learned>.*?</learned>',
|
||||
r'<investigated>.*?</investigated>',
|
||||
r'<completed>.*?</completed>',
|
||||
r'<next_steps>.*?</next_steps>',
|
||||
r'<notes>.*?</notes>',
|
||||
r'<title>.*?</title>',
|
||||
r'<subtitle>.*?</subtitle>',
|
||||
r'<text>.*?</text>',
|
||||
r'<type>.*?</type>',
|
||||
r'<tool_used>.*?</tool_used>',
|
||||
r'<tool_name>.*?</tool_name>',
|
||||
r'<tool_input>.*?</tool_input>',
|
||||
r'<tool_output>.*?</tool_output>',
|
||||
r'<tool_time>.*?</tool_time>',
|
||||
]
|
||||
|
||||
blocks = []
|
||||
for pattern in xml_patterns:
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
blocks.extend(matches)
|
||||
|
||||
return blocks
|
||||
|
||||
def is_example_xml(xml_block):
|
||||
"""Check if XML block is an example/template"""
|
||||
# Patterns that indicate this is example/template XML
|
||||
example_indicators = [
|
||||
r'\[.*?\]', # Square brackets with placeholders
|
||||
r'\*\*\w+\*\*:', # Bold markdown like **title**:
|
||||
r'\.\.\..*?\.\.\.', # Ellipsis indicating placeholder
|
||||
r'feature\|bugfix\|refactor', # Multiple options separated by |
|
||||
r'change \| discovery \| decision', # Example types
|
||||
r'\{.*?\}', # Curly braces (template variables)
|
||||
r'Concise, self-contained statement', # Literal example text
|
||||
r'Short title capturing',
|
||||
r'One sentence explanation',
|
||||
r'What was the user trying',
|
||||
r'What code/systems did you explore',
|
||||
r'What did you learn',
|
||||
r'What was done',
|
||||
r'What should happen next',
|
||||
r'file1\.ts', # Example filenames
|
||||
r'file2\.ts',
|
||||
r'file3\.ts',
|
||||
r'Any additional context',
|
||||
]
|
||||
|
||||
for pattern in example_indicators:
|
||||
if re.search(pattern, xml_block):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def process_transcript_file(filepath):
|
||||
"""Process a single transcript file and extract only real XML from assistant responses"""
|
||||
results = []
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
# Get timestamp
|
||||
timestamp = data.get('timestamp', 'unknown')
|
||||
|
||||
# Only process assistant messages
|
||||
message = data.get('message', {})
|
||||
role = message.get('role')
|
||||
|
||||
if role != 'assistant':
|
||||
continue
|
||||
|
||||
content = message.get('content', [])
|
||||
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get('type') == 'text':
|
||||
# This is text in an assistant response, not tool_use
|
||||
text = item.get('text', '')
|
||||
|
||||
# Extract XML blocks
|
||||
xml_blocks = extract_xml_blocks(text)
|
||||
|
||||
for block in xml_blocks:
|
||||
# Filter out example/template XML
|
||||
if not is_example_xml(block):
|
||||
results.append({
|
||||
'timestamp': timestamp,
|
||||
'xml': block
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
# Get list of Oct 18 transcript files
|
||||
import subprocess
|
||||
|
||||
transcript_dir = os.path.expanduser('~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/')
|
||||
os.chdir(transcript_dir)
|
||||
|
||||
# Get all transcript files sorted by modification time
|
||||
result = subprocess.run(['ls', '-t'], capture_output=True, text=True)
|
||||
files = [f for f in result.stdout.strip().split('\n') if f.endswith('.jsonl')][:62]
|
||||
|
||||
all_results = []
|
||||
for filename in files:
|
||||
filepath = os.path.join(transcript_dir, filename)
|
||||
print(f"Processing {filename}...")
|
||||
results = process_transcript_file(filepath)
|
||||
all_results.extend(results)
|
||||
print(f" Found {len(results)} actual XML blocks")
|
||||
|
||||
# Write results with timestamps
|
||||
output_file = os.path.expanduser('~/Scripts/claude-mem/actual_xml_only_with_timestamps.xml')
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||
f.write('<!-- Actual XML blocks from assistant responses only -->\n')
|
||||
f.write('<!-- Excludes: tool_use inputs, user prompts, and example/template XML -->\n')
|
||||
f.write('<transcript_extracts>\n\n')
|
||||
|
||||
for i, item in enumerate(all_results, 1):
|
||||
timestamp = item['timestamp']
|
||||
xml = item['xml']
|
||||
|
||||
# Format timestamp nicely if it's ISO format
|
||||
if timestamp != 'unknown' and timestamp:
|
||||
try:
|
||||
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
||||
formatted_time = dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
except:
|
||||
formatted_time = timestamp
|
||||
else:
|
||||
formatted_time = 'unknown'
|
||||
|
||||
f.write(f'<!-- Block {i} | {formatted_time} -->\n')
|
||||
f.write(xml)
|
||||
f.write('\n\n')
|
||||
|
||||
f.write('</transcript_extracts>\n')
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Extracted {len(all_results)} actual XML blocks (filtered) to {output_file}")
|
||||
print(f"{'='*80}")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user