memory-crystal 0.2.0

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 (104) hide show
  1. package/.env.example +20 -0
  2. package/CHANGELOG.md +6 -0
  3. package/LETTERS.md +22 -0
  4. package/LICENSE +21 -0
  5. package/README-ENTERPRISE.md +162 -0
  6. package/README-old.md +275 -0
  7. package/README.md +91 -0
  8. package/RELAY.md +88 -0
  9. package/TECHNICAL.md +379 -0
  10. package/ai/dev-updates/2026-02-25--cc-air--phase2-architecture-pivot.md +70 -0
  11. package/ai/dev-updates/2026-02-25--cc-air--phase2-worker-build.md +72 -0
  12. package/ai/dev-updates/2026-02-26--10-25-16--cc-mini--phase2-implementation.md +49 -0
  13. package/ai/dev-updates/2026-02-27--20-30-00--cc-mini--readme-overhaul-and-public-deploy.md +69 -0
  14. package/ai/notes/2026-02-26--cc-air--notes.md +412 -0
  15. package/ai/notes/2026-02-27--cc-mini--grok-feedback.md +44 -0
  16. package/ai/notes/2026-02-27--cc-mini--lesa-feedback.md +45 -0
  17. package/ai/notes/RESEARCH.md +1185 -0
  18. package/ai/notes/salience-research/README.md +29 -0
  19. package/ai/notes/salience-research/eurosla-salience-review.md +64 -0
  20. package/ai/notes/salience-research/full-research-summary.md +269 -0
  21. package/ai/notes/salience-research/salience-levels-diagram.png +0 -0
  22. package/ai/plan/2026-02-27--cc-mini--qr-pairing-spec.md +203 -0
  23. package/ai/plan/_archive/PLAN.md +194 -0
  24. package/ai/plan/_archive/PRD.md +1014 -0
  25. package/ai/plan/cc-plans-duplicates-from-dot-claude/2026-02-26--cc-mini--phase2-implementation-plan.md +245 -0
  26. package/ai/plan/dev-conventions-note.md +70 -0
  27. package/ai/plan/ldm-os-install-and-boot-architecture.md +285 -0
  28. package/ai/plan/memory-crystal-phase2-plan.md +192 -0
  29. package/ai/plan/memory-system-lay-of-the-land.md +214 -0
  30. package/ai/plan/phase2-ephemeral-relay.md +238 -0
  31. package/ai/plan/readme-first.md +68 -0
  32. package/ai/plan/roadmap.md +159 -0
  33. package/ai/todos/PUNCHLIST.md +44 -0
  34. package/ai/todos/README.md +31 -0
  35. package/ai/todos/inboxes/cc-air/2026-02-26--cc-air--post-relay-todos.md +85 -0
  36. package/ai/todos/inboxes/cc-mini/2026-02-26--cc-mini--phase2-status.md +100 -0
  37. package/ai/todos/inboxes/cc-mini/_archive/TODO.md +25 -0
  38. package/ai/todos/inboxes/parker/2026-02-25--cc-air--setup-checklist.md +139 -0
  39. package/ai/todos/inboxes/parker/2026-02-26--cc-mini--phase2-your-moves.md +72 -0
  40. package/dist/cc-hook.d.ts +1 -0
  41. package/dist/cc-hook.js +349 -0
  42. package/dist/chunk-3VFIJYS4.js +818 -0
  43. package/dist/chunk-52QE3YI3.js +1169 -0
  44. package/dist/chunk-AA3OPP4Z.js +432 -0
  45. package/dist/chunk-D3I3ZSE2.js +411 -0
  46. package/dist/chunk-EKSACBTJ.js +1070 -0
  47. package/dist/chunk-F3Y7EL7K.js +83 -0
  48. package/dist/chunk-JWZXYVET.js +1068 -0
  49. package/dist/chunk-KYVWO6ZM.js +1069 -0
  50. package/dist/chunk-L3VHARQH.js +413 -0
  51. package/dist/chunk-LOVAHSQV.js +411 -0
  52. package/dist/chunk-LQOYCAGG.js +446 -0
  53. package/dist/chunk-MK42FMEG.js +147 -0
  54. package/dist/chunk-NIJCVN3O.js +147 -0
  55. package/dist/chunk-O2UITJGH.js +465 -0
  56. package/dist/chunk-PEK6JH65.js +432 -0
  57. package/dist/chunk-PJ6FFKEX.js +77 -0
  58. package/dist/chunk-PLUBBZYR.js +800 -0
  59. package/dist/chunk-SGL6ISBJ.js +1061 -0
  60. package/dist/chunk-UNHVZB5G.js +411 -0
  61. package/dist/chunk-VAFTWSTE.js +1061 -0
  62. package/dist/chunk-XZ3S56RQ.js +1061 -0
  63. package/dist/chunk-Y72C7F6O.js +148 -0
  64. package/dist/cli.d.ts +1 -0
  65. package/dist/cli.js +325 -0
  66. package/dist/core.d.ts +188 -0
  67. package/dist/core.js +12 -0
  68. package/dist/crypto.d.ts +16 -0
  69. package/dist/crypto.js +18 -0
  70. package/dist/dev-update-SZ2Z4WCQ.js +6 -0
  71. package/dist/ldm.d.ts +17 -0
  72. package/dist/ldm.js +12 -0
  73. package/dist/mcp-server.d.ts +1 -0
  74. package/dist/mcp-server.js +250 -0
  75. package/dist/migrate.d.ts +1 -0
  76. package/dist/migrate.js +89 -0
  77. package/dist/mirror-sync.d.ts +1 -0
  78. package/dist/mirror-sync.js +130 -0
  79. package/dist/openclaw.d.ts +5 -0
  80. package/dist/openclaw.js +349 -0
  81. package/dist/poller.d.ts +1 -0
  82. package/dist/poller.js +272 -0
  83. package/dist/summarize.d.ts +19 -0
  84. package/dist/summarize.js +10 -0
  85. package/dist/worker.js +137 -0
  86. package/openclaw.plugin.json +11 -0
  87. package/package.json +40 -0
  88. package/scripts/migrate-lance-to-sqlite.mjs +217 -0
  89. package/skills/memory/SKILL.md +61 -0
  90. package/src/cc-hook.ts +447 -0
  91. package/src/cli.ts +356 -0
  92. package/src/core.ts +1472 -0
  93. package/src/crypto.ts +113 -0
  94. package/src/dev-update.ts +178 -0
  95. package/src/ldm.ts +117 -0
  96. package/src/mcp-server.ts +274 -0
  97. package/src/migrate.ts +104 -0
  98. package/src/mirror-sync.ts +175 -0
  99. package/src/openclaw.ts +250 -0
  100. package/src/poller.ts +345 -0
  101. package/src/summarize.ts +210 -0
  102. package/src/worker.ts +208 -0
  103. package/tsconfig.json +18 -0
  104. package/wrangler.toml +20 -0
