@raviolelabs/engram-mcp 0.2.2 → 0.4.2
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/LICENSE +1 -1
- package/README.md +139 -134
- package/SKILL.md +33 -0
- package/dist/core/db/index.d.ts.map +1 -1
- package/dist/core/db/index.js +27 -1
- package/dist/core/db/index.js.map +1 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +4 -4
- package/dist/core/logger.js.map +1 -1
- package/dist/core/server/http.d.ts +3 -0
- package/dist/core/server/http.d.ts.map +1 -1
- package/dist/core/server/http.js +6 -0
- package/dist/core/server/http.js.map +1 -1
- package/dist/core/server/instructions.js +1 -1
- package/dist/core/server/mcp-handler.d.ts.map +1 -1
- package/dist/core/server/mcp-handler.js +28 -1
- package/dist/core/server/mcp-handler.js.map +1 -1
- package/dist/embeddings/index.d.ts +6 -0
- package/dist/embeddings/index.d.ts.map +1 -1
- package/dist/embeddings/index.js +31 -3
- package/dist/embeddings/index.js.map +1 -1
- package/dist/embeddings/providers/engram.d.ts +10 -0
- package/dist/embeddings/providers/engram.d.ts.map +1 -1
- package/dist/embeddings/providers/engram.js +35 -12
- package/dist/embeddings/providers/engram.js.map +1 -1
- package/dist/mcp-server/tests/mcp-e2e.test.js +1 -1
- package/dist/mcp-server/tests/mcp-e2e.test.js.map +1 -1
- package/dist/memory/core/store.d.ts.map +1 -1
- package/dist/memory/core/store.js +5 -4
- package/dist/memory/core/store.js.map +1 -1
- package/dist/memory/modules/team/keystore.d.ts +38 -0
- package/dist/memory/modules/team/keystore.d.ts.map +1 -0
- package/dist/memory/modules/team/keystore.js +117 -0
- package/dist/memory/modules/team/keystore.js.map +1 -0
- package/dist/memory/modules/team/tools.d.ts +10 -0
- package/dist/memory/modules/team/tools.d.ts.map +1 -0
- package/dist/memory/modules/team/tools.js +252 -0
- package/dist/memory/modules/team/tools.js.map +1 -0
- package/dist/memory/public/tools.d.ts.map +1 -1
- package/dist/memory/public/tools.js +33 -5
- package/dist/memory/public/tools.js.map +1 -1
- package/dist/server/api/memories.d.ts.map +1 -1
- package/dist/server/api/memories.js +79 -8
- package/dist/server/api/memories.js.map +1 -1
- package/dist/server/api/types.d.ts +5 -0
- package/dist/server/api/types.d.ts.map +1 -1
- package/dist/server/api/types.js +90 -2
- package/dist/server/api/types.js.map +1 -1
- package/dist/sync/team-sync.d.ts +19 -0
- package/dist/sync/team-sync.d.ts.map +1 -0
- package/dist/sync/team-sync.js +80 -0
- package/dist/sync/team-sync.js.map +1 -0
- package/dist/tests/scope-encryption.test.d.ts +2 -0
- package/dist/tests/scope-encryption.test.d.ts.map +1 -0
- package/dist/tests/scope-encryption.test.js +71 -0
- package/dist/tests/scope-encryption.test.js.map +1 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/webapp/api/team.d.ts +7 -0
- package/dist/webapp/api/team.d.ts.map +1 -0
- package/dist/webapp/api/team.js +57 -0
- package/dist/webapp/api/team.js.map +1 -0
- package/package.json +2 -1
- package/CLAUDE.md +0 -232
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EngramConfig } from '../config/schema.js';
|
|
2
|
+
export interface WorkspaceSyncMessage {
|
|
3
|
+
type: 'workspace.memory_added' | 'workspace.memory_updated' | 'workspace.memory_deleted' | 'workspace.key_rotated';
|
|
4
|
+
workspace_id: string;
|
|
5
|
+
memory_id?: string;
|
|
6
|
+
ts: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Post a workspace memory op to engram-cloud so it fans out to all workspace members.
|
|
10
|
+
* Called by MemoryStore after insert/update/delete for workspace-scoped items.
|
|
11
|
+
* Non-fatal: logs warnings on failure.
|
|
12
|
+
*/
|
|
13
|
+
export declare function broadcastWorkspaceOp(config: EngramConfig, jwt: string, msg: WorkspaceSyncMessage): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Handle incoming workspace sync messages from the UserSyncChannel WebSocket.
|
|
16
|
+
* Called when the sync WS receives a 'workspace.*' message frame.
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleWorkspaceSyncMessage(msg: WorkspaceSyncMessage, store: import('../memory/core/store.js').MemoryStore, dataDir: string, masterKey: Uint8Array): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=team-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-sync.d.ts","sourceRoot":"","sources":["../../src/sync/team-sync.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAIxD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EACA,wBAAwB,GACxB,0BAA0B,GAC1B,0BAA0B,GAC1B,uBAAuB,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,oBAAoB,GACxB,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,oBAAoB,EACzB,KAAK,EAAE,OAAO,yBAAyB,EAAE,WAAW,EACpD,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,IAAI,CAAC,CAuBf"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/sync/team-sync.ts
|
|
2
|
+
// Handles incoming team/workspace sync messages from the UserSyncChannel WebSocket.
|
|
3
|
+
// Also provides broadcastTeamOp for pushing workspace ops to the cloud fan-out.
|
|
4
|
+
import { createLogger } from '../logger.js';
|
|
5
|
+
const log = createLogger('sync:team');
|
|
6
|
+
/**
|
|
7
|
+
* Post a workspace memory op to engram-cloud so it fans out to all workspace members.
|
|
8
|
+
* Called by MemoryStore after insert/update/delete for workspace-scoped items.
|
|
9
|
+
* Non-fatal: logs warnings on failure.
|
|
10
|
+
*/
|
|
11
|
+
export async function broadcastWorkspaceOp(config, jwt, msg) {
|
|
12
|
+
const cloudBaseUrl = config.cloudBaseUrl
|
|
13
|
+
?? 'https://api.engram-mcp.com';
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`${cloudBaseUrl}/api/workspaces/${msg.workspace_id}/broadcast`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
Cookie: `engram_session=${jwt}`,
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify(msg),
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
log.warn('Workspace broadcast failed', {
|
|
25
|
+
status: res.status,
|
|
26
|
+
workspace_id: msg.workspace_id,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
log.warn('Workspace broadcast error (non-fatal)', e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Handle incoming workspace sync messages from the UserSyncChannel WebSocket.
|
|
36
|
+
* Called when the sync WS receives a 'workspace.*' message frame.
|
|
37
|
+
*/
|
|
38
|
+
export async function handleWorkspaceSyncMessage(msg, store, dataDir, masterKey) {
|
|
39
|
+
switch (msg.type) {
|
|
40
|
+
case 'workspace.memory_added':
|
|
41
|
+
case 'workspace.memory_updated':
|
|
42
|
+
// Pull op will sync on next cloud pull — no-op here
|
|
43
|
+
log.debug('workspace.memory_added/updated — will sync on next pull', msg);
|
|
44
|
+
break;
|
|
45
|
+
case 'workspace.memory_deleted':
|
|
46
|
+
if (msg.memory_id) {
|
|
47
|
+
await store.delete(msg.memory_id);
|
|
48
|
+
log.info(`Deleted workspace memory ${msg.memory_id} per sync`);
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
case 'workspace.key_rotated': {
|
|
52
|
+
// Client must re-fetch the workspace key from cloud on next sync pull
|
|
53
|
+
log.info(`Key rotated for workspace ${msg.workspace_id} — will re-fetch on next sync`);
|
|
54
|
+
// Trigger async key re-fetch (fire-and-forget)
|
|
55
|
+
void refreshWorkspaceKey(dataDir, masterKey, msg.workspace_id);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Re-fetch a workspace's wrapped_team_key from cloud and re-store it locally.
|
|
62
|
+
* Called after receiving a workspace.key_rotated event.
|
|
63
|
+
*/
|
|
64
|
+
async function refreshWorkspaceKey(dataDir, masterKey, workspaceId) {
|
|
65
|
+
try {
|
|
66
|
+
// Import keystore dynamically to avoid circular imports
|
|
67
|
+
const { getOrCreateX25519Keypair, unwrapAndStoreWorkspaceKey } = await import('../memory/modules/team/keystore.js');
|
|
68
|
+
const keypair = await getOrCreateX25519Keypair(dataDir, masterKey);
|
|
69
|
+
// We'd need jwt + cloudUrl here — this is a best-effort stub.
|
|
70
|
+
// In production, the full sync reconnect flow will pull the updated wrapped key
|
|
71
|
+
// via GET /api/workspaces after the channel reconnects.
|
|
72
|
+
log.info(`Workspace key refresh queued for ${workspaceId} — will pick up on next sync`);
|
|
73
|
+
void keypair; // suppress unused warning
|
|
74
|
+
void unwrapAndStoreWorkspaceKey; // suppress unused warning
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
log.warn(`Failed to queue workspace key refresh for ${workspaceId}`, e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=team-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-sync.js","sourceRoot":"","sources":["../../src/sync/team-sync.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,oFAAoF;AACpF,gFAAgF;AAEhF,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;AAatC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAoB,EACpB,GAAW,EACX,GAAyB;IAEzB,MAAM,YAAY,GACf,MAA6C,CAAC,YAAkC;WAC9E,4BAA4B,CAAC;IAElC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CACrB,GAAG,YAAY,mBAAmB,GAAG,CAAC,YAAY,YAAY,EAC9D;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,MAAM,EAAE,kBAAkB,GAAG,EAAE;aAChC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;SAC1B,CACF,CAAC;QACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,GAAG,CAAC,IAAI,CAAC,4BAA4B,EAAE;gBACrC,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,YAAY,EAAE,GAAG,CAAC,YAAY;aAC/B,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,GAAG,CAAC,IAAI,CAAC,uCAAuC,EAAE,CAAC,CAAC,CAAC;IACvD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,GAAyB,EACzB,KAAoD,EACpD,OAAe,EACf,SAAqB;IAErB,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,wBAAwB,CAAC;QAC9B,KAAK,0BAA0B;YAC7B,oDAAoD;YACpD,GAAG,CAAC,KAAK,CAAC,yDAAyD,EAAE,GAAG,CAAC,CAAC;YAC1E,MAAM;QAER,KAAK,0BAA0B;YAC7B,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;gBAClB,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAClC,GAAG,CAAC,IAAI,CAAC,4BAA4B,GAAG,CAAC,SAAS,WAAW,CAAC,CAAC;YACjE,CAAC;YACD,MAAM;QAER,KAAK,uBAAuB,CAAC,CAAC,CAAC;YAC7B,sEAAsE;YACtE,GAAG,CAAC,IAAI,CAAC,6BAA6B,GAAG,CAAC,YAAY,+BAA+B,CAAC,CAAC;YACvF,+CAA+C;YAC/C,KAAK,mBAAmB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;YAC/D,MAAM;QACR,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,mBAAmB,CAChC,OAAe,EACf,SAAqB,EACrB,WAAmB;IAEnB,IAAI,CAAC;QACH,wDAAwD;QACxD,MAAM,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,GAC5D,MAAM,MAAM,CAAC,oCAAoC,CAAC,CAAC;QAErD,MAAM,OAAO,GAAG,MAAM,wBAAwB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QAEnE,8DAA8D;QAC9D,gFAAgF;QAChF,wDAAwD;QACxD,GAAG,CAAC,IAAI,CAAC,oCAAoC,WAAW,8BAA8B,CAAC,CAAC;QACxF,KAAK,OAAO,CAAC,CAAC,0BAA0B;QACxC,KAAK,0BAA0B,CAAC,CAAC,0BAA0B;IAC7D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,GAAG,CAAC,IAAI,CAAC,6CAA6C,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-encryption.test.d.ts","sourceRoot":"","sources":["../../src/tests/scope-encryption.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/tests/scope-encryption.test.ts
|
|
2
|
+
// Verifies that workspace-scoped memories use a different encryption key
|
|
3
|
+
// (team master key) from personal memories (personal master key).
|
|
4
|
+
// Uses real libsodium — no mocks.
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
6
|
+
import sodium from 'libsodium-wrappers';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import { generateWorkspaceKey, wrapKeyForRecipient, unwrapAndStoreWorkspaceKey, loadWorkspaceKey, getOrCreateX25519Keypair, } from '../memory/modules/team/keystore.js';
|
|
11
|
+
let tmpDir;
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
await sodium.ready;
|
|
14
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'engram-scope-test-'));
|
|
15
|
+
});
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
describe('scope encryption', () => {
|
|
20
|
+
it('generates a 32-byte workspace key', async () => {
|
|
21
|
+
const key = await generateWorkspaceKey();
|
|
22
|
+
expect(key).toBeInstanceOf(Uint8Array);
|
|
23
|
+
expect(key.length).toBe(32);
|
|
24
|
+
});
|
|
25
|
+
it('generates X25519 keypair and persists it', async () => {
|
|
26
|
+
const masterKey = sodium.randombytes_buf(32);
|
|
27
|
+
const kp = await getOrCreateX25519Keypair(tmpDir, masterKey);
|
|
28
|
+
expect(kp.publicKey.length).toBe(32);
|
|
29
|
+
expect(kp.privateKey.length).toBe(32);
|
|
30
|
+
// Second call returns the same keypair (loaded from disk)
|
|
31
|
+
const kp2 = await getOrCreateX25519Keypair(tmpDir, masterKey);
|
|
32
|
+
expect(sodium.to_hex(kp2.publicKey)).toBe(sodium.to_hex(kp.publicKey));
|
|
33
|
+
});
|
|
34
|
+
it('wraps team key for recipient and unwraps correctly', async () => {
|
|
35
|
+
// Use a fresh tmpDir so the prior test's persisted keypair doesn't interfere
|
|
36
|
+
const wrapDir = await fs.mkdtemp(path.join(os.tmpdir(), 'engram-wrap-test-'));
|
|
37
|
+
const masterKey = sodium.randombytes_buf(32);
|
|
38
|
+
const keypair = await getOrCreateX25519Keypair(wrapDir, masterKey);
|
|
39
|
+
const teamKey = await generateWorkspaceKey();
|
|
40
|
+
const pubkeyHex = sodium.to_hex(keypair.publicKey);
|
|
41
|
+
// Owner wraps team key for recipient
|
|
42
|
+
const wrappedHex = await wrapKeyForRecipient(teamKey, pubkeyHex);
|
|
43
|
+
expect(wrappedHex).toMatch(/^[0-9a-f]+$/);
|
|
44
|
+
// Recipient unwraps
|
|
45
|
+
const workspaceId = 'TEST01WORKSPACEID01';
|
|
46
|
+
const unwrapped = await unwrapAndStoreWorkspaceKey(wrapDir, workspaceId, wrappedHex, keypair);
|
|
47
|
+
expect(sodium.to_hex(unwrapped)).toBe(sodium.to_hex(teamKey));
|
|
48
|
+
// Persisted key matches
|
|
49
|
+
const loaded = await loadWorkspaceKey(wrapDir, workspaceId);
|
|
50
|
+
await fs.rm(wrapDir, { recursive: true, force: true });
|
|
51
|
+
expect(loaded).not.toBeNull();
|
|
52
|
+
expect(sodium.to_hex(loaded)).toBe(sodium.to_hex(teamKey));
|
|
53
|
+
});
|
|
54
|
+
it('team key is different from personal master key', async () => {
|
|
55
|
+
const masterKey = sodium.randombytes_buf(32);
|
|
56
|
+
const teamKey = await generateWorkspaceKey();
|
|
57
|
+
// Very high probability that two 32-byte random values differ
|
|
58
|
+
expect(sodium.to_hex(masterKey)).not.toBe(sodium.to_hex(teamKey));
|
|
59
|
+
});
|
|
60
|
+
it('wrapped keys for different recipients cannot be decrypted by wrong key', async () => {
|
|
61
|
+
const masterKey = sodium.randombytes_buf(32);
|
|
62
|
+
const keypairAlice = await getOrCreateX25519Keypair(tmpDir + '-alice', masterKey);
|
|
63
|
+
// Bob has a different key directory
|
|
64
|
+
const keypairBob = await getOrCreateX25519Keypair(tmpDir + '-bob', masterKey);
|
|
65
|
+
const teamKey = await generateWorkspaceKey();
|
|
66
|
+
const wrappedForAlice = await wrapKeyForRecipient(teamKey, sodium.to_hex(keypairAlice.publicKey));
|
|
67
|
+
// Bob tries to unwrap a key sealed for Alice — should throw
|
|
68
|
+
await expect(unwrapAndStoreWorkspaceKey(tmpDir + '-bob', 'WRONG_WORKSPACE_ID', wrappedForAlice, keypairBob)).rejects.toThrow();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=scope-encryption.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-encryption.test.js","sourceRoot":"","sources":["../../src/tests/scope-encryption.test.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -120,7 +120,11 @@ export declare const MemoryItemSchema: z.ZodObject<{
|
|
|
120
120
|
related_ids: string[];
|
|
121
121
|
embedding_model: string;
|
|
122
122
|
}>;
|
|
123
|
-
|
|
123
|
+
/** Runtime type for a memory item. scope is optional — defaults to 'personal'. */
|
|
124
|
+
export type MemoryItem = z.infer<typeof MemoryItemSchema> & {
|
|
125
|
+
/** Scope: 'personal' (default) or 'workspace:<ulid>' for team shared memories. */
|
|
126
|
+
scope?: string;
|
|
127
|
+
};
|
|
124
128
|
export interface IngestInput {
|
|
125
129
|
content: string;
|
|
126
130
|
source_id: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU3B,CAAC;AAEH,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,GAAG;IAC1D,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,WAAW,WAAW;IAE1B,OAAO,EAAE,MAAM,CAAC;IAEhB,SAAS,EAAE,MAAM,CAAC;IAElB,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../../src/webapp/api/team.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAUjC,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAsD/D"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/webapp/api/team.ts
|
|
2
|
+
// Local HTTP endpoints for workspace (team) key operations.
|
|
3
|
+
// Called by the engram-mcp.com browser UI to perform key wrapping
|
|
4
|
+
// (the browser cannot hold the master key — it lives in this local process).
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import { createLogger } from '../../logger.js';
|
|
7
|
+
import { loadWorkspaceKey, wrapKeyForRecipient, getOrCreateX25519Keypair, } from '../../memory/modules/team/keystore.js';
|
|
8
|
+
const log = createLogger('api:team');
|
|
9
|
+
export function buildTeamRouter(opts) {
|
|
10
|
+
const router = Router();
|
|
11
|
+
// POST /api/team/wrap-key
|
|
12
|
+
// Body: { workspace_id: string; recipient_pubkey: string } (hex)
|
|
13
|
+
// Returns: { wrapped_team_key: string } (hex)
|
|
14
|
+
// Used by engram-mcp.com to wrap the workspace master key before sending an invitation.
|
|
15
|
+
router.post('/wrap-key', async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const { workspace_id, recipient_pubkey } = req.body;
|
|
18
|
+
if (!workspace_id || !recipient_pubkey) {
|
|
19
|
+
return res.status(400).json({ error: 'workspace_id and recipient_pubkey required' });
|
|
20
|
+
}
|
|
21
|
+
if (!/^[0-9a-f]{64}$/i.test(recipient_pubkey)) {
|
|
22
|
+
return res.status(400).json({ error: 'recipient_pubkey must be 64 hex chars (32 bytes)' });
|
|
23
|
+
}
|
|
24
|
+
const teamKey = await loadWorkspaceKey(opts.dataDir, workspace_id);
|
|
25
|
+
if (!teamKey) {
|
|
26
|
+
return res.status(404).json({
|
|
27
|
+
error: 'workspace_key_not_found',
|
|
28
|
+
message: 'Local workspace key not found. Are you the owner?',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const wrappedHex = await wrapKeyForRecipient(teamKey, recipient_pubkey);
|
|
32
|
+
log.info(`Wrapped workspace key for workspace ${workspace_id}`);
|
|
33
|
+
return res.json({ wrapped_team_key: wrappedHex });
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
log.error('wrap-key error', e);
|
|
37
|
+
return res.status(500).json({ error: 'internal_error' });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// GET /api/team/pubkey
|
|
41
|
+
// Returns the local X25519 public key (hex).
|
|
42
|
+
// Used for registration with engram-cloud after pairing.
|
|
43
|
+
router.get('/pubkey', async (_req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const keypair = await getOrCreateX25519Keypair(opts.dataDir, opts.masterKey);
|
|
46
|
+
const { default: sodium } = await import('libsodium-wrappers');
|
|
47
|
+
await sodium.ready;
|
|
48
|
+
return res.json({ x25519_pubkey: sodium.to_hex(keypair.publicKey) });
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
log.error('pubkey error', e);
|
|
52
|
+
return res.status(500).json({ error: 'internal_error' });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return router;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=team.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team.js","sourceRoot":"","sources":["../../../src/webapp/api/team.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/CLAUDE.md
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
# EngramMCP
|
|
2
|
-
|
|
3
|
-
> Local-first semantic memory layer for AI agents.
|
|
4
|
-
|
|
5
|
-
EngramMCP exposes a Model Context Protocol (MCP) server that lets any AI agent (Claude Code, Cursor, custom Claude/GPT runtimes) capture, search, and enrich memory across multiple typed sources (notes, conversations, Drive, Notion, YouTube, Obsidian, audio, plus user-defined custom types).
|
|
6
|
-
|
|
7
|
-
Forked from Argos. Local-first: vectors + content live on the user's machine; embeddings via local Ollama by default; no cloud required for Phase 1.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Instructions for Claude Code
|
|
12
|
-
|
|
13
|
-
Tu es le stagiaire technique d'EngramMCP. Sois **proactif**, **force de proposition**, et **autonome**.
|
|
14
|
-
|
|
15
|
-
### Comportement attendu
|
|
16
|
-
|
|
17
|
-
- **Lis le code existant** avant de modifier — comprends le pattern en place
|
|
18
|
-
- **Respecte les conventions** (voir ci-dessous)
|
|
19
|
-
- **Pense modularité** — chaque memory type est un MemoryModule indépendant
|
|
20
|
-
- **Sois concis** — pas de blabla, va droit au but, montre le code
|
|
21
|
-
- **Challenge les décisions** sous-optimales avec arguments
|
|
22
|
-
- **No mocks** pour les intégrations externes — tests E2E avec Ollama/LanceDB/SQLite/API réels
|
|
23
|
-
|
|
24
|
-
### Quand tu travailles sur EngramMCP
|
|
25
|
-
|
|
26
|
-
1. Lis toujours le code existant avant de modifier
|
|
27
|
-
2. Respecte la convention ES modules avec extensions `.js` dans les imports
|
|
28
|
-
3. Property extraction est **off par défaut** — l'agent appelant (toi) fournit `title`/`tags` directement
|
|
29
|
-
4. Vector store dimension est hardcoded à 768 (nomic-embed-text). Voyage (1024)/OpenAI (1536) requiert un fix dans `src/vector/store.ts`
|
|
30
|
-
5. Tool descriptions doivent être agent-friendly (mention WHEN to call, WHAT inputs help retrieval)
|
|
31
|
-
6. **v0.2 surface**: 10 public tools only. Admin tools behind `--admin` flag. See SKILL.md for agent usage guide.
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## Quick Reference
|
|
36
|
-
|
|
37
|
-
### Commands
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
npm run dev # tsx watch src/scripts/serve.ts
|
|
41
|
-
npm run build # tsc → dist/
|
|
42
|
-
npm run build:client # Vite → src/client/dist/
|
|
43
|
-
npm run build:all # client + server
|
|
44
|
-
npm start # node dist/scripts/serve.js
|
|
45
|
-
npm run reindex # re-embed all memories after provider change
|
|
46
|
-
npm run install:wizard # interactive first-time setup (tsx)
|
|
47
|
-
npm run pair # interactive pairing wizard (engram-mcp-pair)
|
|
48
|
-
npm run rebuild # rebuild SQLite+LanceDB from ops_log (engram-mcp rebuild)
|
|
49
|
-
npm test # vitest run
|
|
50
|
-
npm run lint # eslint src/
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Tech Stack
|
|
54
|
-
|
|
55
|
-
| Layer | Tech |
|
|
56
|
-
|---|---|
|
|
57
|
-
| Runtime | Node.js >= 22, TypeScript 5.7 strict, ESM |
|
|
58
|
-
| MCP | `@modelcontextprotocol/sdk@1.28` (stdio + StreamableHTTP) |
|
|
59
|
-
| Vector | LanceDB (`@lancedb/lancedb@0.27`) per-type tables |
|
|
60
|
-
| SQL | better-sqlite3 + FTS5 |
|
|
61
|
-
| Embeddings | OpenAI-compat dispatch → ollama / engram / voyage / openai / openai-compatible |
|
|
62
|
-
| Audio | nodejs-whisper (whisper.cpp) |
|
|
63
|
-
| YouTube | watch-page scrape + yt-dlp fallback |
|
|
64
|
-
| Web | Express 5, ws, React 19, Vite 5, Tailwind v4, react-query |
|
|
65
|
-
| Validation | Zod everywhere |
|
|
66
|
-
| IDs | ULID |
|
|
67
|
-
|
|
68
|
-
### Key conventions
|
|
69
|
-
|
|
70
|
-
- **Module pattern**: `"type": "module"`, imports with `.js` extension
|
|
71
|
-
- **Config**: Zod schema in `src/config/schema.ts`, loaded from `~/.engram/config.json` or env (`ENGRAM_CONFIG_DIR`, `DATA_DIR`)
|
|
72
|
-
- **DB**: `getDb()` singleton, prepared statements only
|
|
73
|
-
- **Logging**: `createLogger(scope)` from `src/logger.ts`
|
|
74
|
-
- **Tool descriptions**: agent-facing — explain WHEN to use the tool and what title/tags improve retrieval
|
|
75
|
-
- **Tests**: real services (Ollama, LanceDB, Notion sandbox, etc.). No mocks. Real-API tests use ephemeral tmpdirs.
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## Architecture (Phase 1)
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
┌─ stdio ─┐
|
|
83
|
-
agent runtime ──┤ ├──▶ MCP server (engram-mcp)
|
|
84
|
-
└─ HTTP ─┘ │
|
|
85
|
-
▼
|
|
86
|
-
┌─ ToolRouter ─┐
|
|
87
|
-
│ 10 tools │ (+18 admin with --admin flag)
|
|
88
|
-
└──────┬───────┘
|
|
89
|
-
▼
|
|
90
|
-
┌─ ModuleRegistry ─┐
|
|
91
|
-
│ notes │
|
|
92
|
-
│ conversations │
|
|
93
|
-
│ drive (OAuth) │
|
|
94
|
-
│ notion (OAuth) │
|
|
95
|
-
│ youtube │
|
|
96
|
-
│ audio (Whisper) │
|
|
97
|
-
│ obsidian │
|
|
98
|
-
│ <custom types> │
|
|
99
|
-
└────────┬─────────┘
|
|
100
|
-
▼
|
|
101
|
-
┌─ MemoryStore ─┐
|
|
102
|
-
│ insert │ ──▶ chunk + embed (Ollama)
|
|
103
|
-
│ search │ ──▶ LanceDB per-type tables
|
|
104
|
-
│ getById │ ──▶ SQLite + FTS5
|
|
105
|
-
│ delete │
|
|
106
|
-
│ findRelated │
|
|
107
|
-
│ setProperties│
|
|
108
|
-
└────────┬──────┘
|
|
109
|
-
│ events: memory.added/deleted/updated
|
|
110
|
-
▼
|
|
111
|
-
WebSocket /ws ──▶ dashboard React UI
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Cloud bits (Phase 2 — Plan K): `engram-mcp pair` CLI activates cloud transit poller (node-cron,
|
|
115
|
-
5 min), Bridge Relay WSS client, and E2E encrypted blob dispatch from the Engram cloud inbox.
|
|
116
|
-
Set ENGRAM_PASSPHRASE env var for the transit poller to start automatically at server boot.
|
|
117
|
-
Mobile app, billing, and Engram-hosted embeddings server are in separate Plans (I, J, M).
|
|
118
|
-
|
|
119
|
-
Phase 3 — Plan N (ops-log sync): Every local write (insert/delete/setProperties) is logged as a
|
|
120
|
-
signed, AES-256-GCM-encrypted op in `ops_log` (SQLite). A `ChannelClient` pushes pending ops to
|
|
121
|
-
the cloud `UserSyncChannel` Durable Object (engram-cloud) and receives ops from other devices.
|
|
122
|
-
The `ReplayApplier` decrypts, verifies ed25519 signatures, and applies ops with LWW + union
|
|
123
|
-
conflict resolution. `engram-mcp rebuild` drops SQLite+LanceDB and replays all ops from scratch.
|
|
124
|
-
Enable via `config.json: {"sync": {"enabled": true, "cloudBaseUrl": "..."}}` + ENGRAM_PASSPHRASE.
|
|
125
|
-
Full multi-PC sync requires Plan O cloud worker deployed (see RUNBOOK.md).
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## Project Structure
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
src/
|
|
133
|
-
├── types.ts # MemoryItem schema (Zod) + interfaces
|
|
134
|
-
├── logger.ts # createLogger
|
|
135
|
-
├── index.ts # (stub — entry is src/scripts/serve.ts)
|
|
136
|
-
├── config/{schema,index}.ts # Zod config + loader
|
|
137
|
-
├── db/index.ts # SQLite init + 3 migrations
|
|
138
|
-
├── embeddings/
|
|
139
|
-
│ ├── index.ts # dispatcher
|
|
140
|
-
│ └── providers/{ollama,engram,voyage,openai,openai-compat}.ts
|
|
141
|
-
├── vector/store.ts # LanceDB per-type tables
|
|
142
|
-
├── memory/
|
|
143
|
-
│ ├── core/{chunker,wikilinks,module-interface,module-registry,
|
|
144
|
-
│ │ store,source-registry,property-extractor,reindex}.ts
|
|
145
|
-
│ ├── modules/
|
|
146
|
-
│ │ ├── notes/ # add_note, search_notes
|
|
147
|
-
│ │ ├── conversations/ # remember_exchange, search_conversations
|
|
148
|
-
│ │ ├── drive/ # OAuth + watcher + ingest/list/watch tools
|
|
149
|
-
│ │ ├── notion/ # OAuth + watcher + ingest/list/watch tools
|
|
150
|
-
│ │ ├── audio/ # Whisper.cpp + add_audio_file/search_audio
|
|
151
|
-
│ │ ├── youtube/ # transcript fetcher + add_youtube_url/search
|
|
152
|
-
│ │ ├── obsidian/ # vault reader + fs.watch + tools
|
|
153
|
-
│ │ └── _custom/ # generic-module factory + create/list/delete
|
|
154
|
-
│ ├── public/tools.ts # 10 public tools: remember, recall, get, update,
|
|
155
|
-
│ │ # forget, relate, list_types, recent, ingest,
|
|
156
|
-
│ │ # suggest_properties
|
|
157
|
-
│ └── admin/tools.ts # ~18 admin tools behind --admin flag:
|
|
158
|
-
├── core/ # LOW-LEVEL UTILITIES (canonical)
|
|
159
|
-
│ ├── logger.ts # createLogger (was src/logger.ts)
|
|
160
|
-
│ ├── db/index.ts # SQLite init + migrations (was src/db/index.ts)
|
|
161
|
-
│ └── server/
|
|
162
|
-
│ ├── mcp-handler.ts # buildEngramRuntime + startStdioMcpServer
|
|
163
|
-
│ ├── tool-router.ts # ToolRouter
|
|
164
|
-
│ ├── http.ts # Express bootstrap (was src/webapp/server.ts)
|
|
165
|
-
│ ├── mcp-http.ts # StreamableHTTP MCP transport
|
|
166
|
-
│ └── websocket.ts # WS broadcaster for memory.* events
|
|
167
|
-
├── tools/ # Top-level tool registry (mirrors predmcp pattern)
|
|
168
|
-
│ └── index.ts # registerAllTools + dynamic private import
|
|
169
|
-
├── private/ # GITIGNORED — premium extensions (hosted Engram only)
|
|
170
|
-
│ ├── index.ts # registerPrivateExtensions (no-op placeholder)
|
|
171
|
-
│ ├── README.md # Explains what goes here
|
|
172
|
-
│ ├── tools/ # Premium MCP tools
|
|
173
|
-
│ ├── algorithms/ # Advanced algorithm overrides
|
|
174
|
-
│ └── prompts/ # Tuned LLM prompts (IP)
|
|
175
|
-
├── server/ # Engram-specific HTTP routes
|
|
176
|
-
│ ├── index.ts # Re-exports from core/server/http.ts
|
|
177
|
-
│ └── api/{memories,sources,types,views,daily,settings,reindex,sync-status,graph}.ts
|
|
178
|
-
├── mcp-server/ # Re-export shims (→ core/server/)
|
|
179
|
-
│ ├── server.ts # shim → core/server/mcp-handler.ts
|
|
180
|
-
│ └── tool-router.ts # shim → core/server/tool-router.ts
|
|
181
|
-
├── webapp/ # Re-export shims (→ core/server/)
|
|
182
|
-
│ ├── server.ts # shim → core/server/http.ts
|
|
183
|
-
│ ├── mcp-http.ts # shim → core/server/mcp-http.ts
|
|
184
|
-
│ ├── websocket.ts # shim → core/server/websocket.ts
|
|
185
|
-
│ └── api/ # original api handlers (not shimmed — used by tests)
|
|
186
|
-
├── client/ # React/Vite/Tailwind dashboard
|
|
187
|
-
│ ├── vite.config.ts
|
|
188
|
-
│ ├── tsconfig.json (separate)
|
|
189
|
-
│ └── src/{App,main,api,ws,index.css}.{ts,tsx,css}
|
|
190
|
-
└── scripts/{serve,install,install-ollama,reindex}.ts
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
## Database Schema
|
|
196
|
-
|
|
197
|
-
SQLite at `<dataDir>/engram.db` (WAL mode, FOREIGN KEYS ON):
|
|
198
|
-
|
|
199
|
-
| Table | Purpose |
|
|
200
|
-
|---|---|
|
|
201
|
-
| `memories` | id, type, source_id, content, content_hash, properties_json, wikilinks_json, related_ids_json, embedding_model, created_at |
|
|
202
|
-
| `memories_fts` | FTS5 virtual table on content/title/tags |
|
|
203
|
-
| `custom_types` | type_name, display_name, schema_json, created_at |
|
|
204
|
-
| `oauth_tokens` | provider, access_token, refresh_token, expires_at, extra_json |
|
|
205
|
-
| `module_state` | (module_id, key) → value_json |
|
|
206
|
-
| `watched_sources` | id, module_id, external_id, display_name, config_json, last_synced_at, last_modified_remote, last_error, enabled |
|
|
207
|
-
| `saved_views` | id, name, description, definition_json, pinned, created_at, updated_at |
|
|
208
|
-
| `settings` | key → value_json |
|
|
209
|
-
|
|
210
|
-
Vector tables: `memories_<type>` per memory type in LanceDB (id, source_id, chunk_index, content, created_at, field1-4, vector 768-d).
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
|
-
## Pricing (planned)
|
|
215
|
-
|
|
216
|
-
- **Free**: full local — Ollama embeddings, all modules, ~36 MCP tools
|
|
217
|
-
- **Pro $9/mo** (Phase 2): mobile app, cloud transit, share-by-email, Engram hosted embeddings, multi-PC encrypted sync, online dashboard
|
|
218
|
-
- **BYO premium embeddings** at any tier: Voyage / OpenAI / OAI-compatible
|
|
219
|
-
|
|
220
|
-
---
|
|
221
|
-
|
|
222
|
-
## Phase status
|
|
223
|
-
|
|
224
|
-
- [x] **Phase 1** — local MCP server with all memory types (DONE — see docs/superpowers/plans/)
|
|
225
|
-
- [ ] **Phase 2** — Mobile + cloud transit + billing + hosted Engram embeddings server
|
|
226
|
-
- [ ] **Phase 3** — Bidirectional multi-PC sync (ops-log + cloud saves) + recovery shards
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
## License
|
|
231
|
-
|
|
232
|
-
MIT
|