1818 * @module memory/facade/Memory
1919 */
2020
21- import crypto from 'node:crypto' ;
22- import fs from 'node:fs/promises' ;
23- import os from 'node:os' ;
24- import path from 'node:path' ;
21+ import { sha256 as crossSha256 , uuid } from '../util/crossPlatformCrypto.js' ;
22+
23+ const _isNode = typeof process !== 'undefined' && ! ! process . versions ?. node ;
24+
25+ async function _getFs ( ) : Promise < typeof import ( 'node:fs/promises' ) > {
26+ if ( ! _isNode ) throw new Error ( 'Filesystem operations are not available in browser environments.' ) ;
27+ return import ( 'node:fs/promises' ) ;
28+ }
29+
30+ async function _getPath ( ) : Promise < typeof import ( 'node:path' ) > {
31+ if ( ! _isNode ) throw new Error ( 'Path operations are not available in browser environments.' ) ;
32+ return import ( 'node:path' ) ;
33+ }
34+
35+ async function _getOs ( ) : Promise < typeof import ( 'node:os' ) > {
36+ if ( ! _isNode ) throw new Error ( 'OS operations are not available in browser environments.' ) ;
37+ return import ( 'node:os' ) ;
38+ }
2539
2640import type { MemoryTrace } from '../types.js' ;
2741import type { ITool } from '../../core/tools/ITool.js' ;
@@ -129,19 +143,19 @@ interface FtsJoinRow extends TraceRow {
129143// ---------------------------------------------------------------------------
130144
131145/**
132- * Generate a globally unique, collision-free trace ID using crypto.randomUUID() .
146+ * Generate a globally unique, collision-free trace ID.
133147 * Previous implementation used a monotonic counter (`mt_{timestamp}_{counter}`)
134148 * which could collide across multiple processes or rapid restarts.
135149 */
136150function nextTraceId ( ) : string {
137- return `mt_${ crypto . randomUUID ( ) } ` ;
151+ return `mt_${ uuid ( ) } ` ;
138152}
139153
140154/**
141155 * Compute SHA-256 hex digest of a string.
142156 */
143- function sha256 ( content : string ) : string {
144- return crypto . createHash ( 'sha256' ) . update ( content ) . digest ( 'hex' ) ;
157+ async function sha256 ( content : string ) : Promise < string > {
158+ return crossSha256 ( content ) ;
145159}
146160
147161// ---------------------------------------------------------------------------
@@ -275,9 +289,17 @@ export class Memory {
275289 static async create ( config ?: MemoryConfig ) : Promise < Memory > {
276290 // Step 1: merge with defaults.
277291 const randomSuffix = Math . random ( ) . toString ( 36 ) . slice ( 2 , 10 ) ;
292+ let defaultPath : string ;
293+ if ( _isNode ) {
294+ const osModule = await _getOs ( ) ;
295+ const pathModule = await _getPath ( ) ;
296+ defaultPath = pathModule . join ( osModule . tmpdir ( ) , `brain-${ randomSuffix } .sqlite` ) ;
297+ } else {
298+ defaultPath = `brain-${ randomSuffix } .sqlite` ;
299+ }
278300 const merged = {
279301 store : 'sqlite' as const ,
280- path : path . join ( os . tmpdir ( ) , `brain- ${ randomSuffix } .sqlite` ) ,
302+ path : defaultPath ,
281303 graph : true ,
282304 selfImprove : true ,
283305 decay : true ,
@@ -326,7 +348,7 @@ export class Memory {
326348 async remember ( content : string , options ?: RememberOptions ) : Promise < MemoryTrace > {
327349 await this . _initPromise ;
328350
329- const contentHash = sha256 ( content ) ;
351+ const contentHash = await sha256 ( content ) ;
330352 const type = options ?. type ?? 'episodic' ;
331353 const scope = options ?. scope ?? 'user' ;
332354 const scopeId = options ?. scopeId ?? '' ;
@@ -664,7 +686,8 @@ export class Memory {
664686
665687 try {
666688 // Detect source type.
667- const stat = await fs . stat ( source ) . catch ( ( ) => null ) ;
689+ const fsModule = await _getFs ( ) ;
690+ const stat = await fsModule . stat ( source ) . catch ( ( ) => null ) ;
668691
669692 if ( stat ?. isDirectory ( ) ) {
670693 // Directory scan.
@@ -925,7 +948,7 @@ export class Memory {
925948 async export ( outputPath : string , options ?: ExportOptions ) : Promise < void > {
926949 await this . _initPromise ;
927950
928- const format = this . _detectExportFormat ( outputPath , options ) ;
951+ const format = await this . _detectExportFormat ( outputPath , options ) ;
929952
930953 switch ( format ) {
931954 case 'json' : {
@@ -1001,6 +1024,47 @@ export class Memory {
10011024 return result ;
10021025 }
10031026
1027+ /**
1028+ * Import memory data from a string without filesystem access.
1029+ *
1030+ * Supports JSON and CSV formats. Useful in browser environments or when
1031+ * the data is already in memory.
1032+ *
1033+ * @param content - The raw string content to import.
1034+ * @param format - The format of the content: `'json'` or `'csv'`.
1035+ * @returns Summary of the import operation.
1036+ */
1037+ async importFromString ( content : string , format : 'json' | 'csv' ) : Promise < ImportResult > {
1038+ await this . _initPromise ;
1039+
1040+ let result : ImportResult ;
1041+ if ( format === 'json' ) {
1042+ result = await new JsonImporter ( this . _brain ) . importFromString ( content ) ;
1043+ } else {
1044+ result = await new CsvImporter ( this . _brain ) . importFromString ( content ) ;
1045+ }
1046+
1047+ if ( result . imported > 0 ) {
1048+ await this . _rebuildFtsIndex ( ) ;
1049+ }
1050+
1051+ return result ;
1052+ }
1053+
1054+ /**
1055+ * Export the full brain state as a JSON string without filesystem access.
1056+ *
1057+ * Useful in browser environments or when the data needs to be sent over
1058+ * a network connection.
1059+ *
1060+ * @param options - Optional export configuration (embeddings, conversations).
1061+ * @returns Pretty-printed JSON string of the full brain payload.
1062+ */
1063+ async exportToString ( options ?: ExportOptions ) : Promise < string > {
1064+ await this . _initPromise ;
1065+ return new JsonExporter ( this . _brain ) . exportToString ( options ) ;
1066+ }
1067+
10041068 // =========================================================================
10051069 // Tool integration
10061070 // =========================================================================
@@ -1300,7 +1364,7 @@ export class Memory {
13001364 } ,
13011365 result : IngestResult ,
13021366 ) : Promise < void > {
1303- const contentHash = sha256 ( doc . content ) ;
1367+ const contentHash = await sha256 ( doc . content ) ;
13041368 const existingDoc = await this . _brain . get < { id : string } > (
13051369 `SELECT id FROM documents WHERE content_hash = ? LIMIT 1` ,
13061370 [ contentHash ] ,
@@ -1311,7 +1375,7 @@ export class Memory {
13111375 }
13121376
13131377 const chunks = await this . _chunkingEngine . chunk ( doc . content , chunking ) ;
1314- const docId = `doc_${ crypto . randomUUID ( ) } ` ;
1378+ const docId = `doc_${ uuid ( ) } ` ;
13151379
13161380 await this . _brain . run (
13171381 `INSERT INTO documents
@@ -1330,7 +1394,7 @@ export class Memory {
13301394 ) ;
13311395
13321396 for ( const chunk of chunks ) {
1333- const chunkId = `chunk_${ crypto . randomUUID ( ) } ` ;
1397+ const chunkId = `chunk_${ uuid ( ) } ` ;
13341398 const traceId = nextTraceId ( ) ;
13351399 const createdAt = Date . now ( ) ;
13361400
@@ -1349,7 +1413,7 @@ export class Memory {
13491413 document_id : docId ,
13501414 chunk_index : chunk . index ,
13511415 } ,
1352- { contentHash : sha256 ( chunk . content ) } ,
1416+ { contentHash : await sha256 ( chunk . content ) } ,
13531417 ) ,
13541418 ) ,
13551419 ] ,
@@ -1402,13 +1466,14 @@ export class Memory {
14021466 /**
14031467 * Detect the export format from options or file extension.
14041468 */
1405- private _detectExportFormat (
1469+ private async _detectExportFormat (
14061470 outputPath : string ,
14071471 options ?: ExportOptions ,
1408- ) : 'json' | 'markdown' | 'obsidian' | 'sqlite' {
1472+ ) : Promise < 'json' | 'markdown' | 'obsidian' | 'sqlite' > {
14091473 if ( options ?. format ) return options . format ;
14101474
1411- const ext = path . extname ( outputPath ) . toLowerCase ( ) ;
1475+ const pathModule = await _getPath ( ) ;
1476+ const ext = pathModule . extname ( outputPath ) . toLowerCase ( ) ;
14121477 switch ( ext ) {
14131478 case '.json' : return 'json' ;
14141479 case '.sqlite' :
@@ -1426,12 +1491,14 @@ export class Memory {
14261491 ) : Promise < 'json' | 'markdown' | 'obsidian' | 'sqlite' | 'chatgpt' | 'csv' > {
14271492 if ( options ?. format && options . format !== 'auto' ) return options . format ;
14281493
1429- const ext = path . extname ( source ) . toLowerCase ( ) ;
1494+ const pathModule = await _getPath ( ) ;
1495+ const ext = pathModule . extname ( source ) . toLowerCase ( ) ;
14301496 switch ( ext ) {
14311497 case '.json' : {
14321498 // Check if it looks like a ChatGPT export.
14331499 try {
1434- const head = await fs . readFile ( source , { encoding : 'utf8' , flag : 'r' } ) ;
1500+ const fsModule = await _getFs ( ) ;
1501+ const head = await fsModule . readFile ( source , { encoding : 'utf8' , flag : 'r' } ) ;
14351502 if ( head . includes ( '"mapping"' ) && head . includes ( '"conversation_id"' ) ) {
14361503 return 'chatgpt' ;
14371504 }
@@ -1444,7 +1511,8 @@ export class Memory {
14441511 default : {
14451512 // Check if source is a directory.
14461513 try {
1447- const stat = await fs . stat ( source ) ;
1514+ const fsModule = await _getFs ( ) ;
1515+ const stat = await fsModule . stat ( source ) ;
14481516 if ( stat . isDirectory ( ) ) return 'markdown' ;
14491517 } catch { /* fall through */ }
14501518 return 'json' ;
0 commit comments