From b7d1a255434664400fa65565bcfd5ce64aeec118 Mon Sep 17 00:00:00 2001 From: wujunchen Date: Sun, 26 Apr 2026 09:48:19 +0800 Subject: [PATCH] security: strip path traversal segments from vault paths Reject '..' and '.' segments in normalizeVaultPath to prevent exportFolder from escaping the vault root directory. Change-Id: Ib4da19f9d0aa1236a3b77b6f2cc722fcff408131 --- main.js | 2 +- src/vault.ts | 2 +- tests/modules.test.js | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/main.js b/main.js index 41e587a..db3b966 100644 --- a/main.js +++ b/main.js @@ -96,7 +96,7 @@ ${o}`:`\u4EE5\u4E0B\u662F\u9700\u8981\u5904\u7406\u7684\u6587\u6863\u5168\u6587\ ${o}`;return{system:u,user:C}}function Tt(t){return t.choices?.[0]?.delta?.content||""}function Et(t){return t.type==="content_block_delta"&&t.delta?.text||""}function U(t){switch(t){case"openai-chat":return Tt;case"anthropic-messages":return Et;default:return null}}function Ie(t,n){let e=[],r=t.split(` `),a=r.pop();for(let i of r){let s=i.trim();if(!s||s==="data: [DONE]"||s.startsWith("event:")||!s.startsWith("data:"))continue;let o=s.slice(s.startsWith("data: ")?6:5);try{let l=JSON.parse(o),d=n(l);d&&e.push(d)}catch{}}return{deltas:e,rest:a}}async function Le(t,n,e,r,a,i,s){if(typeof globalThis.fetch!="function")throw new Error("Streaming requires fetch API");let o=await globalThis.fetch(t,{method:"POST",headers:n,body:JSON.stringify(e),signal:i});if(!o.ok){let h=await o.text();throw new Error(b(s||null,"errorProviderApiStatus",{label:"Streaming",status:o.status,excerpt:h.slice(0,500)}))}if(!o.body)throw new Error("Response has no body for streaming");let l=o.body.getReader(),d=new TextDecoder,c="",p="";try{for(;;){let{done:h,value:f}=await l.read();if(h)break;p+=d.decode(f,{stream:!0});let u=Ie(p,r);p=u.rest;for(let C of u.deltas)c+=C;u.deltas.length>0&&a?.({accumulated:c,done:!1})}}finally{l.releaseLock()}return a?.({accumulated:c,done:!0}),c}function X(t,n){let e=t.replace(/\/+$/,"");for(let r of n)if(e.endsWith(r))return e;return e+n[0]}function kt(t,n){let e=(t||"").trim();if(!e)return{};if(e.startsWith("{")){let a;try{a=JSON.parse(e)}catch(s){throw new Error(b(n||null,"errorCustomHeadersJsonParse",{error:s.message}))}if(!a||typeof a!="object"||Array.isArray(a))throw new Error(b(n||null,"errorCustomHeadersJsonObject"));let i={};for(let[s,o]of Object.entries(a))typeof o=="string"&&s.trim()&&(i[s.trim()]=o);return i}let r={};for(let a of e.split(/\r?\n/)){let i=a.trim();if(!i||i.startsWith("#"))continue;let s=i.indexOf(":");if(s<=0)throw new Error(b(n||null,"errorCustomHeadersLineFormat"));let o=i.slice(0,s).trim(),l=i.slice(s+1).trim();o&&(r[o]=l)}return r}function Ft(t){let n=Fe(t);if(n==="none")return{};let e=nt(t);if(!e){let r=(t.apiKeyEnvVar||w(t).envVar||"").trim(),a=r?b(t,"errorApiKeyEnvHint",{envVar:r}):"";throw new Error(b(t,"errorApiKeyMissing",{hint:a}))}return n==="bearer"?{authorization:`Bearer ${e}`}:n==="x-api-key"?{"x-api-key":e}:n==="x-goog-api-key"?{"x-goog-api-key":e}:n==="api-key"?{"api-key":e}:{authorization:`Bearer ${e}`}}function G(t,n){return{"content-type":"application/json",...Ft(t),...n||{},...kt(t.apiHeaders,t)}}function Nt(t,n,e){if(t.json&&typeof t.json=="object")return t.json;try{return JSON.parse(t.text||"{}")}catch{throw new Error(b(e||null,"errorProviderNonJson",{label:n,excerpt:(t.text||"").slice(0,500)}))}}async function it(t,n,e,r,a,i){let s;try{s=await t({url:e,method:"POST",headers:r,body:JSON.stringify(a),throw:!1})}catch(o){throw new Error(b(i||null,"errorProviderRequestFailed",{label:n,error:o.message||o}))}if(s.status>=400)throw new Error(b(i||null,"errorProviderApiStatus",{label:n,status:s.status,excerpt:(s.text||"").slice(0,500)}));return Nt(s,n,i)}function Dt(t){let n=String(t?.message?t.message:t);return/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(n)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(n):!1}async function he(t,n,e,r,a,i,s){try{return await it(t,n,e,r,a,s)}catch(o){if(!i||!Dt(o))throw o;return console.warn(`[parallel-reader] ${n} structured output rejected; retrying without structured output`,o),it(t,n+" fallback",e,r,i,s)}}function Oe(t){return typeof t=="string"?t:Array.isArray(t)?t.map(n=>typeof n=="string"?n:n&&typeof n=="object"&&(n.text||n.output_text)||"").join(""):t&&typeof t=="object"&&(t.text||t.output_text)||""}function Rt(t){if(typeof t.output_text=="string")return t.output_text;let n=[],e=r=>{if(r&&typeof r!="string"){if(Array.isArray(r)){r.forEach(e);return}typeof r=="object"&&(typeof r.text=="string"&&n.push(r.text),typeof r.output_text=="string"&&n.push(r.output_text),r.type==="output_text"&&typeof r.content=="string"&&n.push(r.content),r.content&&e(r.content),r.output&&e(r.output))}};return e(t.output),n.join("")}function _e(t){let n=w(t),e=S[A(t)];return n.tokenLimitField||e?.tokenLimitField||"max_tokens"}function W(t,n,e,r){let a=!r||r.structured!==!1,i={model:L(e),max_tokens:Number(e.apiMaxTokens)||4096,system:t,messages:[{role:"user",content:n}]};return a&&(i.tools=[qe()],i.tool_choice={type:"tool",name:ie}),i}function Y(t,n,e,r){let a=!r||r.structured!==!1,i={model:L(e),messages:[{role:"system",content:t},{role:"user",content:n}]};return i[_e(e)]=Number(e.apiMaxTokens)||4096,a&&(i.response_format=$e()),i}function de(t,n,e,r){let a=!r||r.structured!==!1,i={model:L(e),instructions:t,input:n,max_output_tokens:Number(e.apiMaxTokens)||4096};return a&&(i.text=ze()),i}function pe(t,n,e,r){let a=!r||r.structured!==!1,i={temperature:0,maxOutputTokens:Number(e.apiMaxTokens)||4096};return a&&(i.responseMimeType="application/json",i.responseJsonSchema=J(!1)),{systemInstruction:{parts:[{text:t}]},contents:[{role:"user",parts:[{text:n}]}],generationConfig:i}}function Mt(t,n){let r=(Array.isArray(t?.content)?t.content:[]).find(a=>a&&a.type==="tool_use"&&a.name===ie);return r?typeof r.input=="string"?P(r.input,n):r.input&&typeof r.input=="object"?j(r.input):[]:null}async function It(t,n,e,r){let a=X(k(r),["/messages"]),i=await he(t,"Anthropic-compatible",a,G(r,{"anthropic-version":"2023-06-01"}),W(n,e,r),W(n,e,r,{structured:!1}),r),s=Mt(i,r);if(s)return s;let o=(i.content||[]).map(l=>Oe(l)).join("").trim();return P(o,r)}async function Lt(t,n,e,r){let a=X(k(r),["/chat/completions"]),s=((await he(t,"OpenAI-compatible Chat",a,G(r),Y(n,e,r),Y(n,e,r,{structured:!1}),r)).choices||[])[0]||{},o=Oe(s.message?.content||s.text||"").trim();return P(o,r)}async function Ot(t,n,e,r){let a=X(k(r),["/responses"]),i=await he(t,"OpenAI Responses",a,G(r),de(n,e,r),de(n,e,r,{structured:!1}),r);return P(Rt(i).trim(),r)}async function _t(t,n,e,r){let a=encodeURIComponent(L(r)),i=k(r);/:generateContent(?:\?|$)/.test(i)||(i=`${i.replace(/\/+$/,"")}/models/${a}:generateContent`);let s=G(r),c=((((await he(t,"Google Gemini",i,s,pe(n,e,r),pe(n,e,r,{structured:!1}),r)).candidates||[])[0]||{}).content?.parts||[]).map(p=>Oe(p)).join("").trim();return P(c,r)}async function me(t,n,e,r){switch(A(r)){case"openai-chat":return Lt(t,n,e,r);case"openai-responses":return Ot(t,n,e,r);case"google-generative-ai":return _t(t,n,e,r);default:return It(t,n,e,r)}}function Ve(t){if(!t.streaming)return!1;let n=A(t);return!!U(n)}async function Vt(t,n,e,r,a){let i=X(k(e),["/chat/completions"]),s=G(e),o=Y(t,n,e,{structured:!1});o.stream=!0;let l=U("openai-chat"),d=await Le(i,s,o,l,r,a,e);return P(d.trim(),e)}async function Bt(t,n,e,r,a){let i=X(k(e),["/messages"]),s=G(e,{"anthropic-version":"2023-06-01"}),o=W(t,n,e,{structured:!1});o.stream=!0;let l=U("anthropic-messages"),d=await Le(i,s,o,l,r,a,e);return P(d.trim(),e)}async function st(t,n,e,r,a){let i=A(e);switch(i){case"openai-chat":return Vt(t,n,e,r,a);case"anthropic-messages":return Bt(t,n,e,r,a);default:throw new Error(`Streaming not supported for format: ${i}`)}}async function ot(t,n){await me(t,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',n);let e=A(n);return`${w(n).label} / ${S[e].label}`}function Be(t,n=80,e=.1){let r=Number(t?.top)||0,a=Math.max(0,Number(t?.height)||0),i=Math.min(Number(n)||0,a*e);return r+Math.max(0,i)}function Ut(t){return typeof requestAnimationFrame=="function"?requestAnimationFrame(t):setTimeout(()=>t(Date.now()),16)}function Gt(t){if(typeof cancelAnimationFrame=="function"){cancelAnimationFrame(t);return}clearTimeout(t)}function Ue(t,n=Ut,e=Gt){let r=null,a=()=>{r===null&&(r=n(()=>{r=null,t()}))};return a.cancel=()=>{r!==null&&(e(r),r=null)},a}var g=require("obsidian");async function Ht(t){if(t.backend==="codex"){let n=M("codex",t.cliPath),{stdout:e}=await K(n,["--version"],"",1e4);return`codex @ ${n} ${e.trim()}`}if(t.backend==="claude-code"){let n=M("claude",t.cliPath),{stdout:e}=await K(n,["--version"],"",1e4);return`claude @ ${n} -${e.trim()}`}if(I(t.backend))return ot(g.requestUrl,t);throw new Error("Unknown backend: "+t.backend)}var ge=class extends g.PluginSettingTab{constructor(n,e){super(n,e),this.plugin=e}display(){let{containerEl:n}=this,e=(i,s)=>this.plugin.t(i,s);n.empty(),n.createEl("h2",{text:e("settingsTitle")}),new g.Setting(n).setName(e("settingUiLanguageName")).setDesc(e("settingUiLanguageDesc")).addDropdown(i=>{for(let[s,o]of Object.entries(Ee))i.addOption(s,o);return i.setValue(this.plugin.settings.uiLanguage||y.uiLanguage).onChange(async s=>{this.plugin.settings.uiLanguage=s,await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingBackendName")).setDesc(e("settingBackendDesc")).addDropdown(i=>i.addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").addOption("api","API / Provider").addOption("anthropic-api","Anthropic API (legacy)").setValue(this.plugin.settings.backend).onChange(async s=>{this.plugin.settings.backend=s,s==="api"&&!this.plugin.settings.apiBaseUrl&&Ne(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let r=I(this.plugin.settings.backend);if(!r)new g.Setting(n).setName(e("settingCliPathName")).setDesc(e("settingCliPathDesc")).addText(i=>i.setPlaceholder(e("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async s=>{this.plugin.settings.cliPath=s.trim(),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingCliTimeoutName")).addText(i=>i.setValue(String(this.plugin.settings.cliTimeoutMs)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.cliTimeoutMs=o,this.plugin.saveSettingsDebounced())}));else{n.createEl("h3",{text:e("apiProviderHeader")});let i=w(this.plugin.settings);new g.Setting(n).setName(e("settingProviderPresetName")).setDesc(e("settingProviderPresetDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(F))s.addOption(o,l.label);return s.setValue(this.plugin.settings.apiProvider).onChange(async o=>{Ne(this.plugin.settings,o),await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingApiFormatName")).setDesc(e("settingApiFormatDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(S))s.addOption(o,l.label);return s.setValue(A(this.plugin.settings)).onChange(async o=>{this.plugin.settings.apiFormat=o,await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingBaseUrlName")).setDesc(e("settingBaseUrlDesc")).addText(s=>s.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":i.baseUrl||S[A(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async o=>{this.plugin.settings.apiBaseUrl=o.trim(),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingApiKeyName")).setDesc(e("settingApiKeyDesc")).addText(s=>(s.inputEl.type="password",s.setPlaceholder("sk-...").setValue(this.plugin.settings.apiKey).onChange(async o=>{this.plugin.settings.apiKey=o.trim(),this.plugin.saveSettingsDebounced()}))),new g.Setting(n).setName(e("settingApiKeyEnvName")).setDesc(e("settingApiKeyEnvDesc")).addText(s=>s.setPlaceholder(i.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async o=>{this.plugin.settings.apiKeyEnvVar=o.trim(),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingAuthTypeName")).setDesc(e("settingAuthTypeDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(ke))s.addOption(o,l);return s.setValue(this.plugin.settings.apiAuthType||"auto").onChange(async o=>{this.plugin.settings.apiAuthType=o,await this.plugin.saveSettings()})}),new g.Setting(n).setName(e("settingHeadersName")).setDesc(e("settingHeadersDesc")).addTextArea(s=>s.setPlaceholder("cf-aig-authorization: Bearer ...").setValue(this.plugin.settings.apiHeaders).onChange(async o=>{this.plugin.settings.apiHeaders=o,this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingMaxTokensName")).addText(s=>s.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async o=>{let l=parseInt(o,10);!Number.isNaN(l)&&l>0&&(this.plugin.settings.apiMaxTokens=l,this.plugin.saveSettingsDebounced())})),new g.Setting(n).setName(e("settingStreamingName")).setDesc(e("settingStreamingDesc")).addToggle(s=>s.setValue(this.plugin.settings.streaming??!0).onChange(async o=>{this.plugin.settings.streaming=o,await this.plugin.saveSettings()}))}new g.Setting(n).setName(e("settingModelName")).setDesc(e(r?"settingModelDescApi":"settingModelDescCli")).addText(i=>i.setPlaceholder(r?w(this.plugin.settings).model||"model-id":y.model).setValue(this.plugin.settings.model).onChange(async s=>{this.plugin.settings.model=s.trim()||(r?"":y.model),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingMaxInputName")).setDesc(e("settingMaxInputDesc")).addText(i=>i.setValue(String(this.plugin.settings.maxDocChars||y.maxDocChars)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>=1e3&&(this.plugin.settings.maxDocChars=o,this.plugin.saveSettingsDebounced())})),n.createEl("h3",{text:e("promptHeader")}),new g.Setting(n).setName(e("settingPromptLanguageName")).setDesc(e("settingPromptLanguageDesc")).addDropdown(i=>{for(let[s,o]of Object.entries(q))i.addOption(s,o);return i.setValue(this.plugin.settings.promptLanguage||y.promptLanguage).onChange(async s=>{this.plugin.settings.promptLanguage=s,await this.plugin.saveSettings()})}),new g.Setting(n).setName(e("settingCardRangeName")).setDesc(e("settingCardRangeDesc")).addText(i=>i.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||y.minCards)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.minCards=o,this.plugin.settings.maxCardsi.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||y.maxCards)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.maxCards=Math.max(o,this.plugin.settings.minCards||y.minCards),this.plugin.saveSettingsDebounced())})),new g.Setting(n).setName(e("settingCustomPromptName")).setDesc(e("settingCustomPromptDesc")).addTextArea(i=>(i.inputEl.rows=8,i.setPlaceholder(e("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async s=>{this.plugin.settings.customSystemPrompt=s,this.plugin.saveSettingsDebounced()}))),new g.Setting(n).setName(e("settingTestBackendName")).setDesc(e(r?"settingTestBackendDescApi":"settingTestBackendDescCli")).addButton(i=>i.setButtonText("Test").onClick(async()=>{try{let s=await Ht(this.plugin.settings);new g.Notice(`\u2713 ${s.slice(0,180)}`,8e3)}catch(s){new g.Notice(e("backendTestFailed",{error:s.message}),1e4)}})),new g.Setting(n).setName(e("settingExportFolderName")).setDesc(e("settingExportFolderDesc")).addText(i=>i.setValue(this.plugin.settings.exportFolder).onChange(async s=>{this.plugin.settings.exportFolder=s.trim()||y.exportFolder,this.plugin.saveSettingsDebounced()})),n.createEl("h3",{text:e("cacheHeader")}),new g.Setting(n).setName(e("settingMaxCacheName")).setDesc(e("settingMaxCacheDesc")).addText(i=>{i.setValue(String(this.plugin.settings.maxCacheEntries||B));let s=async()=>{let o=parseInt(i.getValue(),10);if(Number.isFinite(o)&&o>0){this.plugin.settings.maxCacheEntries=o,await this.plugin.saveSettings();let l=await this.plugin.pruneCacheIfNeeded();l.length>0&&new g.Notice(e("cachePruned",{count:l.length})),this.display()}};return i.inputEl.addEventListener("change",s),i.inputEl.addEventListener("keydown",o=>{o.key==="Enter"&&i.inputEl.blur()}),i});let a=Object.keys(this.plugin.cache).length;new g.Setting(n).setName(e("cachedNotesName",{count:a})).setDesc(e("cachedNotesDesc")).addButton(i=>i.setButtonText(e("clearAllCacheButton")).setWarning().onClick(async()=>{let s=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new g.Notice(e("cacheClearedAll",{count:s})),this.display()}))}};var E=require("obsidian");function N(t,n,e,r){let a=t.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":e}});return a.title=e,typeof E.setIcon=="function"?(0,E.setIcon)(a,n):a.textContent=e,a.addEventListener("click",async i=>{i.preventDefault(),i.stopPropagation();try{await r()}catch(s){console.error(s),new E.Notice(`${e} failed: `+(s.message||s))}}),a}function D(t,n,e,r,a){let i=t.createEl("button",{cls:a||"parallel-reader-text-button",attr:{type:"button"}});return n&&typeof E.setIcon=="function"&&(0,E.setIcon)(i,n),i.createSpan({text:e}),i.addEventListener("click",async s=>{s.preventDefault(),s.stopPropagation();try{await r()}catch(o){console.error(o),new E.Notice(`${e} failed: `+(o.message||o))}}),i}async function O(t,n){try{await navigator.clipboard.writeText(t),new E.Notice(n)}catch(e){new E.Notice("Copy failed: "+(e.message||e))}}function Ge(t){return String(t||"").split("/").map(n=>n.trim()).filter(Boolean).join("/")}function He(t){let n=Ge(t);if(!n)return[];let e=n.split("/");return e.map((r,a)=>e.slice(0,a+1).join("/"))}async function lt(t,n){for(let e of He(n))if(!t.vault.getAbstractFileByPath(e))try{await t.vault.createFolder(e)}catch(r){if(!t.vault.getAbstractFileByPath(e))throw r}}var v=require("obsidian");var ct=require("obsidian");var fe=class extends ct.Modal{constructor(n,e,r,a){super(n),this.plugin=e,this.card=r||{},this.onSave=a}onOpen(){let{contentEl:n}=this;n.empty(),n.createEl("h2",{text:this.plugin.t("editCardTitle")});let e=this.createLabeledInput(n,this.plugin.t("editCardTitleField"),this.card.title||""),r=this.createLabeledTextarea(n,this.plugin.t("editCardGistField"),this.card.gist||"",3),a=this.createLabeledTextarea(n,this.plugin.t("editCardBulletsField"),(this.card.bullets||[]).join(` +${e.trim()}`}if(I(t.backend))return ot(g.requestUrl,t);throw new Error("Unknown backend: "+t.backend)}var ge=class extends g.PluginSettingTab{constructor(n,e){super(n,e),this.plugin=e}display(){let{containerEl:n}=this,e=(i,s)=>this.plugin.t(i,s);n.empty(),n.createEl("h2",{text:e("settingsTitle")}),new g.Setting(n).setName(e("settingUiLanguageName")).setDesc(e("settingUiLanguageDesc")).addDropdown(i=>{for(let[s,o]of Object.entries(Ee))i.addOption(s,o);return i.setValue(this.plugin.settings.uiLanguage||y.uiLanguage).onChange(async s=>{this.plugin.settings.uiLanguage=s,await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingBackendName")).setDesc(e("settingBackendDesc")).addDropdown(i=>i.addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").addOption("api","API / Provider").addOption("anthropic-api","Anthropic API (legacy)").setValue(this.plugin.settings.backend).onChange(async s=>{this.plugin.settings.backend=s,s==="api"&&!this.plugin.settings.apiBaseUrl&&Ne(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let r=I(this.plugin.settings.backend);if(!r)new g.Setting(n).setName(e("settingCliPathName")).setDesc(e("settingCliPathDesc")).addText(i=>i.setPlaceholder(e("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async s=>{this.plugin.settings.cliPath=s.trim(),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingCliTimeoutName")).addText(i=>i.setValue(String(this.plugin.settings.cliTimeoutMs)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.cliTimeoutMs=o,this.plugin.saveSettingsDebounced())}));else{n.createEl("h3",{text:e("apiProviderHeader")});let i=w(this.plugin.settings);new g.Setting(n).setName(e("settingProviderPresetName")).setDesc(e("settingProviderPresetDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(F))s.addOption(o,l.label);return s.setValue(this.plugin.settings.apiProvider).onChange(async o=>{Ne(this.plugin.settings,o),await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingApiFormatName")).setDesc(e("settingApiFormatDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(S))s.addOption(o,l.label);return s.setValue(A(this.plugin.settings)).onChange(async o=>{this.plugin.settings.apiFormat=o,await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingBaseUrlName")).setDesc(e("settingBaseUrlDesc")).addText(s=>s.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":i.baseUrl||S[A(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async o=>{this.plugin.settings.apiBaseUrl=o.trim(),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingApiKeyName")).setDesc(e("settingApiKeyDesc")).addText(s=>(s.inputEl.type="password",s.setPlaceholder("sk-...").setValue(this.plugin.settings.apiKey).onChange(async o=>{this.plugin.settings.apiKey=o.trim(),this.plugin.saveSettingsDebounced()}))),new g.Setting(n).setName(e("settingApiKeyEnvName")).setDesc(e("settingApiKeyEnvDesc")).addText(s=>s.setPlaceholder(i.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async o=>{this.plugin.settings.apiKeyEnvVar=o.trim(),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingAuthTypeName")).setDesc(e("settingAuthTypeDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(ke))s.addOption(o,l);return s.setValue(this.plugin.settings.apiAuthType||"auto").onChange(async o=>{this.plugin.settings.apiAuthType=o,await this.plugin.saveSettings()})}),new g.Setting(n).setName(e("settingHeadersName")).setDesc(e("settingHeadersDesc")).addTextArea(s=>s.setPlaceholder("cf-aig-authorization: Bearer ...").setValue(this.plugin.settings.apiHeaders).onChange(async o=>{this.plugin.settings.apiHeaders=o,this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingMaxTokensName")).addText(s=>s.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async o=>{let l=parseInt(o,10);!Number.isNaN(l)&&l>0&&(this.plugin.settings.apiMaxTokens=l,this.plugin.saveSettingsDebounced())})),new g.Setting(n).setName(e("settingStreamingName")).setDesc(e("settingStreamingDesc")).addToggle(s=>s.setValue(this.plugin.settings.streaming??!0).onChange(async o=>{this.plugin.settings.streaming=o,await this.plugin.saveSettings()}))}new g.Setting(n).setName(e("settingModelName")).setDesc(e(r?"settingModelDescApi":"settingModelDescCli")).addText(i=>i.setPlaceholder(r?w(this.plugin.settings).model||"model-id":y.model).setValue(this.plugin.settings.model).onChange(async s=>{this.plugin.settings.model=s.trim()||(r?"":y.model),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingMaxInputName")).setDesc(e("settingMaxInputDesc")).addText(i=>i.setValue(String(this.plugin.settings.maxDocChars||y.maxDocChars)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>=1e3&&(this.plugin.settings.maxDocChars=o,this.plugin.saveSettingsDebounced())})),n.createEl("h3",{text:e("promptHeader")}),new g.Setting(n).setName(e("settingPromptLanguageName")).setDesc(e("settingPromptLanguageDesc")).addDropdown(i=>{for(let[s,o]of Object.entries(q))i.addOption(s,o);return i.setValue(this.plugin.settings.promptLanguage||y.promptLanguage).onChange(async s=>{this.plugin.settings.promptLanguage=s,await this.plugin.saveSettings()})}),new g.Setting(n).setName(e("settingCardRangeName")).setDesc(e("settingCardRangeDesc")).addText(i=>i.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||y.minCards)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.minCards=o,this.plugin.settings.maxCardsi.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||y.maxCards)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.maxCards=Math.max(o,this.plugin.settings.minCards||y.minCards),this.plugin.saveSettingsDebounced())})),new g.Setting(n).setName(e("settingCustomPromptName")).setDesc(e("settingCustomPromptDesc")).addTextArea(i=>(i.inputEl.rows=8,i.setPlaceholder(e("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async s=>{this.plugin.settings.customSystemPrompt=s,this.plugin.saveSettingsDebounced()}))),new g.Setting(n).setName(e("settingTestBackendName")).setDesc(e(r?"settingTestBackendDescApi":"settingTestBackendDescCli")).addButton(i=>i.setButtonText("Test").onClick(async()=>{try{let s=await Ht(this.plugin.settings);new g.Notice(`\u2713 ${s.slice(0,180)}`,8e3)}catch(s){new g.Notice(e("backendTestFailed",{error:s.message}),1e4)}})),new g.Setting(n).setName(e("settingExportFolderName")).setDesc(e("settingExportFolderDesc")).addText(i=>i.setValue(this.plugin.settings.exportFolder).onChange(async s=>{this.plugin.settings.exportFolder=s.trim()||y.exportFolder,this.plugin.saveSettingsDebounced()})),n.createEl("h3",{text:e("cacheHeader")}),new g.Setting(n).setName(e("settingMaxCacheName")).setDesc(e("settingMaxCacheDesc")).addText(i=>{i.setValue(String(this.plugin.settings.maxCacheEntries||B));let s=async()=>{let o=parseInt(i.getValue(),10);if(Number.isFinite(o)&&o>0){this.plugin.settings.maxCacheEntries=o,await this.plugin.saveSettings();let l=await this.plugin.pruneCacheIfNeeded();l.length>0&&new g.Notice(e("cachePruned",{count:l.length})),this.display()}};return i.inputEl.addEventListener("change",s),i.inputEl.addEventListener("keydown",o=>{o.key==="Enter"&&i.inputEl.blur()}),i});let a=Object.keys(this.plugin.cache).length;new g.Setting(n).setName(e("cachedNotesName",{count:a})).setDesc(e("cachedNotesDesc")).addButton(i=>i.setButtonText(e("clearAllCacheButton")).setWarning().onClick(async()=>{let s=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new g.Notice(e("cacheClearedAll",{count:s})),this.display()}))}};var E=require("obsidian");function N(t,n,e,r){let a=t.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":e}});return a.title=e,typeof E.setIcon=="function"?(0,E.setIcon)(a,n):a.textContent=e,a.addEventListener("click",async i=>{i.preventDefault(),i.stopPropagation();try{await r()}catch(s){console.error(s),new E.Notice(`${e} failed: `+(s.message||s))}}),a}function D(t,n,e,r,a){let i=t.createEl("button",{cls:a||"parallel-reader-text-button",attr:{type:"button"}});return n&&typeof E.setIcon=="function"&&(0,E.setIcon)(i,n),i.createSpan({text:e}),i.addEventListener("click",async s=>{s.preventDefault(),s.stopPropagation();try{await r()}catch(o){console.error(o),new E.Notice(`${e} failed: `+(o.message||o))}}),i}async function O(t,n){try{await navigator.clipboard.writeText(t),new E.Notice(n)}catch(e){new E.Notice("Copy failed: "+(e.message||e))}}function Ge(t){return String(t||"").split("/").map(n=>n.trim()).filter(n=>!!n&&n!==".."&&n!==".").join("/")}function He(t){let n=Ge(t);if(!n)return[];let e=n.split("/");return e.map((r,a)=>e.slice(0,a+1).join("/"))}async function lt(t,n){for(let e of He(n))if(!t.vault.getAbstractFileByPath(e))try{await t.vault.createFolder(e)}catch(r){if(!t.vault.getAbstractFileByPath(e))throw r}}var v=require("obsidian");var ct=require("obsidian");var fe=class extends ct.Modal{constructor(n,e,r,a){super(n),this.plugin=e,this.card=r||{},this.onSave=a}onOpen(){let{contentEl:n}=this;n.empty(),n.createEl("h2",{text:this.plugin.t("editCardTitle")});let e=this.createLabeledInput(n,this.plugin.t("editCardTitleField"),this.card.title||""),r=this.createLabeledTextarea(n,this.plugin.t("editCardGistField"),this.card.gist||"",3),a=this.createLabeledTextarea(n,this.plugin.t("editCardBulletsField"),(this.card.bullets||[]).join(` `),8),i=n.createDiv({cls:"parallel-reader-modal-actions"});D(i,null,this.plugin.t("editCardCancel"),()=>this.close(),"parallel-reader-text-button"),D(i,null,this.plugin.t("editCardSave"),async()=>{await this.onSave({title:e.value.trim()||this.card.title||"",gist:r.value.trim(),bullets:a.value.split(/\r?\n/).map(s=>s.trim()).filter(Boolean)}),this.close()},"parallel-reader-text-button")}createLabeledInput(n,e,r){let a=n.createDiv({cls:"parallel-reader-modal-field"});a.createEl("label",{text:e});let i=a.createEl("input",{attr:{type:"text"}});return i.value=r,i}createLabeledTextarea(n,e,r,a){let i=n.createDiv({cls:"parallel-reader-modal-field"});i.createEl("label",{text:e});let s=i.createEl("textarea");return s.rows=a,s.value=r,s}};var R="parallel-reader-view",ye=class extends v.ItemView{constructor(e,r){super(e);this.stale=!1;this.loadingMessage="";this.errorMessage="";this.plugin=r,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1}getViewType(){return R}getDisplayText(){return this.plugin.t("displayName")}getIcon(){return"book-open"}onOpen(){let e=this.containerEl.children[1];return e.empty(),e.addClass("parallel-reader-container"),e.setAttr("tabindex","0"),e.addEventListener("keydown",r=>this.handleKeydown(r)),this.renderEmpty(),this.focusSummaryPane(),Promise.resolve()}onClose(){return Promise.resolve()}renderEmpty(){this.sourceFile=null,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let e=this.containerEl.children[1];e.empty();let r=e.createDiv({cls:"parallel-reader-empty"});r.createEl("h3",{text:this.plugin.t("appTitle")}),r.createEl("p",{text:this.plugin.t("emptyOpenNote")}),r.createEl("code",{text:this.plugin.t("commandGenerate")})}focusSummaryPane(){let e=this.containerEl.children[1];return!e||typeof e.focus!="function"?!1:(e.focus({preventScroll:!0}),!0)}async loadFor(e,r,a){this.sourceFile=e,this.sections=r,this.stale=!!a,this.loadingMessage="",this.errorMessage="",this.render()}async renderLoading(e,r){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage=r||this.plugin.t("loadingDefault"),this.errorMessage="",this.render()}renderStreamingPreview(e,r){this.sourceFile?.path!==e.path&&(this.sourceFile=e);let a=this.containerEl.children[1],i=a.querySelector(".parallel-reader-streaming-preview");if(i){let h=i.querySelector("pre");h&&(h.textContent=r.slice(-2e3));let f=i.querySelector(".parallel-reader-stream-counter");f&&(f.textContent=`${r.length} chars`);return}a.empty();let o=a.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});o.createEl("div",{text:e.basename,cls:"parallel-reader-title"});let l=o.createDiv({cls:"parallel-reader-actions"});N(l,"square",this.plugin.t("actionCancel"),()=>{this.plugin.cancelGenerationForFile(e)});let d=a.createDiv({cls:"parallel-reader-state parallel-reader-loading parallel-reader-streaming-preview"});d.createDiv({cls:"parallel-reader-spinner"});let c=d.createEl("div",{cls:"parallel-reader-state-title"});c.createSpan({text:this.plugin.t("loadingGenerating")+" "}),c.createSpan({cls:"parallel-reader-stream-counter",text:`${r.length} chars`});let p=d.createEl("pre",{cls:"parallel-reader-stream-text"});p.textContent=r.slice(-2e3)}async renderError(e,r){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage=r||this.plugin.t("errorTitle"),this.render()}renderEmptyWithHint(e){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let r=this.containerEl.children[1];r.empty();let a=r.createDiv({cls:"parallel-reader-empty"});a.createEl("h3",{text:e.basename}),a.createEl("p",{text:this.plugin.t("emptyNoCache")}),a.createEl("code",{text:this.plugin.t("commandGenerate")})}render(){let e=this.containerEl.children[1];e.empty();let a=e.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});a.createEl("div",{text:this.sourceFile?.basename||"",cls:"parallel-reader-title"});let i=a.createDiv({cls:"parallel-reader-actions"});if(this.sourceFile&&(this.plugin.isGeneratingFile(this.sourceFile)?N(i,"square",this.plugin.t("actionCancel"),()=>{this.plugin.cancelGenerationForFile(this.sourceFile)}):N(i,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0)),N(i,"copy",this.plugin.t("actionCopyAll"),()=>this.plugin.copyCurrentViewMarkdown()),N(i,"download",this.plugin.t("actionExport"),()=>this.exportToVault())),this.stale){let l=e.createDiv({cls:"parallel-reader-stale-banner"});l.createSpan({text:this.plugin.t("staleBanner")}),D(l,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let l=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});l.createDiv({cls:"parallel-reader-spinner"}),l.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),l.createEl("div",{text:this.plugin.t("loadingSubtitle"),cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let l=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});l.createEl("div",{text:this.plugin.t("errorTitle"),cls:"parallel-reader-state-title"}),l.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),D(l,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let s=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let o=this.sourceFile?.path||"";this.sections.forEach((l,d)=>{let c=s.createDiv({cls:"parallel-reader-card"});c.dataset.idx=String(d),l.startLine<0&&c.addClass("parallel-reader-card-unanchored");let p=c.createEl("div",{cls:"parallel-reader-card-title"});if(p.createSpan({text:l.title}),l.startLine<0&&p.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:this.plugin.t("anchorMismatch")}),l.gist){let f=c.createEl("div",{cls:"parallel-reader-gist"});v.MarkdownRenderer.render(this.app,l.gist,f,o,this).catch(()=>{f.setText(l.gist)})}let h=l.bullets||[];if(h.length>0){let f=c.createEl("div",{cls:"parallel-reader-bullets-md"}),u=h.map(C=>`- ${C}`).join(` `);v.MarkdownRenderer.render(this.app,u,f,o,this).catch(()=>{f.setText(u)})}else l.gist||c.createEl("div",{cls:"parallel-reader-empty-li",text:this.plugin.t("emptyCard")});c.addEventListener("click",f=>{let u=window.getSelection();if(u&&u.toString().length>0)return;let C=f.target;C&&C.tagName==="A"||l.startLine>=0&&this.plugin.scrollEditorToLine(l.startLine,this.sourceFile)}),c.addEventListener("contextmenu",f=>{f.preventDefault();let u=new v.Menu;u.addItem(C=>C.setTitle(this.plugin.t("menuCopyMarkdown")).setIcon("copy").onClick(()=>O(Pe(l),this.plugin.t("copiedMarkdown")))),u.addItem(C=>C.setTitle(this.plugin.t("menuCopyPlain")).setIcon("clipboard-copy").onClick(()=>O(Qe(l),this.plugin.t("copiedPlain")))),l.anchor&&u.addItem(C=>C.setTitle(this.plugin.t("menuCopyAnchor")).setIcon("quote-glyph").onClick(()=>O(l.anchor,this.plugin.t("copiedAnchor")))),u.addSeparator(),l.startLine>=0&&u.addItem(C=>C.setTitle(this.plugin.t("menuJumpSource")).setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(l.startLine,this.sourceFile))),u.addSeparator(),u.addItem(C=>C.setTitle(this.plugin.t("menuEditCard")).setIcon("pencil").onClick(()=>this.openEditCardModal(d))),u.addItem(C=>C.setTitle(this.plugin.t("menuDeleteCard")).setIcon("trash").onClick(()=>this.deleteCard(d))),u.showAtMouseEvent(f)}),this.cards.push(c)}),this.activeIdx>=0&&this.cards[this.activeIdx]&&this.cards[this.activeIdx].addClass("is-active")}setActiveSection(e){e!==this.activeIdx&&(this.activeIdx>=0&&this.cards[this.activeIdx]&&this.cards[this.activeIdx].removeClass("is-active"),this.activeIdx=e,e>=0&&this.cards[e]&&(this.cards[e].addClass("is-active"),this.cards[e].scrollIntoView({block:"nearest",behavior:"smooth"})))}moveActiveSection(e){let r=oe(this.activeIdx,this.sections.length,e);return this.setActiveSection(r),this.focusSummaryPane(),r}jumpToActiveSection(){let e=le(this.sections,this.activeIdx);return e<0||!this.sourceFile?-1:(this.plugin.scrollEditorToLine(e,this.sourceFile),e)}handleKeydown(e){if(e.altKey&&e.key==="ArrowUp"){e.preventDefault(),this.moveActiveSection(-1);return}if(e.altKey&&e.key==="ArrowDown"){e.preventDefault(),this.moveActiveSection(1);return}!e.altKey&&!e.metaKey&&!e.ctrlKey&&!e.shiftKey&&e.key==="Enter"&&this.jumpToActiveSection()>=0&&e.preventDefault()}async deleteCard(e){if(!this.sourceFile)return!1;let r=te(this.sections,e);if(r.length===this.sections.length)return!1;let a=this.sections.length;return this.sections=r,this.activeIdx=re(e,a,this.activeIdx),await this.plugin.cacheReplaceCards(this.sourceFile.path,r),this.render(),new v.Notice(this.plugin.t("cardDeleted")),!0}openEditCardModal(e){return!this.sourceFile||!this.sections[e]?!1:(new fe(this.app,this.plugin,this.sections[e],async r=>{await this.updateCard(e,r)}).open(),!0)}async updateCard(e,r){if(!this.sourceFile)return!1;let a=ne(this.sections,e,r);return a.length!==this.sections.length?!1:(this.sections=a,await this.plugin.cacheReplaceCards(this.sourceFile.path,a),this.render(),new v.Notice(this.plugin.t("cardSaved")),!0)}async exportToVault(){if(!this.sourceFile)return;let e=Ge(this.plugin.settings.exportFolder),r=`${this.sourceFile.basename} - ${this.plugin.t("displayName")}.md`,a=`${e}/${r}`,i=["---",`source: [[${this.sourceFile.basename}]]`,`generated: ${new Date().toISOString().slice(0,10)}`,"tool: parallel-reader","---","",$(`${this.sourceFile.basename} \xB7 ${this.plugin.t("displayName")}`,this.sections),""].join(` `),s=this.plugin.app;await lt(s,e);let o=s.vault.getAbstractFileByPath(a);o instanceof v.TFile?await s.vault.modify(o,i):await s.vault.create(a,i),new v.Notice(this.plugin.t("exported",{path:a}))}};async function jt(t,n,e,r){let{system:a,user:i}=Me(t,n),s,o=I(n.backend)&&Ve(n),l=o?new AbortController:null;switch(l&&e.onCancel(()=>l.abort()),n.backend){case"codex":s=await Ze(a,i,n,e);break;case"api":case"anthropic-api":o?s=await st(a,i,n,r,l.signal):s=await me(m.requestUrl,a,i,n);break;default:s=await Xe(a,i,n,e);break}let d=s.map(c=>({title:c.title,level:2,anchor:c.anchor,gist:c.gist,startLine:ee(t,c.anchor),bullets:c.bullets}));return d.sort((c,p)=>c.startLine<0&&p.startLine<0?0:c.startLine<0?1:p.startLine<0?-1:c.startLine-p.startLine),d}function ut(t,n){return n?.phase==="generating"&&t&&I(t.backend)?"cancelRequestedApiInFlight":"cancelRequested"}var je=class extends m.Plugin{constructor(){super(...arguments);this._scrollDispose=null;this._settingsSaveTimer=null;this._cacheSaveTimer=null;this._cacheDirty=!1}t(e,r){return b(this.settings||y,e,r)}async onload(){await this.loadSettings(),this.jobs=new H,this._cacheSaveTimer=null,this._cacheDirty=!1,this.addRibbonIcon("book-open",this.t("ribbonOpen"),async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&await this.syncViewToFile(e.file)}),this.registerView(R,e=>new ye(e,this)),this.addCommand({id:"parallel-reader-run",name:this.t("cmdRun"),callback:()=>this.runForActiveFile(!1)}),this.addCommand({id:"parallel-reader-regen",name:this.t("cmdRegen"),callback:()=>this.runForActiveFile(!0)}),this.addCommand({id:"parallel-reader-open-view",name:this.t("cmdOpenView"),callback:async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&this.syncViewToFile(e.file)}}),this.addCommand({id:"parallel-reader-export-current",name:this.t("cmdExport"),callback:()=>this.exportCurrentView()}),this.addCommand({id:"parallel-reader-copy-current-markdown",name:this.t("cmdCopyMarkdown"),callback:()=>this.copyCurrentViewMarkdown()}),this.addCommand({id:"parallel-reader-cancel-current",name:this.t("cmdCancel"),callback:()=>this.cancelActiveGeneration()}),this.addCommand({id:"parallel-reader-clear-current",name:this.t("cmdClearCurrent"),callback:async()=>{let e=this.getActiveView();if(!e?.file)return new m.Notice(this.t("noCurrentNote"));await this.cacheDelete(e.file.path),new m.Notice(this.t("cacheClearedFile",{name:e.file.basename}))}}),this.addCommand({id:"parallel-reader-clear-all",name:this.t("cmdClearAll"),callback:async()=>{let e=Object.keys(this.cache).length;await this.cacheClear(),new m.Notice(this.t("cacheClearedAll",{count:e}))}}),this.addCommand({id:"parallel-reader-card-prev",name:this.t("cmdCardPrev"),hotkeys:[{modifiers:["Alt"],key:"ArrowUp"}],callback:()=>this.moveActiveCard(-1)}),this.addCommand({id:"parallel-reader-card-next",name:this.t("cmdCardNext"),hotkeys:[{modifiers:["Alt"],key:"ArrowDown"}],callback:()=>this.moveActiveCard(1)}),this.addCommand({id:"parallel-reader-card-jump",name:this.t("cmdCardJump"),callback:()=>this.jumpActiveCard()}),this.addSettingTab(new ge(this.app,this)),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>this.bindScrollSync())),this.registerEvent(this.app.workspace.on("file-open",e=>{e&&this.syncViewToFile(e)})),this.registerEvent(this.app.workspace.on("file-menu",(e,r)=>this.addFileMenuItems(e,r))),this.registerEvent(this.app.vault.on("rename",(e,r)=>this.handleFileRename(e,r))),this.registerEvent(this.app.vault.on("delete",e=>this.handleFileDelete(e))),this.bindScrollSync()}async onunload(){await this.flushSettingsSave(),await this.flushCacheSave(),this.app.workspace.detachLeavesOfType(R)}async loadSettings(){let r=(await this.loadData()||{}).settings||{};this.settings=De(Object.assign({},y,r)),await this.loadCache()}async saveSettings(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null),await this.saveData({settings:this.settings})}saveSettingsDebounced(e=400){this._settingsSaveTimer&&clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=setTimeout(()=>{this._settingsSaveTimer=null,this.saveSettings().catch(r=>console.error("[parallel-reader] failed to save settings",r))},e)}async flushSettingsSave(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null,await this.saveSettings())}async saveCache(){this._cacheSaveTimer&&(clearTimeout(this._cacheSaveTimer),this._cacheSaveTimer=null),this.pruneCache(),await this.writeCacheFile(),this._cacheDirty=!1}scheduleCacheSave(e=5e3){this._cacheDirty=!0,!this._cacheSaveTimer&&(this._cacheSaveTimer=setTimeout(()=>{this._cacheSaveTimer=null,this._cacheDirty&&this.saveCache().catch(r=>console.error("[parallel-reader] failed to save cache",r))},e))}async flushCacheSave(){this._cacheSaveTimer&&(clearTimeout(this._cacheSaveTimer),this._cacheSaveTimer=null),this._cacheDirty&&await this.saveCache()}cacheFilePath(){let e=this.app.vault.configDir||".obsidian",r=this.manifest?.id||"parallel-reader";return`${e}/plugins/${r}/cache.json`}async ensurePluginDataDir(){let e=this.app.vault.adapter,r=this.app.vault.configDir||".obsidian",a=this.manifest?.id||"parallel-reader",i=`${r}/plugins/${a}`;try{if(typeof e.exists=="function"&&await e.exists(i))return;await e.mkdir(i)}catch{}}async readCacheFile(){let e=this.app.vault.adapter;try{let r=await e.read(this.cacheFilePath()),a=JSON.parse(r);if(a&&typeof a=="object"&&a.entries&&typeof a.entries=="object")return a.entries}catch(r){let a=String(r?.message||r||"");/not found|does not exist|ENOENT/i.test(a)||console.warn("[parallel-reader] failed to read cache.json",r)}return{}}async writeCacheFile(){await this.ensurePluginDataDir(),await this.app.vault.adapter.write(this.cacheFilePath(),be(this.cache))}async loadCache(){this.cache=await this.readCacheFile(),this.pruneCache().length>0&&await this.writeCacheFile()}pruneCache(){return Re(this.cache,this.settings?.maxCacheEntries||B)}async pruneCacheIfNeeded(){let e=this.pruneCache();return e.length>0&&await this.saveCache(),e}cacheGet(e){return this.cache[e]||null}async cacheTouch(e){let r=Ce(this.cache[e]||null);return r?(this.scheduleCacheSave(),r):null}async cachePut(e,r,a,i){let s=new Date().toISOString();this.cache[e]={schemaVersion:z,contentHash:V(r),settingsHash:ce(i||this.settings),cards:a,generatedAt:s,lastAccessedAt:s},await this.saveCache()}async cacheReplaceCards(e,r){let a=this.cache[e];if(!a)return!1;let i=new Date().toISOString();return a.cards=(r||[]).map(s=>({title:s.title,anchor:s.anchor,gist:s.gist,bullets:s.bullets||[]})),a.updatedAt=i,a.lastAccessedAt=i,await this.saveCache(),!0}async cacheDelete(e){this.cache[e]&&(delete this.cache[e],await this.saveCache())}async cacheClear(){this.cache={},await this.saveCache()}async ensureView(){let{workspace:e}=this.app,r=e.getLeavesOfType(R)[0];return r||(r=e.getRightLeaf(!1),await r.setViewState({type:R,active:!0})),e.revealLeaf(r),r.view}getActiveView(){return this.app.workspace.getActiveViewOfType(m.MarkdownView)}getParallelView(){return this.app.workspace.getLeavesOfType(R)[0]?.view}moveActiveCard(e){let r=this.getParallelView();return r?.sections.length?r.moveActiveSection(e):(new m.Notice(this.t("noActiveCard")),-1)}jumpActiveCard(){let e=this.getParallelView();return!e||e.jumpToActiveSection()<0?(new m.Notice(this.t("noActiveCard")),!1):!0}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e?.path)return new m.Notice(this.t("noCancelableJob")),!1;let r=this.jobs.get(e.path),a=ut(this.settings,r),i=this.jobs.cancel(e.path);return new m.Notice(i?this.t(a):this.t("noCancelableJob")),i}viewIsShowingFile(e,r){return!!e&&!!r&&e.sourceFile?.path===r.path}activeFileStillMatches(e){let r=this.getActiveView();return!r?.file||r.file.path===e.path}cancelActiveGeneration(){let e=this.getActiveView();if(e?.file&&this.cancelGenerationForFile(e.file))return;let r=this.getParallelView();r?.sourceFile?this.cancelGenerationForFile(r.sourceFile):new m.Notice(this.t("noCancelableJob"))}confirmRegenerateEditedCards(){let e=this.t("confirmRegenerateEditedCards");return typeof window<"u"&&typeof window.confirm=="function"?window.confirm(e):!0}addFileMenuItems(e,r){!(r instanceof m.TFile)||!r.path.endsWith(".md")||(e.addSeparator(),e.addItem(a=>a.setTitle(this.t("fileMenuGenerate")).setIcon("book-open").onClick(()=>this.runForFile(r,!1))),e.addItem(a=>a.setTitle(this.t("fileMenuRegen")).setIcon("refresh-cw").onClick(()=>this.runForFile(r,!0))),this.cacheGet(r.path)&&e.addItem(a=>a.setTitle(this.t("fileMenuClear")).setIcon("trash").onClick(async()=>{await this.cacheDelete(r.path),new m.Notice(this.t("cacheClearedFile",{name:r.basename}))})))}async handleFileRename(e,r){if(!(e instanceof m.TFile)||!r)return;let a=r.endsWith(".md"),i=e.path.endsWith(".md");if(!a&&!i)return;if(a&&!i){this.cache[r]&&(delete this.cache[r],await this.saveCache());let o=this.getParallelView();o&&o.sourceFile?.path===r&&o.renderEmpty();return}if(!a)return;this.cache[r]&&(this.cache[e.path]=this.cache[r],delete this.cache[r],await this.saveCache());let s=this.getParallelView();s?.sourceFile&&(s.sourceFile.path===r||s.sourceFile.path===e.path)&&(s.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof m.TFile))return;this.cache[e.path]&&(delete this.cache[e.path],await this.saveCache());let r=this.getParallelView();r?.sourceFile?.path===e.path&&r.renderEmpty()}async exportCurrentView(){let e=this.getActiveView(),r=await this.ensureView();if((!r.sourceFile||!r.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!r.sourceFile||!r.sections.length){new m.Notice(this.t("noExportContent"));return}await r.exportToVault()}async copyCurrentViewMarkdown(){let e=this.getActiveView(),r=await this.ensureView();if((!r.sourceFile||!r.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!r.sourceFile||!r.sections.length){new m.Notice(this.t("noCopyContent"));return}await O($(`${r.sourceFile.basename} \xB7 ${this.t("displayName")}`,r.sections),this.t("copiedAllMarkdown"))}async runForActiveFile(e){let r=this.getActiveView();if(!r?.file){new m.Notice(this.t("openNoteFirst"));return}return this.runForFile(r.file,e)}async runForFile(e,r){if(!e){new m.Notice(this.t("openNoteFirst"));return}if(this.jobs.isRunning(e.path)){new m.Notice(this.t("alreadyGenerating"));return}if(we(this.cacheGet(e.path),r)&&!this.confirmRegenerateEditedCards()){new m.Notice(this.t("regenerateCancelled"));return}let a=null;return this.jobs.start(e.path,async i=>{i.setPhase("reading");let s=await this.app.vault.read(e);if(i.throwIfCancelled(),!s.trim()){new m.Notice(this.t("emptyNote"));return}if(a=await this.ensureView(),i.throwIfCancelled(),i.setPhase("cache-check"),!r){let h=this.cacheGet(e.path);if(ue(h,s,this.settings)){await this.cacheTouch(e.path),this.activeFileStillMatches(e)&&await a.loadFor(e,this.resolveCardAnchors(s,h.cards),!1);return}}await a.renderLoading(e,this.t("loadingGenerating"));let o=Number(this.settings.maxDocChars)||y.maxDocChars;s.length>o&&new m.Notice(this.t("longNoteTruncated",{count:o})),new m.Notice(this.t("generatingNotice")),i.setPhase("generating");let l=a?h=>{!h.done&&this.viewIsShowingFile(a,e)&&a.renderStreamingPreview(e,h.accumulated)}:void 0,d=await jt(s,this.settings,i,l);if(i.throwIfCancelled(),d.length===0){new m.Notice(this.t("noCardsReturned"));return}let c=d.map(h=>({title:h.title,anchor:h.anchor,gist:h.gist,bullets:h.bullets}));i.setPhase("saving"),await this.cachePut(e.path,s,c,this.settings),i.throwIfCancelled(),this.viewIsShowingFile(a,e)&&await a.loadFor(e,d,!1);let p=d.filter(h=>h.startLine<0).length;new m.Notice(this.t("generationDone",{count:d.length,suffix:p?this.t("unanchoredSuffix",{count:p}):""}))}).catch(async i=>{if(i instanceof _){new m.Notice(i.message);return}if(i instanceof x){a&&this.viewIsShowingFile(a,e)&&await a.renderError(e,this.t("cancelledError")),new m.Notice(this.t("cancelled"));return}let s=Ae(i);console.error(i),a&&this.viewIsShowingFile(a,e)&&await a.renderError(e,i.message||String(i)),new m.Notice(this.t("generationFailed",{kind:s==="unknown"?"":` (${s})`,error:i.message||i}))})}resolveCardAnchors(e,r){let a=(r||[]).map(i=>({title:i.title,level:2,anchor:i.anchor,gist:i.gist,startLine:ee(e,i.anchor),bullets:i.bullets||[]}));return a.sort((i,s)=>i.startLine<0&&s.startLine<0?0:i.startLine<0?1:s.startLine<0?-1:i.startLine-s.startLine),a}async syncViewToFile(e){if(!e?.path?.endsWith(".md"))return;let r=this.app.workspace.getLeavesOfType(R);if(r.length===0)return;let a=r[0].view;if(!a||!this.activeFileStillMatches(e))return;let i=this.cacheGet(e.path);if(!i){await a.loadFor(e,[],!1),a.renderEmptyWithHint(e);return}let s=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let o=!ue(i,s,this.settings);await this.cacheTouch(e.path),await a.loadFor(e,this.resolveCardAnchors(s,i.cards),o)}bindScrollSync(){this._scrollDispose&&(this._scrollDispose(),this._scrollDispose=null);let e=this.getActiveView();if(!e)return;let r=e.editor,a=r&&r.cm,i=a?.scrollDOM?a.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!i)return;let s=Ue(()=>this.handleEditorScroll(e));i.addEventListener("scroll",s,{passive:!0}),this._scrollDispose=()=>{s.cancel(),i.removeEventListener("scroll",s)}}handleEditorScroll(e){let r=this.getParallelView();if(!r||!e.file||r.sourceFile?.path!==e.file.path)return;let a=e.editor,i=a&&a.cm;if(!i?.scrollDOM)return;let s=i.scrollDOM.getBoundingClientRect(),o=Be(s),l=0;try{let c=i.posAtCoords({x:s.left+20,y:o});c!=null&&(l=i.state.doc.lineAt(c).number-1)}catch{return}let d=-1;for(let c=0;c part.trim()) - .filter(Boolean) + .filter((part) => !!part && part !== '..' && part !== '.') .join('/'); } diff --git a/tests/modules.test.js b/tests/modules.test.js index 38f162e..77e251e 100644 --- a/tests/modules.test.js +++ b/tests/modules.test.js @@ -173,6 +173,9 @@ assert.deepStrictEqual(t.folderPathsForTarget(''), [], 'empty path'); assert.deepStrictEqual(t.folderPathsForTarget('A/B/C'), ['A', 'A/B', 'A/B/C']); assert.deepStrictEqual(t.folderPathsForTarget('/A//B/'), ['A', 'A/B'], 'normalizes slashes'); assert.deepStrictEqual(t.folderPathsForTarget('single'), ['single']); +assert.deepStrictEqual(t.folderPathsForTarget('../../etc/passwd'), ['etc', 'etc/passwd'], 'strips .. path traversal'); +assert.deepStrictEqual(t.folderPathsForTarget('./a/../b'), ['a', 'a/b'], 'strips . and .. segments'); +assert.deepStrictEqual(t.folderPathsForTarget('a/../../b'), ['a', 'a/b'], 'strips mid-path ..'); // ── i18n.ts ──