package/src/worker.ts ADDED
@@ -0,0 +1,208 @@
1
+ // memory-crystal/worker.ts — Cloudflare Worker (Ephemeral Relay).
2
+ // Dead drop for encrypted blobs. No search, no database, no intelligence.
3
+ // Data passes through encrypted, gets picked up, gets deleted.
4
+ // The Worker cannot read what it holds.
5
+ //
6
+ // Channels:
7
+ // conversations — devices drop encrypted conversation chunks for Mini to pick up
8
+ // mirror — Mini drops encrypted DB snapshot for devices to pick up
9
+ //
10
+ // Endpoints:
11
+ // POST /drop/:channel — deposit encrypted blob
12
+ // GET /pickup/:channel — list available blobs
13
+ // GET /pickup/:channel/:id — retrieve specific blob
14
+ // DELETE /confirm/:channel/:id — confirm receipt, delete blob
15
+ // GET /health — alive check (no auth)
16
+
17
+ export interface Env {
18
+ RELAY: R2Bucket;
19
+ AUTH_TOKEN_CC_AIR: string;
20
+ AUTH_TOKEN_CC_MINI: string;
21
+ AUTH_TOKEN_LESA: string;
22
+ }
23
+
24
+ // ── Auth ──
25
+
26
+ interface AuthResult {
27
+ agentId: string;
28
+ }
29
+
30
+ function authenticate(request: Request, env: Env): AuthResult | Response {
31
+ const auth = request.headers.get('Authorization');
32
+ if (!auth?.startsWith('Bearer ')) {
33
+ return json({ error: 'Missing Authorization header' }, 401);
34
+ }
35
+
36
+ const token = auth.slice(7);
37
+ const tokenMap: Record<string, string> = {};
38
+ if (env.AUTH_TOKEN_CC_AIR) tokenMap[env.AUTH_TOKEN_CC_AIR] = 'cc-air';
39
+ if (env.AUTH_TOKEN_CC_MINI) tokenMap[env.AUTH_TOKEN_CC_MINI] = 'cc-mini';
40
+ if (env.AUTH_TOKEN_LESA) tokenMap[env.AUTH_TOKEN_LESA] = 'lesa-mini';
41
+
42
+ const agentId = tokenMap[token];
43
+ if (!agentId) {
44
+ return json({ error: 'Invalid token' }, 403);
45
+ }
46
+
47
+ return { agentId };
48
+ }
49
+
50
+ // ── Channel validation ──
51
+
52
+ const VALID_CHANNELS = ['conversations', 'mirror'];
53
+
54
+ function isValidChannel(channel: string): boolean {
55
+ return VALID_CHANNELS.includes(channel);
56
+ }
57
+
58
+ // ── Handlers ──
59
+
60
+ async function handleDrop(request: Request, env: Env, agentId: string, channel: string): Promise<Response> {
61
+ if (!isValidChannel(channel)) {
62
+ return json({ error: `Invalid channel: ${channel}. Valid: ${VALID_CHANNELS.join(', ')}` }, 400);
63
+ }
64
+
65
+ const body = await request.arrayBuffer();
66
+ if (body.byteLength === 0) {
67
+ return json({ error: 'Empty payload' }, 400);
68
+ }
69
+
70
+ // Max 100MB per blob (DB snapshots can be large)
71
+ if (body.byteLength > 100 * 1024 * 1024) {
72
+ return json({ error: 'Payload too large (max 100MB)' }, 413);
73
+ }
74
+
75
+ const id = crypto.randomUUID();
76
+ const key = `${channel}/${id}`;
77
+ const now = new Date().toISOString();
78
+
79
+ await env.RELAY.put(key, body, {
80
+ customMetadata: {
81
+ agent_id: agentId,
82
+ dropped_at: now,
83
+ size: String(body.byteLength),
84
+ },
85
+ });
86
+
87
+ return json({ ok: true, id, channel, size: body.byteLength, dropped_at: now });
88
+ }
89
+
90
+ async function handlePickupList(env: Env, channel: string): Promise<Response> {
91
+ if (!isValidChannel(channel)) {
92
+ return json({ error: `Invalid channel: ${channel}` }, 400);
93
+ }
94
+
95
+ const listed = await env.RELAY.list({ prefix: `${channel}/` });
96
+ const blobs = listed.objects.map(obj => ({
97
+ id: obj.key.split('/')[1],
98
+ size: obj.size,
99
+ dropped_at: obj.customMetadata?.dropped_at || obj.uploaded.toISOString(),
100
+ agent_id: obj.customMetadata?.agent_id || 'unknown',
101
+ }));
102
+
103
+ return json({ channel, count: blobs.length, blobs });
104
+ }
105
+
106
+ async function handlePickup(env: Env, channel: string, id: string): Promise<Response> {
107
+ if (!isValidChannel(channel)) {
108
+ return json({ error: `Invalid channel: ${channel}` }, 400);
109
+ }
110
+
111
+ const key = `${channel}/${id}`;
112
+ const obj = await env.RELAY.get(key);
113
+
114
+ if (!obj) {
115
+ return json({ error: 'Blob not found (already picked up or expired)' }, 404);
116
+ }
117
+
118
+ return new Response(obj.body, {
119
+ headers: {
120
+ 'Content-Type': 'application/octet-stream',
121
+ 'X-Agent-Id': obj.customMetadata?.agent_id || 'unknown',
122
+ 'X-Dropped-At': obj.customMetadata?.dropped_at || '',
123
+ },
124
+ });
125
+ }
126
+
127
+ async function handleConfirm(env: Env, channel: string, id: string): Promise<Response> {
128
+ if (!isValidChannel(channel)) {
129
+ return json({ error: `Invalid channel: ${channel}` }, 400);
130
+ }
131
+
132
+ const key = `${channel}/${id}`;
133
+ const obj = await env.RELAY.head(key);
134
+
135
+ if (!obj) {
136
+ return json({ error: 'Blob not found (already confirmed or expired)' }, 404);
137
+ }
138
+
139
+ await env.RELAY.delete(key);
140
+ return json({ ok: true, deleted: key });
141
+ }
142
+
143
+ // ── Response helper ──
144
+
145
+ function json(data: unknown, status = 200): Response {
146
+ return new Response(JSON.stringify(data), {
147
+ status,
148
+ headers: { 'Content-Type': 'application/json' },
149
+ });
150
+ }
151
+
152
+ // ── Router ──
153
+
154
+ export default {
155
+ async fetch(request: Request, env: Env): Promise<Response> {
156
+ const url = new URL(request.url);
157
+ const parts = url.pathname.split('/').filter(Boolean);
158
+
159
+ // Health check (no auth)
160
+ if (parts[0] === 'health' && request.method === 'GET') {
161
+ return json({ ok: true, service: 'memory-crystal-relay', mode: 'ephemeral' });
162
+ }
163
+
164
+ // Everything else requires auth
165
+ const authResult = authenticate(request, env);
166
+ if (authResult instanceof Response) return authResult;
167
+ const { agentId } = authResult;
168
+
169
+ try {
170
+ // POST /drop/:channel
171
+ if (parts[0] === 'drop' && parts[1] && request.method === 'POST') {
172
+ return handleDrop(request, env, agentId, parts[1]);
173
+ }
174
+
175
+ // GET /pickup/:channel (list) or GET /pickup/:channel/:id (retrieve)
176
+ if (parts[0] === 'pickup' && parts[1] && request.method === 'GET') {
177
+ if (parts[2]) {
178
+ return handlePickup(env, parts[1], parts[2]);
179
+ }
180
+ return handlePickupList(env, parts[1]);
181
+ }
182
+
183
+ // DELETE /confirm/:channel/:id
184
+ if (parts[0] === 'confirm' && parts[1] && parts[2] && request.method === 'DELETE') {
185
+ return handleConfirm(env, parts[1], parts[2]);
186
+ }
187
+
188
+ return json({ error: 'Not found' }, 404);
189
+ } catch (err: any) {
190
+ return json({ error: err.message || 'Internal error' }, 500);
191
+ }
192
+ },
193
+
194
+ // Scheduled cleanup: delete blobs older than 24h (TTL safety net)
195
+ async scheduled(event: ScheduledEvent, env: Env): Promise<void> {
196
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
197
+
198
+ for (const channel of VALID_CHANNELS) {
199
+ const listed = await env.RELAY.list({ prefix: `${channel}/` });
200
+ for (const obj of listed.objects) {
201
+ const droppedAt = obj.customMetadata?.dropped_at;
202
+ if (droppedAt && new Date(droppedAt).getTime() < cutoff) {
203
+ await env.RELAY.delete(obj.key);
204
+ }
205
+ }
206
+ }
207
+ },
208
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "declaration": true,
13
+ "resolveJsonModule": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
package/wrangler.toml ADDED
@@ -0,0 +1,20 @@
1
+ name = "memory-crystal-relay"
2
+ main = "dist/worker.js"
3
+ compatibility_date = "2024-12-01"
4
+
5
+ # R2 — Ephemeral blob storage (encrypted payloads, auto-cleaned after 24h)
6
+ [[r2_buckets]]
7
+ binding = "RELAY"
8
+ bucket_name = "memory-crystal-relay"
9
+
10
+ # Cron trigger: clean up expired blobs every hour
11
+ [triggers]
12
+ crons = ["0 * * * *"]
13
+
14
+ # Secrets (set via `wrangler secret put`):
15
+ # AUTH_TOKEN_CC_AIR — bearer token for cc-air (MacBook Air)
16
+ # AUTH_TOKEN_CC_MINI — bearer token for cc-mini (Mac Mini)
17
+ # AUTH_TOKEN_LESA — bearer token for lēsa-mini
18
+ #
19
+ # NO encryption keys here. NO API keys here.
20
+ # The Worker cannot read what it holds.