@wipcomputer/memory-crystal 0.7.28 → 0.7.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/.env.example +20 -0
  2. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/.publish-skill.json +1 -0
  3. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/CHANGELOG.md +1297 -0
  4. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/CLA.md +19 -0
  5. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/LICENSE +52 -0
  6. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/README-ENTERPRISE.md +226 -0
  7. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/README.md +151 -0
  8. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/RELAY.md +199 -0
  9. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/SKILL.md +462 -0
  10. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/TECHNICAL.md +656 -0
  11. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/_trash/RELEASE-NOTES-v0-7-23.md +48 -0
  12. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/_trash/RELEASE-NOTES-v0-7-25.md +24 -0
  13. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/_trash/RELEASE-NOTES-v0-7-26.md +7 -0
  14. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/_trash/RELEASE-NOTES-v0-7-28.md +31 -0
  15. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/_trash/RELEASE-NOTES-v0-7-29.md +28 -0
  16. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/_trash/RELEASE-NOTES-v0-7-4.md +64 -0
  17. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/_trash/RELEASE-NOTES-v0-7-5.md +19 -0
  18. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/cloud/README.md +116 -0
  19. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/cloud/docs/gpt-system-instructions.md +69 -0
  20. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/cloud/migrations/0001_init.sql +52 -0
  21. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/migrations/0001_init.sql +51 -0
  22. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/migrations/0002_cloud_storage.sql +49 -0
  23. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/openclaw.plugin.json +11 -0
  24. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/package-lock.json +4169 -0
  25. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/package.json +61 -0
  26. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/scripts/crystal-capture.sh +29 -0
  27. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/scripts/deploy-cloud.sh +153 -0
  28. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/scripts/ldm-backup.sh +116 -0
  29. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/scripts/migrate-lance-to-sqlite.mjs +218 -0
  30. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/skills/memory/SKILL.md +438 -0
  31. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/wrangler-demo.toml +8 -0
  32. package/.worktrees/memory-crystal-private--cc-mini-fix-home-fallback/wrangler-mcp.toml +24 -0
  33. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/.env.example +20 -0
  34. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/.publish-skill.json +1 -0
  35. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/CHANGELOG.md +1297 -0
  36. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/CLA.md +19 -0
  37. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/LICENSE +52 -0
  38. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/README-ENTERPRISE.md +226 -0
  39. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/README.md +151 -0
  40. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/RELAY.md +199 -0
  41. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/RELEASE-NOTES-v0.7.30.md +29 -0
  42. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/SKILL.md +462 -0
  43. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/TECHNICAL.md +656 -0
  44. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/_trash/RELEASE-NOTES-v0-7-23.md +48 -0
  45. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/_trash/RELEASE-NOTES-v0-7-25.md +24 -0
  46. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/_trash/RELEASE-NOTES-v0-7-26.md +7 -0
  47. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/_trash/RELEASE-NOTES-v0-7-28.md +31 -0
  48. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/_trash/RELEASE-NOTES-v0-7-29.md +28 -0
  49. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/_trash/RELEASE-NOTES-v0-7-4.md +64 -0
  50. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/_trash/RELEASE-NOTES-v0-7-5.md +19 -0
  51. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/cloud/README.md +116 -0
  52. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/cloud/docs/gpt-system-instructions.md +69 -0
  53. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/cloud/migrations/0001_init.sql +52 -0
  54. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/migrations/0001_init.sql +51 -0
  55. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/migrations/0002_cloud_storage.sql +49 -0
  56. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/openclaw.plugin.json +11 -0
  57. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/package-lock.json +4169 -0
  58. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/package.json +61 -0
  59. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/scripts/crystal-capture.sh +29 -0
  60. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/scripts/deploy-cloud.sh +153 -0
  61. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/scripts/ldm-backup.sh +116 -0
  62. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/scripts/migrate-lance-to-sqlite.mjs +218 -0
  63. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/skills/memory/SKILL.md +438 -0
  64. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/wrangler-demo.toml +8 -0
  65. package/.worktrees/memory-crystal-private--cc-mini-release-notes-v0.7.30/wrangler-mcp.toml +24 -0
  66. package/CHANGELOG.md +63 -0
  67. package/SKILL.md +13 -3
  68. package/TECHNICAL.md +30 -2
  69. package/_trash/RELEASE-NOTES-v0-7-28.md +15 -8
  70. package/_trash/RELEASE-NOTES-v0-7-29.md +28 -0
  71. package/_trash/RELEASE-NOTES-v0.7.30.md +29 -0
  72. package/package.json +1 -1
  73. package/scripts/migrate-lance-to-sqlite.mjs +2 -1
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@wipcomputer/memory-crystal",
3
+ "version": "0.7.29",
4
+ "description": "Sovereign memory system — local-first with ephemeral encrypted relay. Your memory, your machine, your rules.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/wipcomputer/memory-crystal-private.git"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/core.js",
11
+ "openclaw": {
12
+ "extensions": [
13
+ "./dist/openclaw.js"
14
+ ]
15
+ },
16
+ "bin": {
17
+ "crystal": "dist/cli.js",
18
+ "crystal-mcp": "dist/mcp-server.js"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/core.ts src/cli.ts src/mcp-server.ts src/openclaw.ts src/migrate.ts src/cc-hook.ts src/cc-poller.ts src/crypto.ts src/pair.ts src/poller.ts src/mirror-sync.ts src/file-sync.ts src/ldm.ts src/summarize.ts src/role.ts src/doctor.ts src/bridge.ts src/discover.ts src/bulk-copy.ts src/oc-backfill.ts src/dream-weaver.ts src/crystal-serve.ts src/staging.ts src/installer.ts --format esm --dts --outDir dist && tsup src/worker.ts --format esm --outDir dist --no-dts && cp scripts/crystal-capture.sh scripts/ldm-backup.sh dist/",
22
+ "build:local": "tsup src/core.ts src/cli.ts src/mcp-server.ts src/openclaw.ts src/migrate.ts src/cc-hook.ts src/cc-poller.ts src/crypto.ts src/pair.ts src/poller.ts src/mirror-sync.ts src/file-sync.ts src/ldm.ts src/summarize.ts src/role.ts src/doctor.ts src/bridge.ts src/discover.ts src/bulk-copy.ts src/oc-backfill.ts src/dream-weaver.ts src/crystal-serve.ts src/staging.ts src/installer.ts --format esm --dts --outDir dist",
23
+ "build:worker": "tsup src/worker.ts --format esm --outDir dist --no-dts",
24
+ "build:cloud": "tsup src/worker-mcp.ts src/cloud-crystal.ts --format esm --outDir dist --no-dts",
25
+ "deploy:cloud": "bash -c 'git diff --quiet HEAD -- src/ wrangler-mcp.toml || (echo \"ERROR: uncommitted changes. commit before deploying.\" && exit 1)' && wrangler deploy --config wrangler-mcp.toml",
26
+ "build:demo": "tsup src/worker-demo.ts --format esm --outDir dist --no-dts",
27
+ "dev:demo": "wrangler dev --config wrangler-demo.toml",
28
+ "deploy:demo": "bash -c 'git diff --quiet HEAD -- src/ wrangler-demo.toml || (echo \"ERROR: uncommitted changes. commit before deploying.\" && exit 1)' && wrangler deploy --config wrangler-demo.toml",
29
+ "dev": "tsup src/core.ts src/cli.ts src/mcp-server.ts src/openclaw.ts src/migrate.ts src/cc-hook.ts src/crypto.ts --format esm --watch --outDir dist",
30
+ "check": "node dist/cli.js status",
31
+ "search": "node dist/cli.js search",
32
+ "migrate": "node dist/migrate.js",
33
+ "cloud:dev": "cd cloud && npx wrangler dev",
34
+ "cloud:deploy": "cd cloud && npx wrangler deploy",
35
+ "cloud:db:migrate": "cd cloud && npx wrangler d1 migrations apply memory-crystal-cloud --local",
36
+ "cloud:db:migrate:remote": "cd cloud && npx wrangler d1 migrations apply memory-crystal-cloud --remote"
37
+ },
38
+ "dependencies": {
39
+ "dream-weaver-protocol": "file:../dream-weaver-protocol-private",
40
+ "@lancedb/lancedb": "^0.15.0",
41
+ "@modelcontextprotocol/sdk": "^1.12.1",
42
+ "agents": "^0.7.2",
43
+ "apache-arrow": "^18.1.0",
44
+ "better-sqlite3": "^11.8.1",
45
+ "qrcode-terminal": "^0.12.0",
46
+ "sqlite-vec": "^0.1.7-alpha.2",
47
+ "zod": "^4.3.6"
48
+ },
49
+ "optionalDependencies": {
50
+ "sqlite-vec-darwin-arm64": "^0.1.7-alpha.2"
51
+ },
52
+ "devDependencies": {
53
+ "@cloudflare/workers-types": "^4.20260228.1",
54
+ "@types/better-sqlite3": "^7.6.13",
55
+ "@types/node": "^22.0.0",
56
+ "@types/qrcode-terminal": "^0.12.2",
57
+ "tsup": "^8.0.0",
58
+ "typescript": "^5.7.0",
59
+ "wrangler": "^3.95.0"
60
+ }
61
+ }
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+ # Job: crystal-capture
3
+ # Continuous capture for Claude Code sessions.
4
+ # Reads JSONL files on disk, ingests into Crystal, exports MD sessions, writes daily logs.
5
+ # Primary capture path. Runs every minute via cron.
6
+ # The Stop hook (cc-hook.ts) is a redundancy check only.
7
+ #
8
+ # Source of truth: memory-crystal-private/scripts/crystal-capture.sh
9
+ # Deployed to: ~/.ldm/bin/crystal-capture.sh (via crystal init)
10
+ # Cron entry: * * * * * ~/.ldm/bin/crystal-capture.sh >> /tmp/ldm-dev-tools/crystal-capture.log 2>&1
11
+ #
12
+ # The Node poller fetches the OpenAI API key internally via opRead() in core.ts.
13
+ # opRead uses: op read "op://Agent Secrets/OpenAI API/api key" with the SA token from
14
+ # ~/.openclaw/secrets/op-sa-token. Do NOT call op from this shell script... it triggers
15
+ # macOS TCC popups when run from cron.
16
+
17
+ # Cron provides minimal PATH. Ensure Homebrew binaries (node, op) are findable.
18
+ export PATH="/opt/homebrew/bin:$PATH"
19
+
20
+ POLLER="$HOME/.ldm/extensions/memory-crystal/dist/cc-poller.js"
21
+ NODE="/opt/homebrew/bin/node"
22
+
23
+ if [ ! -f "$POLLER" ]; then
24
+ echo "ERROR: cc-poller not found at $POLLER"
25
+ exit 1
26
+ fi
27
+
28
+ # Single run: scan all sessions, ingest new turns, export MD, exit.
29
+ $NODE "$POLLER" 2>&1
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # deploy-cloud.sh — Deploy Memory Crystal Cloud MCP server to Cloudflare.
4
+ # Pulls all credentials from 1Password. No keys in env files.
5
+ #
6
+ # Usage:
7
+ # bash scripts/deploy-cloud.sh # full setup (first time)
8
+ # bash scripts/deploy-cloud.sh deploy # just redeploy Worker code
9
+ #
10
+ # Prerequisites:
11
+ # - wrangler CLI installed (npm install -g wrangler)
12
+ # - 1Password items populated:
13
+ # "Parker - Cloudflare Memory Crystal Keys" (api-token, account-id)
14
+ # "OpenAI API" (api key)
15
+
16
+ set -euo pipefail
17
+
18
+ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
19
+ cd "$REPO_DIR"
20
+
21
+ # ── Pull credentials from 1Password ──
22
+
23
+ echo "Pulling credentials from 1Password..."
24
+
25
+ OP_TOKEN=$(cat ~/.openclaw/secrets/op-sa-token)
26
+
27
+ CF_API_TOKEN=$(OP_SERVICE_ACCOUNT_TOKEN="$OP_TOKEN" op item get "Parker - Cloudflare Memory Crystal Keys" \
28
+ --vault="Agent Secrets" --fields label=api-token --reveal)
29
+
30
+ CF_ACCOUNT_ID=$(OP_SERVICE_ACCOUNT_TOKEN="$OP_TOKEN" op item get "Parker - Cloudflare Memory Crystal Keys" \
31
+ --vault="Agent Secrets" --fields label=account-id --reveal)
32
+
33
+ OPENAI_API_KEY=$(OP_SERVICE_ACCOUNT_TOKEN="$OP_TOKEN" op item get "OpenAI API" \
34
+ --vault="Agent Secrets" --fields label="api key" --reveal)
35
+
36
+ if [[ "$CF_API_TOKEN" == "REPLACE_WITH_CLOUDFLARE_API_TOKEN" || "$CF_ACCOUNT_ID" == "REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID" ]]; then
37
+ echo "Error: Cloudflare credentials not yet filled in 1Password."
38
+ echo "Update 'Parker - Cloudflare Memory Crystal Keys' in Agent Secrets vault."
39
+ exit 1
40
+ fi
41
+
42
+ export CLOUDFLARE_API_TOKEN="$CF_API_TOKEN"
43
+ export CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT_ID"
44
+
45
+ echo " Cloudflare Account ID: ${CF_ACCOUNT_ID:0:8}..."
46
+ echo " Cloudflare API Token: ${CF_API_TOKEN:0:8}..."
47
+ echo " OpenAI API Key: ${OPENAI_API_KEY:0:8}..."
48
+
49
+ # ── Deploy only? ──
50
+
51
+ if [[ "${1:-}" == "deploy" ]]; then
52
+ echo ""
53
+ echo "Building and deploying Worker..."
54
+ npm run build:cloud
55
+ npx wrangler deploy --config wrangler-mcp.toml
56
+ echo "Done. Worker deployed."
57
+ exit 0
58
+ fi
59
+
60
+ # ── Full setup (first time) ──
61
+
62
+ echo ""
63
+ echo "=== Step 1: Create D1 database ==="
64
+
65
+ # Check if database already exists
66
+ DB_ID=$(npx wrangler d1 list --json 2>/dev/null | python3 -c "
67
+ import sys, json
68
+ dbs = json.load(sys.stdin)
69
+ for db in dbs:
70
+ if db['name'] == 'memory-crystal-cloud':
71
+ print(db['uuid'])
72
+ break
73
+ " 2>/dev/null || echo "")
74
+
75
+ if [[ -z "$DB_ID" ]]; then
76
+ echo "Creating D1 database: memory-crystal-cloud"
77
+ DB_OUTPUT=$(npx wrangler d1 create memory-crystal-cloud 2>&1)
78
+ DB_ID=$(echo "$DB_OUTPUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)
79
+ echo " Created: $DB_ID"
80
+ else
81
+ echo " Already exists: $DB_ID"
82
+ fi
83
+
84
+ if [[ -z "$DB_ID" ]]; then
85
+ echo "Error: Could not get D1 database ID"
86
+ exit 1
87
+ fi
88
+
89
+ # Update wrangler-mcp.toml with database ID
90
+ if grep -q 'database_id = ""' wrangler-mcp.toml; then
91
+ sed -i.bak "s/database_id = \"\"/database_id = \"$DB_ID\"/" wrangler-mcp.toml
92
+ rm -f wrangler-mcp.toml.bak
93
+ echo " Updated wrangler-mcp.toml with database_id"
94
+ fi
95
+
96
+ echo ""
97
+ echo "=== Step 2: Create Vectorize index ==="
98
+
99
+ VEC_EXISTS=$(npx wrangler vectorize list --json 2>/dev/null | python3 -c "
100
+ import sys, json
101
+ indexes = json.load(sys.stdin)
102
+ for idx in indexes:
103
+ if idx['name'] == 'memory-crystal-chunks':
104
+ print('yes')
105
+ break
106
+ " 2>/dev/null || echo "")
107
+
108
+ if [[ "$VEC_EXISTS" != "yes" ]]; then
109
+ echo "Creating Vectorize index: memory-crystal-chunks (1024 dims, cosine)"
110
+ npx wrangler vectorize create memory-crystal-chunks --dimensions 1024 --metric cosine
111
+ echo " Created."
112
+ else
113
+ echo " Already exists."
114
+ fi
115
+
116
+ echo ""
117
+ echo "=== Step 3: Run D1 migrations ==="
118
+
119
+ npx wrangler d1 migrations apply memory-crystal-cloud --config wrangler-mcp.toml
120
+ echo " Migrations applied."
121
+
122
+ echo ""
123
+ echo "=== Step 4: Set Worker secrets ==="
124
+
125
+ echo "$OPENAI_API_KEY" | npx wrangler secret put OPENAI_API_KEY --config wrangler-mcp.toml
126
+ echo " OPENAI_API_KEY set."
127
+
128
+ # Generate signing key for OAuth tokens
129
+ MCP_SIGNING_KEY=$(openssl rand -hex 32)
130
+ echo "$MCP_SIGNING_KEY" | npx wrangler secret put MCP_SIGNING_KEY --config wrangler-mcp.toml
131
+ echo " MCP_SIGNING_KEY set (generated)."
132
+
133
+ # Generate relay encryption key (base64, 32 bytes)
134
+ RELAY_KEY=$(openssl rand -base64 32)
135
+ echo "$RELAY_KEY" | npx wrangler secret put RELAY_ENCRYPTION_KEY --config wrangler-mcp.toml
136
+ echo " RELAY_ENCRYPTION_KEY set (generated)."
137
+
138
+ echo ""
139
+ echo "=== Step 5: Build and deploy ==="
140
+
141
+ npm run build:cloud
142
+ npx wrangler deploy --config wrangler-mcp.toml
143
+
144
+ echo ""
145
+ echo "=== Done ==="
146
+ echo ""
147
+ echo "Memory Crystal Cloud MCP server deployed."
148
+ echo "Worker URL: https://memory-crystal-cloud.<your-subdomain>.workers.dev"
149
+ echo ""
150
+ echo "Next steps:"
151
+ echo " 1. Test: curl https://memory-crystal-cloud.<subdomain>.workers.dev/health"
152
+ echo " 2. Test OAuth: GET /.well-known/oauth-authorization-server"
153
+ echo " 3. Connect from ChatGPT or Claude"
@@ -0,0 +1,116 @@
1
+ #!/bin/bash
2
+ # Job: ldm-backup
3
+ # Backs up the LDM directory (~/.ldm/) to a timestamped snapshot.
4
+ # Handles SQLite databases safely (sqlite3 .backup if available, cp otherwise).
5
+ #
6
+ # Source of truth: memory-crystal-private/scripts/ldm-backup.sh
7
+ # Deployed to: ~/.ldm/bin/ldm-backup.sh (via crystal init)
8
+ #
9
+ # Usage:
10
+ # ldm-backup.sh # backup to default location
11
+ # ldm-backup.sh --keep 14 # keep last 14 backups (default: 7)
12
+ # ldm-backup.sh --include-secrets # include secrets/ dir
13
+ #
14
+ # Destination: $LDM_BACKUP_DIR or ~/.ldm/backups/
15
+
16
+ set -euo pipefail
17
+
18
+ # Cron provides minimal PATH
19
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
20
+
21
+ LDM_HOME="$HOME/.ldm"
22
+ BACKUP_ROOT="${LDM_BACKUP_DIR:-$LDM_HOME/backups}"
23
+ KEEP=7
24
+ INCLUDE_SECRETS=false
25
+
26
+ # Parse flags
27
+ while [[ $# -gt 0 ]]; do
28
+ case "$1" in
29
+ --keep)
30
+ KEEP="$2"
31
+ shift 2
32
+ ;;
33
+ --include-secrets)
34
+ INCLUDE_SECRETS=true
35
+ shift
36
+ ;;
37
+ *)
38
+ echo "Unknown flag: $1" >&2
39
+ exit 1
40
+ ;;
41
+ esac
42
+ done
43
+
44
+ if [ ! -d "$LDM_HOME" ]; then
45
+ echo "ERROR: LDM home not found at $LDM_HOME" >&2
46
+ exit 1
47
+ fi
48
+
49
+ TIMESTAMP=$(date +%Y-%m-%d-%H%M%S)
50
+ DEST="$BACKUP_ROOT/$TIMESTAMP"
51
+
52
+ echo "LDM Backup: $DEST"
53
+ mkdir -p "$DEST"
54
+
55
+ # ── Back up crystal.db (safe copy) ──
56
+
57
+ CRYSTAL_DB="$LDM_HOME/memory/crystal.db"
58
+ if [ -f "$CRYSTAL_DB" ]; then
59
+ mkdir -p "$DEST/memory"
60
+ if command -v sqlite3 &>/dev/null; then
61
+ # Safe backup via sqlite3 .backup (handles WAL mode correctly)
62
+ sqlite3 "$CRYSTAL_DB" ".backup '$DEST/memory/crystal.db'"
63
+ echo " crystal.db: backed up (sqlite3 .backup)"
64
+ else
65
+ # Fallback: file copy (may include partial WAL state)
66
+ cp "$CRYSTAL_DB" "$DEST/memory/crystal.db"
67
+ # Copy WAL and SHM if present
68
+ [ -f "$CRYSTAL_DB-wal" ] && cp "$CRYSTAL_DB-wal" "$DEST/memory/crystal.db-wal"
69
+ [ -f "$CRYSTAL_DB-shm" ] && cp "$CRYSTAL_DB-shm" "$DEST/memory/crystal.db-shm"
70
+ echo " crystal.db: backed up (file copy)"
71
+ fi
72
+ else
73
+ echo " crystal.db: not found (skipped)"
74
+ fi
75
+
76
+ # ── Back up config ──
77
+
78
+ if [ -f "$LDM_HOME/config.json" ]; then
79
+ cp "$LDM_HOME/config.json" "$DEST/config.json"
80
+ echo " config.json: backed up"
81
+ fi
82
+
83
+ # ── Back up state files ──
84
+
85
+ if [ -d "$LDM_HOME/state" ]; then
86
+ cp -a "$LDM_HOME/state" "$DEST/state"
87
+ echo " state/: backed up"
88
+ fi
89
+
90
+ # ── Back up agents (transcripts, sessions, daily logs, journals) ──
91
+
92
+ if [ -d "$LDM_HOME/agents" ]; then
93
+ cp -a "$LDM_HOME/agents" "$DEST/agents"
94
+ echo " agents/: backed up"
95
+ fi
96
+
97
+ # ── Back up secrets (optional) ──
98
+
99
+ if [ "$INCLUDE_SECRETS" = true ] && [ -d "$LDM_HOME/secrets" ]; then
100
+ cp -a "$LDM_HOME/secrets" "$DEST/secrets"
101
+ chmod 700 "$DEST/secrets"
102
+ echo " secrets/: backed up"
103
+ fi
104
+
105
+ # ── Retention: remove old backups ──
106
+
107
+ BACKUP_COUNT=$(ls -1d "$BACKUP_ROOT"/????-??-??-?????? 2>/dev/null | wc -l | tr -d ' ')
108
+ if [ "$BACKUP_COUNT" -gt "$KEEP" ]; then
109
+ REMOVE_COUNT=$((BACKUP_COUNT - KEEP))
110
+ ls -1d "$BACKUP_ROOT"/????-??-??-?????? | head -n "$REMOVE_COUNT" | while read OLD; do
111
+ rm -rf "$OLD"
112
+ echo " Removed old: $(basename "$OLD")"
113
+ done
114
+ fi
115
+
116
+ echo "Done. $BACKUP_COUNT backups total (keeping $KEEP)."
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ // migrate-lance-to-sqlite.mjs — Copy all chunks + vectors from LanceDB to sqlite-vec.
3
+ // Reads vectors directly from LanceDB (no re-embedding needed).
4
+ // Deduplicates by SHA-256 hash of text content.
5
+ //
6
+ // Usage:
7
+ // node scripts/migrate-lance-to-sqlite.mjs [--dry-run] [--batch-size N]
8
+ //
9
+ // Data dir: ~/.openclaw/memory-crystal/
10
+
11
+ import * as lancedb from '@lancedb/lancedb';
12
+ import Database from 'better-sqlite3';
13
+ import * as sqliteVec from 'sqlite-vec';
14
+ import { createHash } from 'node:crypto';
15
+ import { existsSync, mkdirSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+
19
+ const BATCH_SIZE = 500;
20
+
21
+ async function main() {
22
+ const args = process.argv.slice(2);
23
+ const dryRun = args.includes('--dry-run');
24
+ const batchSizeArg = args.find((_, i) => args[i - 1] === '--batch-size');
25
+ const batchSize = batchSizeArg ? parseInt(batchSizeArg) : BATCH_SIZE;
26
+
27
+ const openclawHome = process.env.OPENCLAW_HOME || join(process.env.HOME || homedir(), '.openclaw');
28
+ const dataDir = join(openclawHome, 'memory-crystal');
29
+ const lanceDir = join(dataDir, 'lance');
30
+ const sqlitePath = join(dataDir, 'crystal.db');
31
+
32
+ if (!existsSync(lanceDir)) {
33
+ console.error(`LanceDB directory not found: ${lanceDir}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ // Open LanceDB
38
+ const lanceDb = await lancedb.connect(lanceDir);
39
+ const tableNames = await lanceDb.tableNames();
40
+ if (!tableNames.includes('chunks')) {
41
+ console.error('No "chunks" table in LanceDB');
42
+ process.exit(1);
43
+ }
44
+ const lanceTable = await lanceDb.openTable('chunks');
45
+ const totalLance = await lanceTable.countRows();
46
+ console.log(`LanceDB chunks: ${totalLance.toLocaleString()}`);
47
+
48
+ // Open SQLite + load sqlite-vec
49
+ const db = new Database(sqlitePath);
50
+ db.pragma('journal_mode = WAL');
51
+ sqliteVec.load(db);
52
+
53
+ // Ensure tables exist
54
+ db.exec(`
55
+ CREATE TABLE IF NOT EXISTS chunks (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ text TEXT NOT NULL,
58
+ text_hash TEXT NOT NULL,
59
+ role TEXT,
60
+ source_type TEXT,
61
+ source_id TEXT,
62
+ agent_id TEXT,
63
+ token_count INTEGER,
64
+ created_at TEXT NOT NULL
65
+ );
66
+ CREATE INDEX IF NOT EXISTS idx_chunks_agent ON chunks(agent_id);
67
+ CREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source_type);
68
+ CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(text_hash);
69
+ CREATE INDEX IF NOT EXISTS idx_chunks_created ON chunks(created_at);
70
+
71
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
72
+ text,
73
+ tokenize='porter unicode61'
74
+ );
75
+
76
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_insert AFTER INSERT ON chunks
77
+ BEGIN
78
+ INSERT INTO chunks_fts(rowid, text) VALUES (NEW.id, NEW.text);
79
+ END;
80
+ `);
81
+
82
+ const existingSqlite = (db.prepare('SELECT COUNT(*) as count FROM chunks').get()).count;
83
+ console.log(`SQLite chunks (before): ${existingSqlite.toLocaleString()}`);
84
+
85
+ if (dryRun) {
86
+ // Sample some rows
87
+ const sample = await lanceTable.query().limit(3).toArray();
88
+ console.log('\nSample (3 rows):');
89
+ for (const row of sample) {
90
+ console.log(` [${row.source_type}] [${row.agent_id}] ${row.text?.slice(0, 80)}...`);
91
+ console.log(` vector: ${row.vector?.length} dims, created: ${row.created_at}`);
92
+ }
93
+ console.log(`\nWould migrate ${totalLance.toLocaleString()} chunks.`);
94
+ console.log(`Estimated crystal.db growth: ~${Math.round(totalLance * 1536 * 4 / 1024 / 1024)}MB vectors + text`);
95
+ db.close();
96
+ return;
97
+ }
98
+
99
+ // Detect dimensions from first row
100
+ const [firstRow] = await lanceTable.query().limit(1).toArray();
101
+ const dimensions = firstRow.vector?.length;
102
+ if (!dimensions) {
103
+ console.error('Could not determine vector dimensions from LanceDB');
104
+ process.exit(1);
105
+ }
106
+ console.log(`Vector dimensions: ${dimensions}`);
107
+
108
+ // Create vec table if needed
109
+ const vecExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks_vec'`).get();
110
+ if (!vecExists) {
111
+ db.exec(`CREATE VIRTUAL TABLE chunks_vec USING vec0(
112
+ chunk_id INTEGER PRIMARY KEY,
113
+ embedding float[${dimensions}] distance_metric=cosine
114
+ )`);
115
+ console.log(`Created chunks_vec table (${dimensions} dims)`);
116
+ }
117
+
118
+ // Build hash set of existing chunks for dedup
119
+ console.log('Building dedup hash set...');
120
+ const existingHashes = new Set();
121
+ const hashRows = db.prepare('SELECT text_hash FROM chunks').all();
122
+ for (const row of hashRows) {
123
+ existingHashes.add(row.text_hash);
124
+ }
125
+ console.log(`Existing unique hashes: ${existingHashes.size.toLocaleString()}`);
126
+
127
+ // Prepare insert statements
128
+ const insertChunk = db.prepare(`
129
+ INSERT INTO chunks (text, text_hash, role, source_type, source_id, agent_id, token_count, created_at)
130
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
131
+ `);
132
+ const insertVec = db.prepare(`
133
+ INSERT INTO chunks_vec (chunk_id, embedding) VALUES (?, ?)
134
+ `);
135
+
136
+ // Read all rows from LanceDB in batches using offset/limit
137
+ let migrated = 0;
138
+ let skippedDedup = 0;
139
+ let offset = 0;
140
+ const startTime = Date.now();
141
+
142
+ while (offset < totalLance) {
143
+ const rows = await lanceTable.query().limit(batchSize).offset(offset).toArray();
144
+ if (rows.length === 0) break;
145
+
146
+ const transaction = db.transaction(() => {
147
+ for (const row of rows) {
148
+ const text = row.text || '';
149
+ const hash = createHash('sha256').update(text).digest('hex');
150
+
151
+ if (existingHashes.has(hash)) {
152
+ skippedDedup++;
153
+ continue;
154
+ }
155
+ existingHashes.add(hash);
156
+
157
+ const result = insertChunk.run(
158
+ text,
159
+ hash,
160
+ row.role || null,
161
+ row.source_type || null,
162
+ row.source_id || null,
163
+ row.agent_id || null,
164
+ row.token_count || Math.ceil(text.length / 4),
165
+ row.created_at || new Date().toISOString()
166
+ );
167
+
168
+ // sqlite-vec needs BigInt for integer primary keys
169
+ const chunkId = typeof result.lastInsertRowid === 'bigint'
170
+ ? result.lastInsertRowid
171
+ : BigInt(result.lastInsertRowid);
172
+
173
+ // Convert vector to Float32Array
174
+ const vector = row.vector;
175
+ const f32 = vector instanceof Float32Array ? vector : new Float32Array(Array.from(vector));
176
+ insertVec.run(chunkId, f32);
177
+
178
+ migrated++;
179
+ }
180
+ });
181
+ transaction();
182
+
183
+ offset += rows.length;
184
+ const elapsed = (Date.now() - startTime) / 1000;
185
+ const rate = Math.round(offset / elapsed);
186
+ const eta = Math.round((totalLance - offset) / rate);
187
+ process.stdout.write(
188
+ `\r ${offset.toLocaleString()}/${totalLance.toLocaleString()} (${Math.round(offset / totalLance * 100)}%) ` +
189
+ `| migrated: ${migrated.toLocaleString()} | dedup: ${skippedDedup.toLocaleString()} ` +
190
+ `| ${rate}/s | ETA: ${eta}s `
191
+ );
192
+ }
193
+
194
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
195
+ console.log(`\n\nMigration complete in ${elapsed}s:`);
196
+ console.log(` Migrated: ${migrated.toLocaleString()}`);
197
+ console.log(` Dedup skip: ${skippedDedup.toLocaleString()}`);
198
+
199
+ // Verify
200
+ const finalCount = (db.prepare('SELECT COUNT(*) as count FROM chunks').get()).count;
201
+ const ftsCount = (db.prepare('SELECT COUNT(*) as count FROM chunks_fts').get()).count;
202
+ console.log(` SQLite chunks: ${finalCount.toLocaleString()}`);
203
+ console.log(` FTS entries: ${ftsCount.toLocaleString()}`);
204
+ console.log(` LanceDB: ${totalLance.toLocaleString()}`);
205
+
206
+ if (finalCount === ftsCount) {
207
+ console.log(' FTS sync: OK');
208
+ } else {
209
+ console.warn(` WARNING: FTS count mismatch (${ftsCount} vs ${finalCount})`);
210
+ }
211
+
212
+ db.close();
213
+ }
214
+
215
+ main().catch(err => {
216
+ console.error(`Migration failed: ${err.message}`);
217
+ process.exit(1);
218
+ });