[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\n# yes, we use tabs\nindent_style = tab\n\n# To control tab size in github diffs\n# See https://github.com/isaacs/github/issues/170#issuecomment-150489692\nindent_size = 4\n\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.yml]\nindent_size = 2\nindent_style = space\n"
  },
  {
    "path": ".gitignore",
    "content": "tmp\nsecrets.json\n.DS_Store\nnode_modules\n"
  },
  {
    "path": ".prettierrc",
    "content": "useTabs: true\ntabWidth: 2\nprintWidth: 100\nsingleQuote: true\nbracketSpacing: true\nparenSpacing: true\njsxBracketSameLine: false\nsemi: true\n"
  },
  {
    "path": "EvernoteSync.js",
    "content": "const Evernote = require( 'evernote' );\nconst ENML = require( 'enml-js' );\nconst RoamSyncAdapter = require( './Sync' );\nconst moment = require( 'moment' );\n\nclass EvernoteSyncAdapter extends RoamSyncAdapter {\n\tEvernoteClient = null;\n\tNoteStore = null;\n\tnotebookGuid = '';\n\tdefaultNotebook = '';\n\ttimeOut = 500;\n\tmapping;\n\tbacklinks = {};\n\tnotesBeingImported = [];\n\n\twrapItem( string, title ) {\n\t\treturn `<li>${ string }</li>`;\n\t}\n\twrapText( string, title ) {\n\t\tconst backlinks = [];\n\t\tstring = this.htmlEntities( string );\n\t\tstring = string.replace( '{{[[TODO]]}}', '[[TODO]]' ); // <en-todo/> will not achieve the same goal.\n\t\tstring = string.replace( '{{{[[DONE]]}}}}', '<en-todo checked=\"true\"/>' );\n\t\tstring = string.replace( /\\!\\[([^\\]]*?)\\]\\(([^\\)]+)\\)/g, '<img src=\"$2\"/>' );\n\t\tstring = string.replace( /\\[([^\\]]+)\\]\\((http|evernote)([^\\)]+)\\)/g, '<a href=\"$2$3\">$1</a>' );\n\t\tstring = string.replace( /(^|[^\"?/])((evernote|http|https|mailto):[a-zA-Z0-9\\/.\\?\\&=;\\-_]+)/g, '$1<a href=\"$2\">$2</a>' );\n\t\tstring = string.replace( /\\*\\*([^*]+)\\*\\*/g, '<b>$1</b>' );\n\t\tstring = string.replace( /__([^_]+)__/g, '<i>$1</i>' );\n\t\tstring = string.replace( /#?\\[\\[([^\\]]+)\\]\\]/g, ( match, contents ) => {\n\t\t\tconst targetPage = this.titleMapping.get( contents );\n\t\t\tif (\n\t\t\t\ttargetPage &&\n\t\t\t\ttargetPage.uid &&\n\t\t\t\tthis.mapping.get( targetPage.uid )\n\t\t\t) {\n\t\t\t\tconst guid = this.mapping.get( targetPage.uid ).guid;\n\t\t\t\tconst url = this.getNoteUrl( guid );\n\t\t\t\tbacklinks.push( contents );\n\t\t\t\treturn `<a href=\"${ url }\">${ contents }</a>`;\n\t\t\t}\n\t\t\treturn match;\n\t\t} );\n\t\tthis.addBacklink( backlinks, title, string );\n\t\treturn string;\n\t}\n\n\twrapChildren( childrenString ) {\n\t\tchildrenString = childrenString.join( '' );\n\t\treturn `<ul>${ childrenString }</ul>`;\n\t}\n\thtmlEntities( str ) {\n\t\treturn String( str )\n\t\t\t.replace( /&/g, '&amp;' )\n\t\t\t.replace( /</g, '&lt;' )\n\t\t\t.replace( />/g, '&gt;' )\n\t\t\t.replace( /\"/g, '&quot;' );\n\t}\n\thtmlEntitiesDecode( str ) {\n\t\treturn String( str )\n\t\t\t.replace( '&amp;', '&' )\n\t\t\t.replace( '&lt;', '<' )\n\t\t\t.replace( '&gt;', '>' )\n\t\t\t.replace( '&quot;', '\"' );\n\t}\n\twrapNote( noteBody ) {\n\t\tnoteBody = noteBody.replace( '&Amp;', '&amp;' ); // Readwise artifact\n\t\tvar nBody = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>';\n\t\tnBody += '<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">';\n\t\tnBody += '<en-note>' + noteBody;\n\t\tnBody += '</en-note>';\n\t\treturn nBody;\n\t}\n\n\tmakeNote( noteTitle, noteBody, url, uid ) {\n\t\t// Create note object\n\t\tvar nBody = this.wrapNote( noteBody );\n\t\tlet foundNote;\n\n\t\tif ( this.mapping.get( uid ) ) {\n\t\t\tfoundNote = Promise.resolve( { \n\t\t\t\ttotalNotes: 1,\n\t\t\t\tnotes: [ this.mapping.get( uid ) ]\n\t\t\t} );\n\t\t} else {\n\t\t\tfoundNote = this.findPreviousNote( url );\n\t\t}\n\n\t\tconst foundNotes = ( notes ) => {\n\t\t\tif ( ! notes.totalNotes ) {\n\t\t\t\treturn Promise.resolve( new Evernote.Types.Note() );\n\t\t\t}\n\t\t\tif ( notes.notes[0].contentLength === nBody.length ) {\n\t\t\t\t//console.log( '[[' + noteTitle + ']]: content length has not changed, skipping' );\n\t\t\t\treturn Promise.resolve( false );\n\t\t\t}\n\t\t\t// These request we want to rate limit.\n\t\t\treturn new Promise( ( resolve, reject ) => setTimeout( () => {\n\t\t\t\tthis.NoteStore.getNote( notes.notes[0].guid, true, false, false, false )\n\t\t\t\t\t.catch( ( err ) => {\n\t\t\t\t\t\tconsole.warn( '[[' + noteTitle + ']] :Took too long to pull note ' );\n\t\t\t\t\t\tif ( err.code === 'ETIMEDOUT' ) {\n\t\t\t\t\t\t\tthis.timeOut = this.timeOut * 2;\n\t\t\t\t\t\t\tconsole.log( 'Exponential Backoff increased: ', this.timeOut );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( this.timeOut > 60000 * 5 ) {\n\t\t\t\t\t\t\tconsole.error( 'Timeot reached 5 minutes. Exiting' );\n\t\t\t\t\t\t\trequire('process').exit();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve( foundNotes( notes ) );\n\t\t\t\t\t} )\n\t\t\t\t\t.then( result => resolve( result ) );\n\t\t\t}, this.timeOut ) );\n\n\t\t}\n\n\t\tfoundNote.then( foundNotes )\n\t\t.then( ( ourNote ) => {\n\t\t\tif ( ! ourNote ) {\n\t\t\t\treturn Promise.resolve( false );\n\t\t\t}\n\t\t\t// Build body of note\n\t\t\tif ( ourNote.content && ourNote.content === nBody ) {\n\t\t\t\tconsole.log( '[[' + noteTitle + ']]: has not changed, skipping' );\n\t\t\t\treturn Promise.resolve( ourNote );\n\t\t\t}\n\t\t\tourNote.content = nBody;\n\n\t\t\tconst attributes = new Evernote.Types.NoteAttributes();\n\t\t\tattributes.contentClass = 'piszek.roam';\n\t\t\tattributes.sourceURL = url;\n\t\t\tattributes.sourceApplication = 'piszek.roam';\n\t\t\tourNote.attributes = attributes;\n\t\t\tourNote.title = this.htmlEntities( noteTitle.trim() );\n\n\t\t\tif ( ourNote.guid ) {\n\t\t\t\tconsole.log( '[[' + noteTitle + ']]: updating' );\n\t\t\t\tourNote.updated = Date.now();\n\t\t\t\treturn this.NoteStore.updateNote( ourNote ).catch( err => {\n\t\t\t\t\tconsole.log( 'Update note problem', err, nBody );\n\t\t\t\t\treturn Promise.resolve( false );\n\t\t\t\t} );\n\t\t\t} else {\n\t\t\t\t// parentNotebook is optional; if omitted, default notebook is used\n\t\t\t\tif ( this.notebookGuid ) {\n\t\t\t\t\tourNote.notebookGuid = this.notebookGuid;\n\t\t\t\t}\n\t\t\t\tconsole.log( '[[' + noteTitle + ']] Creating new note ' );\n\t\t\t\treturn this.NoteStore.createNote( ourNote ).then( ( note ) => {\n\t\t\t\t\tthis.mapping.set( uid, {\n\t\t\t\t\t\tguid: note.guid,\n\t\t\t\t\t\ttitle: note.title,\n\t\t\t\t\t\tcontentLength: note.contentLength\n\t\t\t\t\t} );\n\t\t\t\t\treturn Promise.resolve( note );\n\t\t\t\t} ).catch( err => {\n\t\t\t\t\tconsole.warn( 'Error creating note:', err );\n\t\t\t\t\tPromise.resolve( false );\n\t\t\t\t});\n\t\t\t}\n\t\t} );\n\t}\n\n\tfindNotebook() {\n\t\treturn new Promise( ( resolve, reject ) => {\n\t\t\tthis.NoteStore.listNotebooks().then( ( notebooks ) => {\n\t\t\t\tconst filtered = notebooks.filter( ( nb ) => nb.name === 'Roam' );\n\t\t\t\tconst def = notebooks.filter( ( nb ) => nb.defaultNotebook );\n\t\t\t\tthis.defaultNotebook = def[ 0 ].guid\n\t\t\t\tif ( filtered ) {\n\t\t\t\t\tthis.notebookGuid = filtered[ 0 ].guid;\n\t\t\t\t\tresolve( this.notebookGuid );\n\t\t\t\t} else {\n\t\t\t\t\tconsole.warn( 'You have to have a notebook named \"Roam\"' );\n\t\t\t\t\treject( 'You have to have a notebook named \"Roam\"' );\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\t}\n\tgetNotesToImport() {\n\t\tconst filter = new Evernote.NoteStore.NoteFilter();\n\t\tconst spec = new Evernote.NoteStore.NotesMetadataResultSpec();\n\t\tspec.includeTitle = false;\n\t\tfilter.words = '-tag:RoamImported';\n\t\tfilter.notebookGuid = this.defaultNotebook;\n\t\tconst batchCount = 100;\n\t\tconst loadMoreNotes = ( result ) => {\n\t\t\tif ( result.notes ) {\n\t\t\t\tthis.notesBeingImported = this.notesBeingImported.concat(\n\t\t\t\t\tresult.notes.map( ( note ) =>\n\t\t\t\t\t\tthis.NoteStore.getNote( note.guid, true, false, false, false )\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\t\t\tif ( result.startIndex < result.totalNotes ) {\n\t\t\t\treturn this.NoteStore.findNotesMetadata(\n\t\t\t\t\tfilter,\n\t\t\t\t\tresult.startIndex + result.notes.length,\n\t\t\t\t\tbatchCount,\n\t\t\t\t\tspec\n\t\t\t\t).then( loadMoreNotes );\n\t\t\t} else {\n\t\t\t\treturn Promise.resolve( this.mapping );\n\t\t\t}\n\t\t};\n\t\treturn this.NoteStore.findNotesMetadata( filter, 0, batchCount, spec )\n\t\t\t.then( loadMoreNotes )\n\t\t\t.then( () =>\n\t\t\t\tPromise.all( this.notesBeingImported ).then( ( notes ) => {\n\t\t\t\t\tthis.notesBeingImported = notes;\n\t\t\t\t\treturn Promise.resolve( notes );\n\t\t\t\t} )\n\t\t\t);\n\t}\n\tadjustTitle( title, force ) {\n\t\tif ( force || title === 'Bez tytułu' || title === 'Untitled Note' ) {\n\t\t\treturn moment( new Date() ).format( 'MMMM Do, YYYY' );\n\t\t} else {\n\t\t\treturn title;\n\t\t}\n\t}\n\tgetRoamPayload() {\n\t\treturn this.notesBeingImported.map( ( note ) => {\n\t\t\tconst md = ENML.PlainTextOfENML( note.content );\n\t\t\treturn {\n\t\t\t\ttitle: this.adjustTitle( note.title, true ),\n\t\t\t\tchildren: [ { string: 'Imported from Evernote: [' + note.title + '](' + this.getNoteUrl( note.guid ) + ')', children: [ { string: md } ] } ],\n\t\t\t};\n\t\t} );\n\t}\n\tcleanupImportNotes() {\n\t\treturn Promise.all(\n\t\t\tthis.notesBeingImported.map( ( note ) => {\n\t\t\t\tnote.tagGuids = [];\n\t\t\t\tnote.tagNames = [ 'RoamImported' ];\n\t\t\t\treturn this.NoteStore.updateNote( note );\n\t\t\t} )\n\t\t);\n\t}\n\n\tfindPreviousNote( url ) {\n\t\tconst filter = new Evernote.NoteStore.NoteFilter();\n\t\tconst spec = new Evernote.NoteStore.NotesMetadataResultSpec();\n\t\tspec.includeContentLength = true;\n\t\tspec.includeTitle = true;\n\t\tfilter.words = `sourceUrl:\"${url}\" contentClass:piszek.roam`;\n\t\treturn this.NoteStore.findNotesMetadata( filter, 0, 1, spec );\n\t}\n\n\tloadPreviousNotes() {\n\t\tlet duplicates = 0;\n\t\tconst filter = new Evernote.NoteStore.NoteFilter();\n\t\tconst spec = new Evernote.NoteStore.NotesMetadataResultSpec();\n\t\tspec.includeTitle = true;\n\t\tspec.includeContentLength = true;\n\t\tspec.includeDeleted = false;\n\t\tspec.includeAttributes = true;\n\t\tfilter.words = 'contentClass:piszek.roam';\n\t\tconst batchCount = 100;\n\n\t\tconst loadMoreNotes = ( result ) => {\n\t\t\tif ( result.notes ) {\n\t\t\t\tresult.notes.forEach( ( note ) => {\n\t\t\t\t\tif ( ! note.attributes.sourceURL ) {\n\t\t\t\t\t\tconsole.log( note.title , 'no src url' );\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst match = note.attributes.sourceURL.match( /https:\\/\\/roamresearch\\.com\\/\\#\\/app\\/[a-z]+\\/page\\/([a-zA-Z0-9_-]+)/);\n\t\t\t\t\tif ( ! match ) {\n\t\t\t\t\t\tconsole.log( note.title ,'no match', note.attributes.sourceURL );\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst uid = match[1];\n\t\t\t\t\t\t\n\t\t\t\t\tif ( this.mapping.get( uid ) && this.mapping.get( uid ).guid !== note.guid ) {\n\t\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t\t'[[' + this.mapping.get( uid ).title + ']]',\n\t\t\t\t\t\t\t'Note is a duplicate ',\n\t\t\t\t\t\t\tthis.getNoteUrl( this.mapping.get( uid ).guid ),\n\t\t\t\t\t\t\tthis.getNoteUrl( note.guid )\n\t\t\t\t\t\t);\n\t\t\t\t\t\t// this.NoteStore.deleteNote( note.guid );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.mapping.set( uid, {\n\t\t\t\t\t\t\tguid: note.guid,\n\t\t\t\t\t\t\ttitle: note.title,\n\t\t\t\t\t\t\tcontentLength: note.contentLength\n\t\t\t\t\t\t} );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\t\t\tif ( result.startIndex < result.totalNotes ) {\n\t\t\t\treturn this.NoteStore.findNotesMetadata(\n\t\t\t\t\tfilter,\n\t\t\t\t\tresult.startIndex + result.notes.length,\n\t\t\t\t\tbatchCount,\n\t\t\t\t\tspec\n\t\t\t\t).then( loadMoreNotes );\n\t\t\t} else {\n\t\t\t\t// console.log( batchCount );\n\t\t\t\treturn Promise.resolve( this.mapping );\n\t\t\t}\n\t\t};\n\t\treturn this.NoteStore.findNotesMetadata( filter, 0, batchCount, spec ).then( loadMoreNotes );\n\t}\n\n\taddBacklink( titles, target, text ) {\n\t\ttitles.forEach( ( title ) => {\n\t\t\tif ( ! this.backlinks[ title ] ) {\n\t\t\t\tthis.backlinks[ title ] = [];\n\t\t\t}\n\t\t\tthis.backlinks[ title ].push( {\n\t\t\t\ttarget: target,\n\t\t\t\ttext: text,\n\t\t\t} );\n\t\t} );\n\t}\n\tgetNoteUrl( guid ) {\n\t\treturn `evernote:///view/${ this.user.id }/${ this.user.shardId }/${ guid }/${ guid }/`;\n\t}\n\tinit( prevData = {} ) {\n\t\tthis.mapping = new Map( prevData );\n\t\tthis.EvernoteClient = new Evernote.Client( this.credentials );\n\t\tthis.NoteStore = this.EvernoteClient.getNoteStore();\n\t\tconst loadNotesPromise = this.loadPreviousNotes();\n\t\tloadNotesPromise.then( () => console.log( 'Loaded previous notes: ', this.mapping.size ) );\n\n\t\treturn Promise.all( [\n\t\t\tnew Promise( ( resolve, reject ) => {\n\t\t\t\tthis.EvernoteClient.getUserStore()\n\t\t\t\t\t.getUser()\n\t\t\t\t\t.then( ( user ) => {\n\t\t\t\t\t\tthis.user = user;\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t} );\n\t\t\t} ),\n\t\t\tthis.findNotebook().catch( ( err ) => console.log( 'Cannot find notebook Roam:', err ) ),\n\t\t\tloadNotesPromise,\n\t\t] );\n\t}\n\n\tsync( pages ) {\n\t\t// This can potentially introduce a race condition, but it's unlikely. Famous last words.\n\t\tvar p = Promise.resolve();\n\t\tpages.forEach( ( page ) => {\n\t\t\tp = p\n\t\t\t\t.then( () => this.syncPage( page ) )\n\t\t\t\t.catch( ( err ) => console.warn( 'Problem with syncing page ' + page.title, err, page.content ) );\n\t\t} );\n\t\treturn p.then( () => Promise.resolve( this.mapping ) );\n\t}\n\n\tsyncPage( page ) {\n\t\tlet url;\n\t\tif ( page.uid ) {\n\t\t\turl = `https://roamresearch.com/#/app/${this.graphName}/page/` + page.uid;\n\t\t} else {\n\t\t\tconsole.warn( \"Page must have UIDs and this one does not. Using title as a backup.\" );\n\t\t\turl = page.title;\n\t\t}\n\t\tlet newContent = page.content;\n\t\tif ( this.backlinks[ page.title ] ) {\n\t\t\tconst list = this.backlinks[ page.title ]\n\t\t\t\t.map( ( target ) => {\n\t\t\t\t\tlet reference = '[[' + target.target + ']]';\n\t\t\t\t\tconst targetPage = this.titleMapping.get( target.target );\n\t\t\t\t\tif (\n\t\t\t\t\t\ttargetPage &&\n\t\t\t\t\t\ttargetPage.uid &&\n\t\t\t\t\t\tthis.mapping.get( targetPage.uid )\n\t\t\t\t\t) {\n\t\t\t\t\t\treference =\n\t\t\t\t\t\t\t'<a href=\"' +\n\t\t\t\t\t\t\tthis.getNoteUrl( this.mapping.get( targetPage.uid ).guid ) +\n\t\t\t\t\t\t\t'\">' +\n\t\t\t\t\t\t\ttarget.target +\n\t\t\t\t\t\t\t'</a>';\n\t\t\t\t\t}\n\t\t\t\t\treturn '<li>' + reference + ': ' + target.text + '</li>';\n\t\t\t\t} )\n\t\t\t\t.join( '' );\n\n\t\t\tconst backlinks = '<h3>Linked References</h3><ul>' + list + '</ul>';\n\t\t\tnewContent = page.content + backlinks;\n\t\t}\n\n\t\treturn this.makeNote( page.title, newContent, url, page.uid );\n\t}\n}\n\nmodule.exports = EvernoteSyncAdapter;\n"
  },
  {
    "path": "README.md",
    "content": "## Roam Private API\n\nThis project exposes command line tool (`roam-api`) and a `node` library to connect Roam Research to your other software. You can use it in bash scripts, Github actions, or as a dependency of your project.\n## How does it work?\n\nIt looks like Roam is not providing a REST API any time soon. If you want to bridge Roam with your other software, you can do so from within Roam (with JavaScript), but that has limited number of use cases.\nWithout a REST API, this project launches an invisible browser and performs automated actions, just as you would manually. No need to install chrome, this library comes with one. It uses your login and password, so **this won't work if you are using Google login**.\nIt wraps around import/export functionality and actions exposed via `roamAlphaApi`.\n## Command line tool `roam-api`\n\nThis package exposes a `roam-api` tool in your system. You can use it to automate Roam and bridge other systems with your Roam graph.\n\n### Installation:\nThis entire library is build on node, so you need `node v12` and `npm v6` in your system. You can install the package the following way:\n```\nnpm i -g roam-research-private-api\n```\n\nNow you can use a variety of commands. All command take the following arguments, which you can also set as environmental variables:\n- `-g`, `--graph` or env variable `ROAM_API_GRAPH` - this is your graph name\n- `-e`, `--email` or env variable `ROAM_API_EMAIL` - email to log into your Roam\n- `-p`, `--password` or env variable `ROAM_API_PASSWORD` - password to your Roam.\n\n#### `roam-api export` will export your Roam graph to a directory of your choice. \n\nThis example will export the graph to your desktop. It will appear as \"db.json\".\n```\nroam-api export ~/Desktop\n```\n\nIt can also push the new version of the graph to an URL of your choosing. That way, you can upload the graph to some other system or use it with Zapier and similar tools.\n```\nroam-api export ~/Desktop http://secret.url?token=secret_token.\n```\n\n#### `roam-api search` will search your Roam graph for a phrase:\n\n```\nroam-api search \"potatoes\"\n```\n\nResult will be JSON array of objects `{ blockUid, pageTitle, string }`\n\n#### `roam-api-query` will let you do a full Datalog query.\n\nThis will find all block uids in your database which have the content \"Import\".\n```\nroam-api query '[:find ?uid :where [?b :block/string \"Import\"] [?b :block/uid ?uid]]'\n```\n\nCheck out [this fantastic article](https://www.zsolt.blog/2021/01/Roam-Data-Structure-Query.html) to know more about the Roam data structure.\n\n#### `roam-api create` create a block under specified uid. If no uid is provided, it will be inserted into your daily page:\n\n```\nroam-api create \"This will be prepended to my daily page\"\n```\n\n## Library to use in your project.\n\nAs mentioned, this is also a library that you can use within your project. Here are examples on how to do so:\n\n- [All the functionality in the roam-api tool](https://github.com/artpi/roam-research-private-api/blob/master/examples/cmd.js)\n- [Sync your Roam graph to Evernote](https://github.com/artpi/roam-research-private-api/blob/master/examples/sync_evernote.js) - you can also use command-line utility `roam-evernote-sync`.\n- [Import arbitrary data into any note](https://github.com/artpi/roam-research-private-api/blob/master/examples/import-data.js)\n- [Send note to Roam using the Quick Capture feature](https://github.com/artpi/roam-research-private-api/blob/master/examples/quick_capture.js)\n\n###\n\nPull requests welcome and I take no responsibility in case this messes up your Roam Graph :).\n"
  },
  {
    "path": "RoamPrivateApi.js",
    "content": "const puppeteer = require( 'puppeteer' );\nconst fs = require( 'fs' );\nconst path = require('path');\nconst os = require( 'os' );\nconst unzip = require( 'node-unzip-2' );\nconst { isString } = require( 'util' );\nconst moment = require( 'moment' );\n\n/**\n * This class represents wraps Puppeteer and exposes a few methods useful in manipulating Roam Research.\n */\nclass RoamPrivateApi {\n\toptions;\n\tbrowser;\n\tpage;\n\tdb;\n\tlogin;\n\tpass;\n\n\tconstructor( db, login, pass, options = { headless: true, folder: null, nodownload: false } ) {\n\t\t// If you dont pass folder option, we will use the system tmp directory.\n\t\tif ( ! options.folder ) {\n\t\t\toptions.folder = os.tmpdir();\n\t\t}\n\t\toptions.folder = fs.realpathSync( options.folder );\n\t\tthis.db = db;\n\t\tthis.login = login;\n\t\tthis.pass = pass;\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * Run a query on the new Roam Alpha API object.\n\t * More about the query syntax: https://www.zsolt.blog/2021/01/Roam-Data-Structure-Query.html\n\t * @param {string} query - datalog query.\n\t */\n\tasync runQuery( query ) {\n\t\treturn await this.page.evaluate( ( query ) => {\n\t\t\tif ( ! window.roamAlphaAPI ) {\n\t\t\t\treturn Promise.reject( 'No Roam API detected' );\n\t\t\t}\n\t\t\tconst result = window.roamAlphaAPI.q( query );\n\t\t\tconsole.log( result );\n\t\t\treturn Promise.resolve( result );\n\t\t}, query );\n\t}\n\n\t/**\n\t * Create a block as a child of block.\n\t * @param {string} text \n\t * @param {uid} uid - parent UID where block has to be inserted.\n\t */\n\tasync createBlock( text, uid ) {\n\t\tconst result = await this.page.evaluate( ( text, uid ) => {\n\t\t\tif ( ! window.roamAlphaAPI ) {\n\t\t\t\treturn Promise.reject( 'No Roam API detected' );\n\t\t\t}\n\t\t\tconst result = window.roamAlphaAPI.createBlock(\n\t\t\t\t{\"location\": \n\t\t\t\t\t{\"parent-uid\": uid, \n\t\t\t\t\t \"order\": 0}, \n\t\t\t\t \"block\": \n\t\t\t\t\t{\"string\": text}})\n\t\t\tconsole.log( result );\n\t\t\treturn Promise.resolve( result );\n\t\t}, text, uid );\n\t\t// Let's give time to sync.\n\t\tawait this.page.waitForTimeout( 1000 );\n\t\treturn result;\n\t}\n\n\t/**\n\t * Delete blocks matching the query. Hass some protections, but\n\t * THIS IS VERY UNSAFE. DO NOT USE THIS IF YOU ARE NOT 100% SURE WHAT YOU ARE DOING\n\t * @param {string} query - datalog query to find blocks to delete. Has to return block uid.\n\t * @param {int} limit - limit deleting to this many blocks. Default is 1.\n\t */\n\tasync deleteBlocksMatchingQuery( query, limit ) {\n\t\tif ( ! limit ) {\n\t\t\tlimit = 1;\n\t\t}\n\t\treturn await this.page.evaluate( ( query, limit ) => {\n\t\t\tif ( ! window.roamAlphaAPI ) {\n\t\t\t\treturn Promise.reject( 'No Roam API detected' );\n\t\t\t}\n\t\t\tconst result = window.roamAlphaAPI.q( query );\n\t\t\tconsole.log( result );\n\t\t\tif ( result.length > 100 ) {\n\t\t\t\treturn Promise.reject( 'Too many results. Is your query ok?' );\n\n\t\t\t}\n\t\t\tconst limited = result.slice( 0, limit );\n\t\t\tlimited.forEach( ( block ) => {\n\t\t\t\tconst id = block[0];\n\t\t\t\tconsole.log( 'DELETING', id );\n\t\t\t\twindow.roamAlphaAPI.deleteBlock( { block: { uid: id } } );\n\t\t\t} );\n\t\t\treturn Promise.resolve( limited );\n\t\t}, query, limit );\n\t}\n\n\t/**\n\t * Returns a query to find blocks with exact text on the page with title.\n\t * Useful with conjuction with deleteBlocksMatchingQuery,\n\t * @param {string} text - Exact text in the block.\n\t * @param {*} pageTitle - page title to find the blocks in.\n\t */\n\tgetQueryToFindBlocksOnPage( text, pageTitle ) {\n\t\ttext = text.replace( '\"', '\\\"' );\n\t\tpageTitle = pageTitle.replace( '\"', '\\\"' );\n\n\t\treturn `[:find ?uid\n\t\t\t:where [?b :block/string \"${text}\"]\n\t\t\t\t   [?b :block/uid  ?uid]\n\t\t\t\t   [?b :block/page ?p]\n\t\t\t\t   [?p :node/title \"${pageTitle}\"]]`;\n\t}\n\n\t/**\n\t * Returns datalog query to find all blocks containing the text.\n\t * Returns results in format [[ blockUid, text, pageTitle ]].\n\t * @param {string} text - text to search.\n\t */\n\tgetQueryToFindBlocks( text ) {\n\t\ttext = text.replace( '\"', '\\\"' );\n\t\treturn `[:find ?uid ?string ?title :where\n\t\t\t[?b :block/string ?string]\n\t\t\t[(clojure.string/includes? ?string \"${text}\")]\n\t\t\t[?b :block/uid  ?uid]\n\t\t\t[?b :block/page ?p]\n\t\t\t[?p :node/title ?title]\n\t\t]`;\n\t}\n\n\t/**\n\t * When importing in Roam, import leaves an \"Import\" block.\n\t * This removes that from your daily page.\n\t * THIS IS UNSAFE since it deletes blocks.\n\t */\n\tasync removeImportBlockFromDailyNote() {\n\t\tawait this.deleteBlocksMatchingQuery(\n\t\t\tthis.getQueryToFindBlocksOnPage(\n\t\t\t\t'Import',\n\t\t\t\tthis.dailyNoteTitle()\n\t\t\t),\n\t\t\t1\n\t\t);\n\t\t//Lets give time to sync\n\t\tawait this.page.waitForTimeout( 1000 );\n\t\treturn;\n\t}\n\n\t/**\n\t * Return page title for the current daily note.\n\t */\n\tdailyNoteTitle() {\n\t\treturn moment( new Date() ).format( 'MMMM Do, YYYY' );\n\t}\n\t/**\n\t * Return page uid for the current daily note.\n\t */\n\tdailyNoteUid() {\n\t\treturn moment( new Date() ).format( 'MM-DD-YYYY' );\n\t}\n\n\t/**\n\t * Export your Roam database and return the JSON data.\n\t * @param {boolean} autoremove - should the zip file be removed after extracting?\n\t */\n\tasync getExportData( autoremove ) {\n\t\t// Mostly for testing purposes when we want to use a preexisting download.\n\t\tif ( ! this.options.nodownload ) {\n\t\t\tawait this.logIn();\n\t\t\tawait this.downloadExport( this.options.folder );\n\t\t}\n\t\tconst latestExport = this.getLatestFile( this.options.folder );\n\t\tconst content = await this.getContentsOfRepo( this.options.folder, latestExport );\n\t\tif ( autoremove ) {\n\t\t\tfs.unlinkSync( latestExport );\n\t\t}\n\t\tawait this.close();\n\t\treturn content;\n\t}\n\t/**\n\t * Logs in to Roam interface.\n\t */\n\tasync logIn() {\n\t\tif ( this.browser ) {\n\t\t\treturn this.browser;\n\t\t}\n\t\tthis.browser = await puppeteer.launch( this.options );\n\t\ttry {\n\t\t\tthis.page = await this.browser.newPage();\n\t\t\tthis.page.setDefaultTimeout( 60000 );\n\t\t\tawait this.page.goto( 'https://roamresearch.com/#/app/' + this.db );\n\t\t\tawait this.page.waitForNavigation();\n\t\t\tawait this.page.waitForSelector( 'input[name=email]' );\n\t\t} catch ( e ) {\n\t\t\tconsole.error( 'Cannot load the login screen!' );\n\t\t\tthrow e;\n\t\t}\n\t\t// Login\n\t\tawait this.page.type( 'input[name=email]', this.login );\n\t\tawait this.page.type( 'input[name=password]', this.pass );\n\t\tawait this.page.click( '.bp3-button' );\n\t\tawait this.page.waitForSelector( '.bp3-icon-more' );\n\t\treturn;\n\t}\n\n\t/**\n\t * Import blocks to your Roam graph\n\t * @see examples/import.js.\n\t * @param {array} items \n\t */\n\tasync import( items = [] ) {\n\t\tconst fileName = path.resolve( this.options.folder, 'roam-research-private-api-sync.json' );\n\t\tfs.writeFileSync( fileName, JSON.stringify( items ) );\n\t\tawait this.logIn();\n\t\tawait this.page.waitForSelector( '.bp3-icon-more' );\n\t\tawait this.clickMenuItem( 'Import Files' );\n\t\t// await this.page.click( '.bp3-icon-more' );\n\t\t// // This should contain \"Export All\"\n\t\t// await this.page.waitFor( 2000 );\n\t\t// await this.page.click( '.bp3-menu :nth-child(5) a' );\n\t\tawait this.page.waitForSelector( 'input[type=file]' );\n\t\tawait this.page.waitForTimeout( 1000 );\n\t\t// get the ElementHandle of the selector above\n\t\tconst inputUploadHandle = await this.page.$( 'input[type=file]' );\n\n\t\t// Sets the value of the file input to fileToUpload\n\t\tinputUploadHandle.uploadFile( fileName );\n\t\tawait this.page.waitForSelector( '.bp3-dialog .bp3-intent-primary' );\n\t\tawait this.page.click( '.bp3-dialog .bp3-intent-primary' );\n\t\tawait this.page.waitForTimeout( 3000 );\n\t\tawait this.removeImportBlockFromDailyNote();\n\t\treturn;\n\t}\n\n\t/**\n\t * Inserts text to your quickcapture.\n\t * @param {string} text \n\t */\n\tasync quickCapture( text = [] ) {\n\t\tawait this.logIn();\n\t\tconst page = await this.browser.newPage();\n\t\tawait page.emulate( puppeteer.devices[ 'iPhone X' ] );\n\t\t// set user agent (override the default headless User Agent)\n\t\tawait page.goto( 'https://roamresearch.com/#/app/' + this.db );\n\n\t\tawait page.waitForSelector( '#block-input-quick-capture-window-qcapture' );\n\t\tif ( isString( text ) ) {\n\t\t\ttext = [ text ];\n\t\t}\n\n\t\ttext.forEach( async function ( t ) {\n\t\t\tawait page.type( '#block-input-quick-capture-window-qcapture', t );\n\t\t\tawait page.click( 'button.bp3-intent-primary' );\n\t\t} );\n\t\tawait page.waitForTimeout( 500 );\n\t\t// page.close();\n\t\tawait this.close();\n\t\treturn;\n\t}\n\n\t/**\n\t * Click item in the side-menu. This is mostly internal.\n\t * @param {string} title \n\t */\n\tasync clickMenuItem( title ) {\n\t\tawait this.page.click( '.bp3-icon-more' );\n\t\t// This should contain \"Export All\"\n\t\tawait this.page.waitForTimeout( 1000 );\n\t\tawait this.page.evaluate( ( title ) => {\n\t\t\tconst items = [ ...document.querySelectorAll( '.bp3-menu li a' ) ];\n\t\t\titems.forEach( ( item ) => {\n\t\t\t\tconsole.log( item.innerText, title );\n\t\t\t\tif ( item.innerText === title ) {\n\t\t\t\t\titem.click();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} );\n\t\t}, title );\n\t}\n\n\t/**\n\t * Download Roam export to a selected folder.\n\t * @param {string} folder \n\t */\n\tasync downloadExport( folder ) {\n\t\tawait this.page._client.send( 'Page.setDownloadBehavior', {\n\t\t\tbehavior: 'allow',\n\t\t\tdownloadPath: folder,\n\t\t} );\n\t\t// Try to download\n\t\t// await this.page.goto( 'https://roamresearch.com/#/app/' + this.db );\n\t\t// await this.page.waitForNavigation();\n\t\tawait this.page.waitForSelector( '.bp3-icon-more' );\n\t\tawait this.clickMenuItem( 'Export All' );\n\t\t// await this.page.click( '.bp3-icon-more' );\n\t\t// // This should contain \"Export All\"\n\t\t// await this.page.waitFor( 2000 );\n\t\t// await this.page.click( '.bp3-menu :nth-child(4) a' );\n\t\t//Change markdown to JSON:\n\t\t// This should contain markdown\n\t\tawait this.page.waitForTimeout( 2000 );\n\t\tawait this.page.click( '.bp3-dialog-container .bp3-popover-wrapper button' );\n\t\t// This should contain JSON\n\t\tawait this.page.waitForTimeout( 2000 );\n\t\tawait this.page.click( '.bp3-dialog-container .bp3-popover-wrapper .bp3-popover-dismiss' );\n\t\t// This should contain \"Export All\"\n\t\tawait this.page.waitForTimeout( 2000 );\n\t\tawait this.page.click( '.bp3-dialog-container .bp3-intent-primary' );\n\n\t\tawait this.page.waitForTimeout( 60000 ); // This can take quite some time on slower systems\n\t\t// Network idle is a hack to wait until we donwloaded stuff. I don't think it works though.\n\t\tawait this.page.goto( 'https://news.ycombinator.com/', { waitUntil: 'networkidle2' } );\n\t\treturn;\n\t}\n\n\t/**\n\t * Close the fake browser session.\n\t */\n\tasync close() {\n\t\tif ( this.browser ) {\n\t\t\tawait this.page.waitForTimeout( 1000 );\n\t\t\tawait this.browser.close();\n\t\t\tthis.browser = null;\n\t\t}\n\t\treturn;\n\t}\n\n\t/**\n\t * Get the freshest file in the directory, for finding the newest export.\n\t * @param {string} dir \n\t */\n\tgetLatestFile( dir ) {\n\t\tconst orderReccentFiles = ( dir ) =>\n\t\t\tfs\n\t\t\t\t.readdirSync( dir )\n\t\t\t\t.filter( ( f ) => fs.lstatSync( path.resolve( dir, f ) ) && fs.lstatSync( path.resolve( dir, f ) ).isFile() )\n\t\t\t\t.filter( ( f ) => f.indexOf( 'Roam-Export' ) !== -1 )\n\t\t\t\t.map( ( file ) => ( { file, mtime: fs.lstatSync( path.resolve( dir, file ) ).mtime } ) )\n\t\t\t\t.sort( ( a, b ) => b.mtime.getTime() - a.mtime.getTime() );\n\n\t\tconst getMostRecentFile = ( dir ) => {\n\t\t\tconst files = orderReccentFiles( dir );\n\t\t\treturn files.length ? files[ 0 ] : undefined;\n\t\t};\n\t\treturn path.resolve( dir, getMostRecentFile( dir ).file );\n\t}\n\n\t/**\n\t * Unzip the export and get the content.\n\t * @param {string} dir \n\t * @param {string} file \n\t */\n\tgetContentsOfRepo( dir, file ) {\n\t\treturn new Promise( ( resolve, reject ) => {\n\t\t\tconst stream = fs.createReadStream( file ).pipe( unzip.Parse() );\n\t\t\tstream.on( 'entry', function ( entry ) {\n\t\t\t\tvar fileName = entry.path;\n\t\t\t\tvar type = entry.type; // 'Directory' or 'File'\n\t\t\t\tvar size = entry.size;\n\t\t\t\tif ( fileName.indexOf( '.json' ) != -1 ) {\n\t\t\t\t\tentry.pipe( fs.createWriteStream( path.resolve( dir, 'db.json' ) ) );\n\t\t\t\t} else {\n\t\t\t\t\tentry.autodrain();\n\t\t\t\t}\n\t\t\t} );\n\t\t\t// Timeouts are here so that the system locks can be removed - takes time on some systems.\n\t\t\tstream.on( 'close', function () {\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\tfs.readFile( path.resolve( dir, 'db.json' ), 'utf8', function ( err, data ) {\n\t\t\t\t\t\tif ( err ) {\n\t\t\t\t\t\t\treject( err );\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresolve( JSON.parse( data ) );\t\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\t\t\t\t}, 1000 );\n\t\t\t} );\n\t\t} );\n\t}\n}\n\nmodule.exports = RoamPrivateApi;\n"
  },
  {
    "path": "Sync.js",
    "content": "class RoamSyncAdapter {\n\tcredentials;\n\tgraphName = '';\n\tpages = [];\n\ttitleMapping;\n\n\tconstructor( data, graphName ) {\n\t\tthis.titleMapping = new Map();\n\t\tthis.credentials = data;\n\t\tthis.graphName = graphName;\n\t}\n\n\tsync( pages ) {\n\t\treturn new Promise( ( resolve, reject ) => {\n\t\t\tconsole.log( pages );\n\t\t\tresolve( this.titleMapping );\n\t\t} );\n\t}\n\n\twrapItem( string, title ) {\n\t\tconst intend = ''; // this has to grow\n\t\treturn (\n\t\t\tintend +\n\t\t\t' - ' +\n\t\t\tstring +\n\t\t\t`\n\t\t`\n\t\t);\n\t}\n\n\twrapChildren( childrenString, title ) {\n\t\treturn childrenString.join( '' );\n\t}\n\n\twrapText( string, title ) {\n\t\treturn string;\n\t}\n\tflattenRoamDB( roamData, level, title ) {\n\t\tlet ret = '';\n\t\tif ( roamData.string ) {\n\t\t\tret += this.wrapText( roamData.string, title );\n\t\t}\n\t\tif ( roamData.children ) {\n\t\t\tret += this.wrapChildren(\n\t\t\t\troamData.children.map( ( child ) => this.flattenRoamDB( child, level + 1, title ) )\n\t\t\t);\n\t\t}\n\t\treturn this.wrapItem( ret, title );\n\t}\n\n\tprocessJSON( newData ) {\n\t\tthis.pages = newData.map( ( page ) => {\n\t\t\tconst newPage = {\n\t\t\t\tuid: page.uid,\n\t\t\t\ttitle: page.title,\n\t\t\t\tupdateTime: page[ 'edit-time' ],\n\t\t\t\tcontent: '',\n\t\t\t};\n\t\t\tif ( page.string ) {\n\t\t\t\tnewPage.content = page.string;\n\t\t\t}\n\t\t\tif ( page.children && page.children[ 0 ] ) {\n\t\t\t\tnewPage.content += this.flattenRoamDB( page, 0, page.title );\n\t\t\t}\n\t\t\tthis.titleMapping.set( page.title, newPage );\n\t\t\treturn newPage;\n\t\t} );\n\t\treturn this.sync( this.pages );\n\t}\n}\n\nmodule.exports = RoamSyncAdapter;\n"
  },
  {
    "path": "examples/cmd.js",
    "content": "#!/usr/bin/env node\nconst yargs = require( 'yargs' );\nconst fs = require( 'fs' );\nconst fetch = require( 'node-fetch' );\n\nconst argv = yargs\n\t.option( 'graph', {\n\t\talias: 'g',\n\t\tdescription: 'Your graph name',\n\t\ttype: 'string',\n\t} )\n\t.option( 'email', {\n\t\talias: 'e',\n\t\tdescription: 'Your Roam Email',\n\t\ttype: 'string',\n\t} )\n\t.option( 'password', {\n\t\talias: 'p',\n\t\tdescription: 'Your Roam Password',\n\t\ttype: 'string',\n\t} )\n\t.option( 'debug', {\n\t\tdescription: 'enable debug mode',\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t} )\n\t.option( 'stdin', {\n\t\talias: 'i',\n\t\tdescription: 'Read from STDIN',\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t} )\n\t.option( 'removezip', {\n\t\tdescription: 'If downloading the Roam Graph, should the timestamp zip file be removed after downloading?',\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t} )\n\t.command(\n\t\t'query [query]',\n\t\t'Query your Roam Graph using datalog syntax',\n\t\t() => {},\n\t\t( argv ) => {\n\t\t\tlet input = '';\n\t\t\tif ( argv.stdin ) {\n\t\t\t\tinput = fs.readFileSync( 0, 'utf-8' );\n\t\t\t} else {\n\t\t\t\tinput = argv['query'];\n\t\t\t}\n\n\t\t\tif ( ! input || input.length < 3 ) {\n\t\t\t\tconsole.warn( 'You have to provide a query at least 3 chars long' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.log( \"Logging in to your Roam and running query:\" );\n\t\t\tconsole.log( input );\n\t\t\tconst RoamPrivateApi = require( '../' );\n\t\t\tconst api = new RoamPrivateApi( argv.graph, argv.email, argv.password, {\n\t\t\t\theadless: ! argv.debug,\n\t\t\t} );\n\n            api.logIn()\n            .then( () => api.runQuery( input ) )\n            .then( result => {\n                console.log( JSON.stringify( result, null, 4 ) );\n                api.close();\n            } );\n\t\t}\n\t)\n\t.command(\n\t\t'search <query>',\n\t\t'Query your Roam Graph blocks using simple text search.',\n\t\t() => {},\n\t\t( argv ) => {\n\t\t\tconst RoamPrivateApi = require( '../' );\n\t\t\tconst api = new RoamPrivateApi( argv.graph, argv.email, argv.password, {\n\t\t\t\theadless: ! argv.debug,\n\t\t\t} );\n\n            api.logIn()\n            .then( () => api.runQuery( api.getQueryToFindBlocks( argv['query'] ) ) )\n            .then( result => {\n\t\t\t\tresult = result.map( result => ( {\n\t\t\t\t\tblockUid: result[0],\n\t\t\t\t\tpageTitle: result[2],\n\t\t\t\t\tstring: result[1]\n\t\t\t\t} ) );\n                console.log( JSON.stringify( result, null, 4 ) );\n                api.close();\n            } );\n\t\t}\n\t)\n\t.command(\n\t\t'create [text] [parentuid]',\n\t\t'Append a block to a block with a selected uid. If no uid is provided, block will be appended to the daily page. You can also pass data from stdin.',\n\t\t() => {},\n\t\t( argv ) => {\n\t\t\tlet input = '';\n\t\t\tif ( argv.stdin ) {\n\t\t\t\tinput = fs.readFileSync( 0, 'utf-8' );\n\t\t\t} else {\n\t\t\t\tinput = argv['text'];\n\t\t\t}\n\n\t\t\tif ( ! input || input.length < 3 ) {\n\t\t\t\tconsole.warn( 'You have to provide content at least 3 chars long' );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst RoamPrivateApi = require( '../' );\n\t\t\tconst api = new RoamPrivateApi( argv.graph, argv.email, argv.password, {\n\t\t\t\theadless: ! argv.debug,\n\t\t\t} );\n\n\t\t\tif ( ! argv['parentuid'] ) {\n\t\t\t\targv['parentuid'] = api.dailyNoteUid();\n\t\t\t}\n\n\t\t\tapi.logIn()\n            .then( () => api.createBlock( input, argv['parentuid'] ) )\n            .then( result => api.close() );\n\t\t}\n\t)\n\t.command(\n\t\t'export <dir> [exporturl]',\n\t\t'Export your Roam database to a selected directory. If URL is provided, then the concent will be sent by POST request to the specified URL.',\n\t\t() => {},\n\t\t( argv ) => {\n\t\t\tconst RoamPrivateApi = require( '../' );\n\t\t\tconst api = new RoamPrivateApi( argv.graph, argv.email, argv.password, {\n\t\t\t\theadless: ! argv.debug,\n\t\t\t\tfolder: argv['dir']\n\t\t\t} );\n\t\t\tlet promises = api.getExportData( argv['removezip'] );\n\t\t\tpromises.then( data => console.log( 'Downloaded' ) );\n\t\t\tif ( argv['exporturl'] ) {\n\t\t\t\tpromises.then( data => fetch( argv['exporturl'], {\n\t\t\t\t\tmethod: 'post',\n\t\t\t\t\tbody: JSON.stringify( {\n\t\t\t\t\t\tgraphContent: data,\n\t\t\t\t\t\tgraphName: api.db\n\t\t\t\t\t} ),\n\t\t\t\t\theaders: {'Content-Type': 'application/json'}\n\t\t\t\t} ) )\n\t\t\t\t.catch( err => console.log( err ) )\n\t\t\t\t.then( () => console.log( \"Uploaded to export url.\" ) )\n\t\t\t}\n\t\t}\n\t)\n\t.help()\n\t.alias( 'help', 'h' )\n\t.env( 'ROAM_API' )\n\t.demandOption(\n\t\t[ 'graph', 'email', 'password' ],\n\t\t'You need to provide graph name, email and password'\n\t).argv;\n"
  },
  {
    "path": "examples/download.js",
    "content": "const RoamPrivateApi = require( '../' );\nconst secrets = require( '../secrets.json' );\n\nconst api = new RoamPrivateApi( secrets.graph, secrets.email, secrets.password, {\n\theadless: false,\n\tfolder: './tmp/',\n\tnodownload: false,\n} );\napi.getExportData().then( data => console.log( 'success', data ) );\n"
  },
  {
    "path": "examples/import-data.js",
    "content": "const RoamPrivateApi = require( '../' );\nconst secrets = require( '../secrets.json' );\nvar fs = require( 'fs' );\n\nconst api = new RoamPrivateApi( secrets.graph, secrets.email, secrets.password, {\n\theadless: false,\n\tfolder: './tmp/',\n} );\napi.import( [\n\t{ title: 'test', children: [ { string: 'Test child' }, { string: 'Another test child' } ] },\n] );\n"
  },
  {
    "path": "examples/quick_capture.js",
    "content": "#!/usr/bin/env node\nconst yargs = require( 'yargs' );\nconst fs = require( 'fs' );\n\nconst argv = yargs\n\t.option( 'graph', {\n\t\talias: 'g',\n\t\tdescription: 'Your graph name',\n\t\ttype: 'string',\n\t} )\n\t.option( 'email', {\n\t\talias: 'e',\n\t\tdescription: 'Your Roam Email',\n\t\ttype: 'string',\n\t} )\n\t.option( 'password', {\n\t\talias: 'p',\n\t\tdescription: 'Your Roam Password',\n\t\ttype: 'string',\n\t} )\n\t.option( 'debug', {\n\t\tdescription: 'enable debug mode',\n\t\ttype: 'boolean',\n\t} )\n\t.option( 'stdin', {\n\t\talias: 'i',\n\t\tdescription: 'Read from STDIN',\n\t\ttype: 'boolean',\n\t} )\n\t.command(\n\t\t'$0',\n\t\t'Save Quick capture',\n\t\t() => {},\n\t\t( argv ) => {\n\t\t\tlet input = '';\n\t\t\tif ( argv.stdin ) {\n\t\t\t\tinput = fs.readFileSync( 0, 'utf-8' );\n\t\t\t} else {\n\t\t\t\tinput = argv[ '_' ].join( ' ' );\n\t\t\t}\n\n\t\t\tif ( ! input || input.length < 3 ) {\n\t\t\t\tconsole.warn( 'You have to provide a note at least 3 chars long' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst RoamPrivateApi = require( '../' );\n\t\t\tconst api = new RoamPrivateApi( argv.graph, argv.email, argv.password, {\n\t\t\t\theadless: ! argv.debug,\n\t\t\t} );\n\n\t\t\tapi.quickCapture( [ input ] );\n\t\t}\n\t)\n\t.help()\n\t.alias( 'help', 'h' )\n\t.demandOption(\n\t\t[ 'graph', 'email', 'password' ],\n\t\t'You need to provide graph name, email and password'\n\t).argv;\n"
  },
  {
    "path": "examples/sync_evernote.js",
    "content": "#!/usr/bin/env node\nconst helpText=`\nThe command exposed here (roam-evernote-sync sync) will sync your Roam Graph to your Evernote database.\nDepending on a few configuration options, it will:\n1. Download payload from external URL to import INTO roam (useful for connecting with other services)\n2. Take all notes in your default Evernote notebook and import them into your daily note. They will be marked with 'RoamImported' tag to prevent doing so multiple times\n3. Export all notes from your Roam and import them to \"Roam\" notebook in your Evernote account\n4. The backlinks will be kept intact, notes will be updated when possible\n5. After all that is done, DB will be pushed to external URL ('exporturl') if provided to provide connection for https://deliber.at/roam/wp-roam-block or similar projects\n\n- 'dir' is a directory where database will be downloaded.\n- 'mappingcachefile' is a JSON file that provides a cache for Roam UID <-> Evernote GUID mapping. This is used to relieve Evernote API a bit\n`;\n\nconst yargs = require( 'yargs' );\nconst fetch = require( 'node-fetch' );\nvar fs = require( 'fs' ).promises;\n\nconst argv = yargs\n\t.option( 'graph', {\n\t\talias: 'g',\n\t\tdescription: 'Your graph name',\n\t\ttype: 'string',\n\t} )\n\t.option( 'email', {\n\t\talias: 'e',\n\t\tdescription: 'Your Roam Email',\n\t\ttype: 'string',\n\t} )\n\t.option( 'password', {\n\t\talias: 'p',\n\t\tdescription: 'Your Roam Password',\n\t\ttype: 'string',\n\t} )\n\t.option( 'evernote_token', {\n\t\talias: 't',\n\t\tdescription: 'Your Evernote Token',\n\t\ttype: 'string',\n\t} )\n\t.option( 'debug', {\n\t\tdescription: 'enable debug mode',\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t} )\n\t.option( 'nodownload', {\n\t\tdescription: 'Skip the download of the roam graph. Default no - do download.',\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t} )\n\t.option( 'nosandbox', {\n\t\tdescription: 'Skip the Chrome Sandbox.',\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t} )\n\t.option( 'executable', {\n\t\tdescription: 'Executable path to Chromium.',\n\t\ttype: 'string',\n\t\tdefault: '',\n\t} )\n\t.option( 'verbose', {\n\t\talias: 'v',\n\t\tdescription: 'You know, verbose.',\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t} )\n\t.option( 'privateapiurl', {\n\t\tdescription: 'Additional endpoint that provides data to sync INTO Roam. Has nothing to do with Evernote, its just convenient.',\n\t\ttype: 'string',\n\t\tdefault: '',\n\t} )\n\t.option( 'removezip', {\n\t\tdescription: 'If downloading the Roam Graph, should the timestamp zip file be removed after downloading?',\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t} )\n\t.command(\n\t\t'sync <dir> <mappingcachefile> [exporturl]',\n\t\thelpText,\n\t\t() => {},\n\t\t( argv ) => {\n\n\t\t\tconst RoamPrivateApi = require( '../' );\n\t\t\tconst EvernoteSyncAdapter = require( '../EvernoteSync' );\n\t\t\tconst options = {\n\t\t\t\theadless: ! argv.debug,\n\t\t\t\tnodownload: argv.nodownload,\n\t\t\t\tfolder: argv['dir']\n\t\t\t};\n\t\t\tif ( argv[ 'executable' ] ) {\n\t\t\t\toptions['executablePath'] = argv[ 'executable' ];\n\t\t\t}\n\t\t\tif ( argv[ 'nosandbox' ] ) {\n\t\t\t\toptions['args'] = ['--no-sandbox', '--disable-setuid-sandbox'];\n\t\t\t}\n\n\t\t\tconst e = new EvernoteSyncAdapter( { token: argv.evernoteToken, sandbox: false }, argv.graph );\n\t\t\tconst api = new RoamPrivateApi( argv.graph, argv.email, argv.password, options );\n\n\t\t\t// This downloads the private additional content for my Roam graph, served by other means.\n\t\t\tconst importIntoRoam = [];\n\t\t\tif ( argv.privateapiurl ) {\n\t\t\t\tconst private_api = fetch( argv.privateapiurl ).then( response => response.json() );\n\t\t\t\tprivate_api.then( data => console.log( 'Private API payload', JSON.stringify( data, null, 2 ) ) );\n\t\t\t\timportIntoRoam.push( private_api );\n\t\t\t}\n\n\t\t\tlet evernote_to_roam;\n\t\t\tif ( argv.mappingcachefile ) {\n\t\t\t\t// There is a mapping file.\n\t\t\t\tevernote_to_roam = fs.readFile( argv.mappingcachefile )\n\t\t\t\t.then( ( data ) => e.init( JSON.parse( data ) ) )\n\t\t\t\t.catch( ( err ) => e.init( null ) )\n\t\t\t} else {\n\t\t\t\tevernote_to_roam = e.init( null );\n\t\t\t}\n\n\t\t\t// This finds notes IN Evernote to import into Roam:\n\t\t\tevernote_to_roam = evernote_to_roam\n\t\t\t\t.then( () => e.getNotesToImport() )\n\t\t\t\t.then( payload => Promise.resolve( e.getRoamPayload( payload ) ) );\n\t\t\t\timportIntoRoam.push( evernote_to_roam );\n\n\t\t\t// Let's start the flow with Roam:\n\t\t\tconst roamdata = Promise.all( importIntoRoam )\n\t\t\t\t.then( results => {\n\t\t\t\t\tconst payload = results[0].concat( results[1] );\n\t\t\t\t\tconsole.log( 'Importing into Roam', JSON.stringify( payload, null, 2 ) );\n\t\t\t\t\tif( payload.length > 0 ) {\n\t\t\t\t\t\treturn api.import( payload );\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn Promise.resolve();\n\t\t\t\t\t}\n\t\t\t\t} )\n\t\t\t\t.then( () => e.cleanupImportNotes() )\n\t\t\t\t.then( () => api.getExportData( ! argv.nodownload && argv['removezip'] ) ); // Removing zip is only possible if we downloaded it.\n\n\t\t\t\t// We are saving the intermediate step of mapping just in case.\n\t\t\t\tif ( argv.mappingcachefile ) {\n\t\t\t\t\troamdata.then( data => fs.writeFile( argv.mappingcachefile, JSON.stringify( [ ...e.mapping ], null, 2 ), 'utf8' ) );\n\t\t\t\t}\n\n\t\t\t\t// This will push Roam graph to the URL of your choice - can be WordPress\n\t\t\t\tif ( argv.exporturl ) {\n\t\t\t\t\troamdata.then( data => fetch( argv.exporturl, {\n\t\t\t\t\t\tmethod: 'post',\n\t\t\t\t\t\tbody: JSON.stringify( {\n\t\t\t\t\t\t\tgraphContent: data,\n\t\t\t\t\t\t\tgraphName: api.db\n\t\t\t\t\t\t} ),\n\t\t\t\t\t\theaders: {'Content-Type': 'application/json'}\n\t\t\t\t\t} ) )\n\t\t\t\t\t.then( response => response.text() )\n\t\t\t\t\t.then( ( data ) => console.log( 'Updated in your remote URL', data ) );\n\t\t\t\t}\n\n\t\t\t\t// This is the actual moment where we sync to Evernote:\n\t\t\t\tlet finish = roamdata.then( ( data ) => e.processJSON( data ) );\n\t\t\t\t// We are saving the final step of mapping just in case.\n\t\t\t\tif ( argv.mappingcachefile ) {\n\t\t\t\t\tfinish = finish.then( data => fs.writeFile( argv.mappingcachefile, JSON.stringify( [ ...e.mapping ], null, 2 ), 'utf8' ) );\n\t\t\t\t}\n\t\t\t\tfinish.then( () => console.log( 'success' ) );\n\t\t}\n\t)\n\t.help()\n\t.alias( 'help', 'h' )\n\t.env( 'ROAM_API' )\n\t.demandOption(\n\t\t[ 'graph', 'email', 'password' ],\n\t\t'You need to provide graph name, email and password'\n\t).argv;\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"roam-research-private-api\",\n\t\"version\": \"0.9.3\",\n\t\"description\": \"Library that loads your Roam Research graph as a browser and performs tasks as you.\",\n\t\"homepage\": \"https://deliber.at/roam/roam-api/\",\n\t\"keywords\": [\n\t\t\"roam\",\n\t\t\"roam-research\",\n\t\t\"evernote\",\n\t\t\"puppeteer\"\n\t],\n\t\"main\": \"RoamPrivateApi.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"https://github.com/artpi/roam-research-private-api\"\n\t},\n\t\"scripts\": {\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n\t\t\"evernote_sync\": \"node examples/sync_evernote.js\",\n\t\t\"reformat-files\": \"./node_modules/.bin/prettier --ignore-path .eslintignore --write \\\"**/*.{js,jsx,json,ts,tsx}\\\"\"\n\t},\n\t\"bin\": {\n\t\t\"roam-api\": \"examples/cmd.js\",\n\t\t\"roam-evernote-sync\": \"examples/sync_evernote.js\"\n\t},\n\t\"author\": \"Artur Piszek ( piszek.com )\",\n\t\"license\": \"MIT\",\n\t\"dependencies\": {\n\t\t\"enml-js\": \"^0.1.3\",\n\t\t\"evernote\": \"^2.0.5\",\n\t\t\"moment\": \"^2.27.0\",\n\t\t\"node-fetch\": \"^2.6.1\",\n\t\t\"node-unzip-2\": \"^0.2.8\",\n\t\t\"puppeteer\": \"^5.5.0\",\n\t\t\"yargs\": \"^15.4.1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"prettier\": \"npm:wp-prettier@2.0.5\"\n\t},\n\t\"engines\": {\n\t\t\"node\": \">=12.0.0\",\n\t\t\"npm\": \">=6.0.0\"\n\t}\n}\n"
  },
  {
    "path": "secrets-template.json",
    "content": "{\n\t\"email\": \"your-roam-account@email.com\",\n\t\"password\": \"roam-account-password\",\n\t\"graph\": \"your-roam-graph-name\",\n\t\"evernote_token\": \"Evernote developer token from https://www.evernote.com/api/DeveloperToken.action\"\n}\n"
  }
]