49278723b1
- Use Node filesystem APIs instead of shell commands for temp cleanup - Add absolute path helpers for cross-platform compatibility - Generate both main.js and dist/main.js artifacts - Improve error handling in document downloader - Track additional document metadata in sync tracker - Replace Unix-specific `find` with recursive directory traversal The build system now generates two output files (main.js and dist/main.js) for better compatibility with Obsidian's plugin loading. Path handling has been centralized in new utilities to ensure cross-platform behavior, replacing shell commands that had Windows compatibility issues.
38 lines
19 KiB
JavaScript
38 lines
19 KiB
JavaScript
var I=Object.defineProperty;var st=Object.getOwnPropertyDescriptor;var ot=Object.getOwnPropertyNames;var lt=Object.prototype.hasOwnProperty;var ct=(r,t)=>{for(var e in t)I(r,e,{get:t[e],enumerable:!0})},dt=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ot(t))!lt.call(r,a)&&a!==e&&I(r,a,{get:()=>t[a],enumerable:!(n=st(t,a))||n.enumerable});return r};var mt=r=>dt(I({},"__esModule",{value:!0}),r);var ht={};ct(ht,{default:()=>pt});module.exports=mt(ht);var h=require("obsidian");var g=require("obsidian"),V={remarkableHost:"https://10.11.99.1",rmapiBinaryPath:"rmapi",downloadPath:"remarkable",convertToPdf:!0,enableHandwritingMd:!0,glmocrServerUrl:"http://localhost:5002",glmocrApiKey:"",ollamaHost:"http://localhost:11435",styleModel:"qwen3:32b",pageRenderer:"drawj2d",javaPath:"java",drawj2dPath:"drawj2d.jar",maxPagesPerBatch:20,syncInterval:0},R=class extends g.PluginSettingTab{plugin;constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"reMarkable Sync Settings"}),new g.Setting(t).setName("reMarkable Host").setDesc("URL of your reMarkable tablet (e.g., https://10.11.99.1)").addText(e=>e.setValue(this.plugin.settings.remarkableHost).onChange(async n=>{this.plugin.settings.remarkableHost=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("rmapi Binary Path").setDesc("Path to rmapi binary (e.g., rmapi or /usr/local/bin/rmapi)").addText(e=>e.setValue(this.plugin.settings.rmapiBinaryPath).onChange(async n=>{this.plugin.settings.rmapiBinaryPath=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Download Path").setDesc("Folder in vault for downloaded documents").addText(e=>e.setValue(this.plugin.settings.downloadPath).onChange(async n=>{this.plugin.settings.downloadPath=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Convert to PDF").setDesc("Automatically convert documents to annotated PDF").addToggle(e=>e.setValue(this.plugin.settings.convertToPdf).onChange(async n=>{this.plugin.settings.convertToPdf=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Enable Handwriting to Markdown").setDesc("Convert handwritten notes to styled Markdown").addToggle(e=>e.setValue(this.plugin.settings.enableHandwritingMd).onChange(async n=>{this.plugin.settings.enableHandwritingMd=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("GLM-OCR Server URL").setDesc("URL of GLM-OCR SDK Server (e.g., http://localhost:5002)").addText(e=>e.setValue(this.plugin.settings.glmocrServerUrl).onChange(async n=>{this.plugin.settings.glmocrServerUrl=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("GLM-OCR API Key").setDesc("API key for GLM-OCR Server (can be any string for self-hosted)").addText(e=>{e.inputEl.type="password",e.setValue(this.plugin.settings.glmocrApiKey).onChange(async n=>{this.plugin.settings.glmocrApiKey=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("Ollama Host").setDesc("URL of Ollama server for style refinement (e.g., http://localhost:11435)").addText(e=>e.setValue(this.plugin.settings.ollamaHost).onChange(async n=>{this.plugin.settings.ollamaHost=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Style Model").setDesc("Ollama model for Markdown style refinement").addText(e=>e.setValue(this.plugin.settings.styleModel).onChange(async n=>{this.plugin.settings.styleModel=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Page Renderer").setDesc("Tool to render .rm pages as PNG").addDropdown(e=>e.addOption("drawj2d","drawj2d (recommended)").addOption("rM2svg","rM2svg (lightweight)").setValue(this.plugin.settings.pageRenderer).onChange(async n=>{this.plugin.settings.pageRenderer=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Java Path").setDesc("Path to Java runtime (for drawj2d)").addText(e=>{e.setValue(this.plugin.settings.javaPath).onChange(async n=>{this.plugin.settings.javaPath=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("drawj2d Path").setDesc("Path to drawj2d.jar (e.g., /opt/drawj2d.jar)").addText(e=>{e.setValue(this.plugin.settings.drawj2dPath).onChange(async n=>{this.plugin.settings.drawj2dPath=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("Max Pages per Batch").setDesc("Max pages per GLM-OCR request (to avoid timeouts)").addText(e=>{e.setValue(String(this.plugin.settings.maxPagesPerBatch)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.maxPagesPerBatch=isNaN(a)||a<1?20:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="1"}),new g.Setting(t).setName("Sync Interval (minutes)").setDesc("0 = manual only, >0 = auto-sync every N minutes").addText(e=>{e.setValue(String(this.plugin.settings.syncInterval)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.syncInterval=isNaN(a)||a<0?0:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="0"})}};var k=require("obsidian");var B=require("obsidian"),G=require("path");function v(r){return r.app.vault.adapter.getBasePath()}function w(...r){let t=r.join("/").split("/").filter(e=>e&&e!=="."&&e!=="..").join("/");return(0,B.normalizePath)(t)}function b(r,t){return(0,G.join)(v(r),...(0,B.normalizePath)(t).split("/"))}function U(r){return r.replace(/[\\/:*?"<>|]/g,"_")}function gt(r){return r.split("/").filter(Boolean).map(U)}var D=class{plugin;constructor(t){this.plugin=t}async syncAll(){try{new k.Notice("Syncing from reMarkable...");let t=await this.listAllDocuments("/");for(let e of t)await this.downloadDocument(e);new k.Notice(`Sync completed! ${t.length} document(s) synced.`)}catch(t){throw new k.Notice(`Sync failed: ${t}`),console.error("Sync error:",t),t}}async listAllDocuments(t){let e=await this.plugin.rmapi.list(t),n=e.filter(a=>a.type==="DocumentType").map(a=>({node:a,remotePath:t==="/"?`/${a.name}`:`${t}/${a.name}`}));for(let a of e.filter(i=>i.type==="CollectionType")){let i=t==="/"?`/${a.name}`:`${t}/${a.name}`,s=await this.listAllDocuments(i);n=n.concat(s)}return n}async downloadDocument(t){let{node:e,remotePath:n}=t,a=await this.plugin.tracker.getSyncedDocument(e.id);if(a&&a.remoteVersion===e.version&&a.remoteModified===e.modifiedClient)return;let i=gt(n),s=i.pop()||U(e.name),o=w(this.plugin.settings.downloadPath,...i),c=w(o,`${s}.rm`),u=b(this.plugin,c);await this.ensureVaultFolder(o),await this.plugin.rmapi.downloadFile(n,u);let l=!1;if(this.plugin.settings.convertToPdf){let p=w(o,`${s}.pdf`);await this.plugin.rmapi.downloadAnnotatedPdf(n,b(this.plugin,p)),l=!0}if(await this.plugin.tracker.trackDocument(e,c,l,!1),this.plugin.settings.enableHandwritingMd){let p=w(o,`${s}.md`);await this.plugin.ocrPipeline.processDocument(c,p)&&await this.plugin.tracker.trackDocument(e,c,l,!0)}}async ensureVaultFolder(t){let e=t.split("/").filter(Boolean),n="";for(let a of e){n=n?w(n,a):a;try{await this.plugin.app.vault.adapter.mkdir(n)}catch(i){if(!await this.plugin.app.vault.adapter.exists(n))throw i}}}};var z=require("child_process");async function d(r,t=[],e={},n){return new Promise(a=>{let i=(0,z.spawn)(r,t,{env:{...process.env,...e},cwd:n}),s="",o="",c=!1,u=(l,p)=>{c||(c=!0,a({stdout:s,stderr:p?`${o}
|
|
${p.message}`:o,code:l}))};i.stdout.on("data",l=>{s+=l.toString()}),i.stderr.on("data",l=>{o+=l.toString()}),i.on("error",l=>{u(1,l)}),i.on("close",l=>{u(l??0)})})}var W=require("path");var x=class{plugin;constructor(t){this.plugin=t}getEnv(){return{RMAPI_HOST:this.plugin.settings.remarkableHost,RMAPI_CONFIG:(0,W.join)(v(this.plugin),".obsidian","rmapi")}}async runRmapi(t){return d(this.plugin.settings.rmapiBinaryPath,t,this.getEnv())}async list(t="/"){let{stdout:e,stderr:n,code:a}=await this.runRmapi(["ls","--json",t]);if(a!==0)throw new Error(`rmapi ls failed: ${n||e}`);try{return JSON.parse(e)}catch(i){throw new Error(`Failed to parse rmapi output: ${i}`)}}async downloadFile(t,e){let{stdout:n,stderr:a,code:i}=await this.runRmapi(["get",t,"-o",e]);if(i!==0)throw new Error(`rmapi get failed: ${a||n}`)}async downloadAnnotatedPdf(t,e){let{stdout:n,stderr:a,code:i}=await this.runRmapi(["geta",t,"-o",e]);if(i!==0)throw new Error(`rmapi geta failed: ${a||n}`)}async isAuthenticated(){let{code:t,stderr:e}=await this.runRmapi(["ls","--json","/"]);return t===0&&!e.includes("auth")&&!e.includes("register")}};var C=class{plugin;constructor(t){this.plugin=t}async renderPageToPng(t,e){this.plugin.settings.pageRenderer==="drawj2d"?await this.renderWithDrawj2d(t,e):await this.renderWithRm2svg(t,e)}async renderWithDrawj2d(t,e){let n=["-jar",this.plugin.settings.drawj2dPath,"-Trm",t,e],{code:a,stderr:i,stdout:s}=await d(this.plugin.settings.javaPath,n);if(a!==0)throw new Error(`drawj2d failed: ${i||s}`)}async renderWithRm2svg(t,e){let n=e.replace(".png",".svg"),{code:a,stderr:i,stdout:s}=await d("rM2svg",[t,n]);if(a!==0)throw new Error(`rM2svg failed: ${i||s}`);let{code:o}=await d("rsvg-convert",["-o",e,n]);if(o!==0){let{code:c,stderr:u,stdout:l}=await d("convert",[n,e]);if(c!==0)throw new Error(`SVG to PNG conversion failed: ${u||l}`)}}};var H=require("fs/promises");async function J(r){let t=`${r}/content.json`;try{let n=await(0,H.readFile)(t,"utf-8");try{let a=JSON.parse(n),i=ut(a);if(i)return i}catch{}}catch{}let e=`${r}/metadata.json`;try{let n=await(0,H.readFile)(e,"utf-8");try{let a=JSON.parse(n);if(a?.text)return a.text}catch{}}catch{}return""}function ut(r){let t=[];if(r.cPages&&Array.isArray(r.cPages.pages)){for(let e of r.cPages.pages)if(e.text&&t.push(e.text),e.layers)for(let n of e.layers)n.text&&t.push(n.text)}if(r.pages&&Array.isArray(r.pages))for(let e of r.pages)e.text&&t.push(e.text);return t.join(`
|
|
|
|
`)}var f=require("fs/promises"),K=require("path"),q=require("os");var O=class{plugin;constructor(t){this.plugin=t}async parseImages(t){let e=`${this.plugin.settings.glmocrServerUrl}/glmocr/parse`,n=this.plugin.settings.glmocrApiKey,a=[];for(let s of t){let o=await(0,f.readFile)(s);a.push(`data:image/png;base64,${o.toString("base64")}`)}let i=(0,K.join)((0,q.tmpdir)(),`glmocr-body-${Date.now()}.json`);await(0,f.writeFile)(i,JSON.stringify({images:a}));try{let s=["-s","-X","POST","-H","Content-Type: application/json","-H",`Authorization: Bearer ${n}`,"--max-time","60","--data-binary",`@${i}`,e],{stdout:o,stderr:c,code:u}=await d("curl",s);if(u!==0)throw new Error(`GLM-OCR request failed: ${c||o}`);try{return JSON.parse(o)}catch(l){throw new Error(`Failed to parse GLM-OCR response: ${l}
|
|
Raw: ${o}`)}}finally{await(0,f.unlink)(i).catch(()=>{})}}};var T=require("fs/promises"),_=require("path"),Y=require("os");var M=class{plugin;constructor(t){this.plugin=t}async refineMarkdown(t){let e=`You are formatting the output of an OCR engine that processed a handwritten document. The input is markdown that may have issues with heading levels, list formatting, and paragraph breaks.
|
|
|
|
Analyze the document's structure and:
|
|
1. Fix heading hierarchy (h1 \u2192 h2 \u2192 h3 logically)
|
|
2. Consolidate fragmented paragraphs
|
|
3. Detect and format lists, checkboxes, tables
|
|
4. Identify emphasized/underlined text and apply bold/italic
|
|
5. Preserve all content \u2014 do not summarize or remove anything
|
|
|
|
INPUT MARKDOWN:
|
|
${t}
|
|
|
|
OUTPUT ONLY THE REFINED MARKDOWN. No explanations, no markdown code fences around the output.`,n=(await this.queryOllama(e)).trim();return this.validateOllamaOutput(t,n),n}async mergeAndRefine(t,e){if(!t.trim())return this.refineMarkdown(e);let n=`You have two sources of text from the same handwritten document:
|
|
|
|
SOURCE 1 \u2014 ReMarkable built-in handwriting recognition (raw text, no formatting):
|
|
${t}
|
|
|
|
SOURCE 2 \u2014 GLM-OCR engine output (markdown with structure but possible errors):
|
|
${e}
|
|
|
|
Your task:
|
|
1. Merge both sources, preferring SOURCE 2 for structure and SOURCE 1 for text accuracy
|
|
2. Fix heading hierarchy (h1 \u2192 h2 \u2192 h3 logically)
|
|
3. Consolidate fragmented paragraphs
|
|
4. Detect and format lists, checkboxes, tables
|
|
5. Identify emphasized/underlined text and apply bold/italic
|
|
6. Preserve all content \u2014 do not summarize or remove anything
|
|
|
|
OUTPUT ONLY THE FINAL REFINED MARKDOWN. No explanations, no markdown code fences around the output.`,a=(await this.queryOllama(n)).trim();return this.validateOllamaOutput(`${t}
|
|
${e}`,a),a}async queryOllama(t){let e=`${this.plugin.settings.ollamaHost}/api/generate`,n=this.plugin.settings.styleModel,a=(0,_.join)((0,Y.tmpdir)(),`ollama-body-${Date.now()}.json`);await(0,T.writeFile)(a,JSON.stringify({model:n,prompt:t,stream:!1}));try{let i=["-s","-X","POST","-H","Content-Type: application/json","--max-time","300","--data-binary",`@${a}`,e],{stdout:s,stderr:o,code:c}=await d("curl",i);if(c!==0)throw new Error(`Ollama request failed: ${o||s}`);try{return JSON.parse(s).response||""}catch(u){throw new Error(`Failed to parse Ollama response: ${u}
|
|
Raw: ${s}`)}}finally{await(0,T.unlink)(a).catch(()=>{})}}validateOllamaOutput(t,e){let n=t.trim().length,a=e.trim().length;if(a===0)throw new Error("Ollama returned empty Markdown");if(n>500&&a<n*.5)throw new Error(`Ollama output is unexpectedly short (${a} chars vs ${n} input chars)`)}};var X=require("fs/promises"),Q=require("path");async function Z(r,t){let{code:e,stderr:n}=await d("unzip",["-o",r,"-d",t]);if(e!==0)throw new Error(`Failed to extract .rm file: ${n}`);return t}async function tt(r){let t=[];async function e(n){let a=await(0,X.readdir)(n,{withFileTypes:!0});for(let i of a){let s=(0,Q.join)(n,i.name);i.isDirectory()?await e(s):i.isFile()&&t.push(s)}}return await e(r),t}async function et(r){let t=await tt(r),e=t.some(a=>a.endsWith(".rm")&&!a.endsWith(".zip")),n=t.some(a=>a.endsWith("content.pdf")||a.endsWith(".pdf"));return e&&!n}async function nt(r){return(await tt(r)).filter(e=>e.endsWith(".rm")&&!e.endsWith(".zip")).sort()}var y=require("obsidian"),j=require("fs/promises"),$=require("path");var N=class{plugin;renderer;glmOcr;refiner;constructor(t){this.plugin=t,this.renderer=new C(t),this.glmOcr=new O(t),this.refiner=new M(t)}async processDocument(t,e){let n=(0,$.join)(v(this.plugin),".obsidian","rmapi-tmp"),a=b(this.plugin,t);try{new y.Notice("Converting handwriting to Markdown..."),await this.ensureDir(n);let i=(0,$.join)(n,"extracted");if(await this.ensureDir(i),await Z(a,i),!await et(i))return new y.Notice("Document is not a handwritten notebook \u2014 skipping OCR."),"";let o=await J(i),c=await nt(i),u=[],l=c.length;for(let m=0;m<c.length;m++){let A=c[m],F=(0,$.join)(n,`page-${m}.png`);await this.renderer.renderPageToPng(A,F),u.push(F),((m+1)%5===0||m===l-1)&&new y.Notice(`Rendering pages... ${m+1}/${l}`)}let p=this.plugin.settings.maxPagesPerBatch,P=[];for(let m=0;m<u.length;m+=p)P.push(u.slice(m,m+p));let L="";for(let m=0;m<P.length;m++){let A=P[m],rt=(await this.glmOcr.parseImages(A)).markdown_result||"";L+=rt+`
|
|
|
|
`,new y.Notice(`OCR processing... ${m+1}/${P.length}`)}let at=L.trim(),it=await this.refiner.mergeAndRefine(o,at);return await this.plugin.app.vault.adapter.write(e,it),new y.Notice("Handwriting conversion complete!"),e}finally{await this.cleanup(n)}}async ensureDir(t){await(0,j.mkdir)(t,{recursive:!0})}async validateDependencies(){let t=["unzip","curl"],e=[];for(let n of t)try{let{code:a}=await d(n,["--version"]);a!==0&&e.push(n)}catch{e.push(n)}if(this.plugin.settings.pageRenderer==="drawj2d")try{let{code:n}=await d(this.plugin.settings.javaPath,["-jar",this.plugin.settings.drawj2dPath,"--help"]);n!==0&&e.push("drawj2d")}catch{e.push("drawj2d")}else{try{let{code:n}=await d("rM2svg",["--help"]);n!==0&&e.push("rM2svg")}catch{e.push("rM2svg")}try{let{code:n}=await d("rsvg-convert",["--version"]);if(n!==0)throw new Error("rsvg-convert failed")}catch{try{let{code:n}=await d("convert",["--version"]);if(n!==0)throw new Error("convert failed")}catch{e.push("rsvg-convert or convert")}}}return e}async cleanup(t){try{await(0,j.rm)(t,{recursive:!0,force:!0})}catch{console.warn("Failed to cleanup temp directory:",t)}}};var E=class{plugin;cache=null;constructor(t){this.plugin=t}async getCache(){if(this.cache===null){let t=await this.plugin.loadData();this.cache=t?.syncedDocuments||[]}return this.cache}async loadSyncedDocuments(){return this.getCache()}async saveSyncedDocuments(t){this.cache=t;let e=await this.plugin.loadData()||{};await this.plugin.saveData({...e,syncedDocuments:t})}async trackDocument(t,e,n,a){let i=await this.getCache(),s=i.findIndex(c=>c.id===t.id),o={id:t.id,name:t.name,lastSynced:new Date().toISOString(),remoteModified:t.modifiedClient,remoteVersion:t.version,localPath:e,hasPdf:n,hasMd:a};s>=0?i[s]=o:i.push(o),await this.saveSyncedDocuments(i)}async isDocumentSynced(t){return(await this.getCache()).some(n=>n.id===t)}async getSyncedDocument(t){return(await this.getCache()).find(n=>n.id===t)||null}};var S=class extends h.Plugin{settings;downloader;rmapi;ocrPipeline;tracker;statusBarItem;isSyncing=!1;async onload(){if(await this.loadSettings(),this.rmapi=new x(this),this.tracker=new E(this),this.downloader=new D(this),this.ocrPipeline=new N(this),this.settings.enableHandwritingMd){let t=await this.ocrPipeline.validateDependencies();t.length>0&&(new h.Notice(`OCR dependencies missing: ${t.join(", ")}. Disable "Enable Handwriting to Markdown" in settings or install required tools.`,15e3),this.settings.enableHandwritingMd=!1,await this.saveSettings())}this.addRibbonIcon("pencil","Sync from reMarkable",()=>{this.performSync()}),this.addCommand({id:"sync-all",name:"Sync from reMarkable",callback:()=>this.performSync()}),this.addCommand({id:"convert-handwriting-md",name:"Convert handwriting to Markdown",callback:()=>this.convertActiveFileToMd()}),this.addSettingTab(new R(this.app,this)),this.statusBarItem=this.addStatusBarItem(),this.updateStatusBar("Idle"),this.settings.syncInterval>0&&this.registerInterval(window.setInterval(()=>{this.performSync()},this.settings.syncInterval*60*1e3))}onunload(){}async performSync(){if(this.isSyncing){new h.Notice("Sync already in progress.");return}this.isSyncing=!0;try{if(!await this.rmapi.isAuthenticated()){new h.Notice("reMarkable not authenticated. Run 'rmapi' in a terminal to pair your device.",1e4);return}this.updateStatusBar("Syncing..."),await this.downloader.syncAll(),this.updateStatusBar(`Last sync: ${new Date().toLocaleTimeString()}`)}catch(t){this.updateStatusBar("Sync failed"),console.error("Sync error:",t)}finally{this.isSyncing=!1}}async convertActiveFileToMd(){let t=this.app.workspace.getActiveFile();if(!t){new h.Notice("No active file selected");return}let e=t.path;if(!e.endsWith(".rm")){new h.Notice("Active file is not a .rm reMarkable document");return}let n=e.replace(/\.rm$/,".md");try{this.updateStatusBar("Converting to Markdown..."),await this.ocrPipeline.processDocument(e,n),this.updateStatusBar(`Converted: ${t.name}`)}catch(a){this.updateStatusBar("Conversion failed"),new h.Notice(`Conversion failed: ${a}`),console.error("OCR error:",a)}}updateStatusBar(t){this.statusBarItem.setText(`reMarkable: ${t}`)}async loadSettings(){this.settings=Object.assign({},V,await this.loadData())}async saveSettings(){let t=await this.loadData()||{};await this.saveData({...t,...this.settings})}};var pt=S;
|