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.
- package/.env.example +20 -0
- package/CHANGELOG.md +6 -0
- package/LETTERS.md +22 -0
- package/LICENSE +21 -0
- package/README-ENTERPRISE.md +162 -0
- package/README-old.md +275 -0
- package/README.md +91 -0
- package/RELAY.md +88 -0
- package/TECHNICAL.md +379 -0
- package/ai/dev-updates/2026-02-25--cc-air--phase2-architecture-pivot.md +70 -0
- package/ai/dev-updates/2026-02-25--cc-air--phase2-worker-build.md +72 -0
- package/ai/dev-updates/2026-02-26--10-25-16--cc-mini--phase2-implementation.md +49 -0
- package/ai/dev-updates/2026-02-27--20-30-00--cc-mini--readme-overhaul-and-public-deploy.md +69 -0
- package/ai/notes/2026-02-26--cc-air--notes.md +412 -0
- package/ai/notes/2026-02-27--cc-mini--grok-feedback.md +44 -0
- package/ai/notes/2026-02-27--cc-mini--lesa-feedback.md +45 -0
- package/ai/notes/RESEARCH.md +1185 -0
- package/ai/notes/salience-research/README.md +29 -0
- package/ai/notes/salience-research/eurosla-salience-review.md +64 -0
- package/ai/notes/salience-research/full-research-summary.md +269 -0
- package/ai/notes/salience-research/salience-levels-diagram.png +0 -0
- package/ai/plan/2026-02-27--cc-mini--qr-pairing-spec.md +203 -0
- package/ai/plan/_archive/PLAN.md +194 -0
- package/ai/plan/_archive/PRD.md +1014 -0
- package/ai/plan/cc-plans-duplicates-from-dot-claude/2026-02-26--cc-mini--phase2-implementation-plan.md +245 -0
- package/ai/plan/dev-conventions-note.md +70 -0
- package/ai/plan/ldm-os-install-and-boot-architecture.md +285 -0
- package/ai/plan/memory-crystal-phase2-plan.md +192 -0
- package/ai/plan/memory-system-lay-of-the-land.md +214 -0
- package/ai/plan/phase2-ephemeral-relay.md +238 -0
- package/ai/plan/readme-first.md +68 -0
- package/ai/plan/roadmap.md +159 -0
- package/ai/todos/PUNCHLIST.md +44 -0
- package/ai/todos/README.md +31 -0
- package/ai/todos/inboxes/cc-air/2026-02-26--cc-air--post-relay-todos.md +85 -0
- package/ai/todos/inboxes/cc-mini/2026-02-26--cc-mini--phase2-status.md +100 -0
- package/ai/todos/inboxes/cc-mini/_archive/TODO.md +25 -0
- package/ai/todos/inboxes/parker/2026-02-25--cc-air--setup-checklist.md +139 -0
- package/ai/todos/inboxes/parker/2026-02-26--cc-mini--phase2-your-moves.md +72 -0
- package/dist/cc-hook.d.ts +1 -0
- package/dist/cc-hook.js +349 -0
- package/dist/chunk-3VFIJYS4.js +818 -0
- package/dist/chunk-52QE3YI3.js +1169 -0
- package/dist/chunk-AA3OPP4Z.js +432 -0
- package/dist/chunk-D3I3ZSE2.js +411 -0
- package/dist/chunk-EKSACBTJ.js +1070 -0
- package/dist/chunk-F3Y7EL7K.js +83 -0
- package/dist/chunk-JWZXYVET.js +1068 -0
- package/dist/chunk-KYVWO6ZM.js +1069 -0
- package/dist/chunk-L3VHARQH.js +413 -0
- package/dist/chunk-LOVAHSQV.js +411 -0
- package/dist/chunk-LQOYCAGG.js +446 -0
- package/dist/chunk-MK42FMEG.js +147 -0
- package/dist/chunk-NIJCVN3O.js +147 -0
- package/dist/chunk-O2UITJGH.js +465 -0
- package/dist/chunk-PEK6JH65.js +432 -0
- package/dist/chunk-PJ6FFKEX.js +77 -0
- package/dist/chunk-PLUBBZYR.js +800 -0
- package/dist/chunk-SGL6ISBJ.js +1061 -0
- package/dist/chunk-UNHVZB5G.js +411 -0
- package/dist/chunk-VAFTWSTE.js +1061 -0
- package/dist/chunk-XZ3S56RQ.js +1061 -0
- package/dist/chunk-Y72C7F6O.js +148 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +325 -0
- package/dist/core.d.ts +188 -0
- package/dist/core.js +12 -0
- package/dist/crypto.d.ts +16 -0
- package/dist/crypto.js +18 -0
- package/dist/dev-update-SZ2Z4WCQ.js +6 -0
- package/dist/ldm.d.ts +17 -0
- package/dist/ldm.js +12 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +250 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +89 -0
- package/dist/mirror-sync.d.ts +1 -0
- package/dist/mirror-sync.js +130 -0
- package/dist/openclaw.d.ts +5 -0
- package/dist/openclaw.js +349 -0
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +272 -0
- package/dist/summarize.d.ts +19 -0
- package/dist/summarize.js +10 -0
- package/dist/worker.js +137 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +40 -0
- package/scripts/migrate-lance-to-sqlite.mjs +217 -0
- package/skills/memory/SKILL.md +61 -0
- package/src/cc-hook.ts +447 -0
- package/src/cli.ts +356 -0
- package/src/core.ts +1472 -0
- package/src/crypto.ts +113 -0
- package/src/dev-update.ts +178 -0
- package/src/ldm.ts +117 -0
- package/src/mcp-server.ts +274 -0
- package/src/migrate.ts +104 -0
- package/src/mirror-sync.ts +175 -0
- package/src/openclaw.ts +250 -0
- package/src/poller.ts +345 -0
- package/src/summarize.ts +210 -0
- package/src/worker.ts +208 -0
- package/tsconfig.json +18 -0
- 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.
|