From 184347988439a72916dfe79a39405cd753af5a50 Mon Sep 17 00:00:00 2001 From: muena Date: Wed, 20 May 2026 06:46:42 +0200 Subject: [PATCH] Initial commit: Velum Raycast extension Pseudonymize-and-AI workflow for handling PII-sensitive text via the Velum API and Raycast AI. Commands cover end-to-end email summary and reply, briefing/action-items/structured-data extraction, manual pseudonymize/depseudonymize on selection or clipboard, and session management. Includes Raycast 2.0 Beta workarounds for selection capture and rich-text clipboard. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 + README.md | 35 + assets/extension-icon.png | Bin 0 -> 11809 bytes eslint.config.js | 10 + package-lock.json | 3019 +++++++++++++++++++++++++++ package.json | 253 +++ src/ai-views.tsx | 341 +++ src/ai.ts | 22 + src/auth.ts | 133 ++ src/briefing-from-notes.tsx | 297 +++ src/depseudonymize-clipboard.ts | 7 + src/depseudonymize-selected-text.ts | 15 + src/depseudonymize-text.tsx | 176 ++ src/extract-action-items.tsx | 302 +++ src/extract-structured-data.tsx | 362 ++++ src/manage-sessions.tsx | 260 +++ src/preferences.ts | 46 + src/pseudonymize-clipboard.ts | 7 + src/pseudonymize-selected-text.ts | 15 + src/pseudonymize-text.tsx | 248 +++ src/quick.ts | 121 ++ src/reply-email.tsx | 783 +++++++ src/reply.ts | 304 +++ src/selection.ts | 35 + src/sessions.ts | 192 ++ src/summarize-email.tsx | 538 +++++ src/summarize.ts | 58 + src/types.ts | 55 + src/ui.ts | 57 + src/velum.ts | 144 ++ tsconfig.json | 16 + 31 files changed, 7855 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 assets/extension-icon.png create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/ai-views.tsx create mode 100644 src/ai.ts create mode 100644 src/auth.ts create mode 100644 src/briefing-from-notes.tsx create mode 100644 src/depseudonymize-clipboard.ts create mode 100644 src/depseudonymize-selected-text.ts create mode 100644 src/depseudonymize-text.tsx create mode 100644 src/extract-action-items.tsx create mode 100644 src/extract-structured-data.tsx create mode 100644 src/manage-sessions.tsx create mode 100644 src/preferences.ts create mode 100644 src/pseudonymize-clipboard.ts create mode 100644 src/pseudonymize-selected-text.ts create mode 100644 src/pseudonymize-text.tsx create mode 100644 src/quick.ts create mode 100644 src/reply-email.tsx create mode 100644 src/reply.ts create mode 100644 src/selection.ts create mode 100644 src/sessions.ts create mode 100644 src/summarize-email.tsx create mode 100644 src/summarize.ts create mode 100644 src/types.ts create mode 100644 src/ui.ts create mode 100644 src/velum.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..092548e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.DS_Store +raycast-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..733324a --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Velum Raycast Extension + +Raycast-Extension zum Pseudonymisieren und Wiederherstellen von Text mit Velum. + +## Konfiguration + +Öffne die Raycast-Einstellungen für die Extension und konfiguriere: + +- Velum Basis-URL, z. B. `https://velum.example.com` +- Authentik Token-URL, üblicherweise `https://auth.example.com/application/o/token/` +- OAuth Client-ID +- Dienstkonto-Benutzername +- Dienstkonto App-Passwort +- Optionaler OAuth-Scope, Standard `profile` + +Das Dienstkonto-Passwort wird als Raycast-Passwort-Preference gespeichert. Access-Tokens und Velum-Sitzungen liegen im Raycast-LocalStorage. + +## Befehle + +- `Email-Konversation zusammenfassen`: markierten Email-Verlauf pseudonymisieren, per Raycast-KI zusammenfassen und wiederherstellen. +- `Text pseudonymisieren`: Text manuell eingeben oder markierten/Zwischenablage-Text laden, Sitzung wählen, Ergebnis kopieren oder einfügen. +- `Markierten Text pseudonymisieren`: Schnellbefehl für markierten Text. +- `Zwischenablage pseudonymisieren`: Schnellbefehl für die Zwischenablage. +- `Text wiederherstellen`: Platzhalter mit einer gespeicherten Sitzungs-Zuordnung wiederherstellen. +- `Sitzungen verwalten`: Zuordnungs-Sitzungen anlegen, aktivieren, ansehen, leeren oder löschen. + +## Sitzungsverhalten + +Velum-Zuordnungen enthalten die Originalwerte. Sitzungen steuern, welche Anfragen sich eine Zuordnung teilen: + +- `Aktive Sitzung wiederverwenden`: setzt die aktuelle Zuordnung fort. +- `Neue Sitzung pro Anfrage`: isoliert jede Schnellbefehl-Anfrage. +- `Tagessitzung`: nutzt eine Zuordnung pro Tag. + +Der interaktive Befehl bietet immer eine explizite Sitzungs-Auswahl plus eine `Neue Sitzung`-Option. diff --git a/assets/extension-icon.png b/assets/extension-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..841df318a0f0a2086db592a4ada8020e4bb0623d GIT binary patch literal 11809 zcmdsdRX|i-^zR-*P$U#AIu$`_3{)7E78I3`7(fAO=~99DR7AQI1SOOKrG`ct{8W$- zBu79(N@3`(nYsJSeBS@d|MkAy2Z!T2tA1;(9cS+qqIXq;br1I*0066&rs@p<=-?wA zVBQ7)toU|s!9TlQHH|$0@a9GSq2B%LI}M3PJk@S_-gLR^>2urN2Ke~+NZH?W^su_^ zY9r<1Zu@HPJU0M>Kuh(azVGYV!3VD~HvaRA^Tlf^O%bA^9zsnzUtJGy?U&!}^YLkz zUi86g;GD;QEho>ew2XgX+TNn^F-s+b&KeknqGF@qS{T;+I(MwiDY?C_e%WKi>3$t z+J(-=Mb7G(AGKKxGe3&od#;nNm}H+TtR-*ZZ(4)1XKUF7wED&DhjS>^gWO*l9I9~2 zlJb_;OyLTaHca8ITiRkKMv*@G!$mf;SA!KV#|i;!+urIm#tCtyPYot^5g0q%P0>;< z2Ujz0VBM@qGBk0qNjcrgZMCBRWjgEQTx0~^QF0F*T6?n1*@ZiFX49~PF}6*%^O9C$ zC7uPG|L{%QF?{7^j6aT-*rEH%aIHd0LX`pgMB#Mmg=6?A>4b;`tq#A~3q4-Cfa961 z*aZnI0ddwIiqrCBqIMjH}L6?zEcsuvrRI1}#86V$e zJWDeRUH##3+3Qva-gyCa1;dj+g1BcZ-mis(L zD}SwPP146+b;qy+OJSV^-29hLqaP>E$kElK%t-^}e3Kk@%{2)v8-gMVILMutiWCp6 z?IrZMeJDNUjfs|K2C-MqPCVB#^|@AO-n)~9>%tJ|f##R6-Xv+=Q(GoJK8=$2Ft@z# zD9g6jYTsVm*i_dNxLd{!z7$Y6gqK;>K3Po9oAu0A@*C|r0`8WaaL!Fo7V3-AP8zM` z2UyPS+>X=fMVmUqTami>uqKl?boJrA9^6O!eyJ36Hi~_7g?`twREcg!bh?s;xu&Au zTw=hU^*E@T^ISW%L{1<hbVbtUWgmwM3!s5G~^(PA#O)CA93q6q*M-TNfBK7E`!#_xvFg2Y2wW_r-6dIImA1$T>8My!Pvd@1zD@}JfReTOLtDU z{D~uBBD*qfV9<9Q&WU1Ywub+vFZwp`Re`USOk0{>rHA#T&#dM&{gWpRR2^sn#38=I; zrKIys4+*!D|5P3X=r_&^h5jzXnJ?HYrL5ly6Bu=#$R2fYFkU)h_3hL79Tea{Go(IQ z`?NPB@LqUR&!q7^CBVx1yG<9s zFJEk?;IiSwv z;28myA@AogkIl%r$N)l6_`*>@8?QM(MW%G93Le>V8AuVS)yUBl2V|;jrWRvnYGSXt zVORjLmz5)GzxdNRG}LI}ZXQMi-_Hj&HM!+(i}KBvZ~#zLC>!e86yJ70y8Ohy_Q1~W z?1S3t6Nh5^UmrjL^qP{S+ry4t-D`Ls18lJeh&(sj|JxCbg+xyG;*Vtqs?FxJ~09%H?d^6z%!}^Z5A1X02%4);9pY z=KtV+KtdCm1v359yI~=2UOA{<$bZ0MiUrjB&V)*KR6MS^maDsbG%FHBVhUUhxrl~~ z3rxVWI#VrUqu2P@3FTAYUf~V`I8)ib-@#}d%kW}{iNm5xLERJYX6Gx;Rx za9N{FFMs&T$LsQy?bHmfMSW%Z@8?~bf(kq6W3@vpcma0wnJyQmld$DQ{THw3=Wv9Z9aszJD zfni4+Pi*y30D^2+Q!?W->JrDrIC)SCC9f|Gzb`WE3|Ez627s+Z_xeOQxy@iu%j`rU z8}|CdQ3RUc$wmn&u{D2G2m>}Ver-5<#Zk)))=c>HEbDUraN_462YyuR>p=2MrEhyz z>{+RhM*!4cUu8=O9=b&|M1SMM!WX}NH|Rlh;#sdx6LmSO!irvi+hm|!zW2Ov?F2i( zo-kJHs30X@J8wSAm^}xlukesnKdN;{b+;WxE1jP9k!+aWme}>?m&G~$5aau%iiqWd zM*eG84Meb$lk$-v#G@A^?afP1!+k@k9A={VyM0Yfj*sq2vVj+H!jLHYVLbxg;=Icf zl`7ej$((bb)$YQ(H$!asaSAm_Jaho7>MRg?G`YOSV{=4xNZ}V7I+>5miMUaJNS-(| zq>}RI)zNO$%X1%U?8W7zwUq9@Jq`CNS$zihy{SJ2s_wV=4$Hh@t9NaBsEy1z3*SoK z+}Pyg^!rqP4m9%l%(=Z-`hH(4E=PPX065}{${%IU{Ut>%=cW05n9^==lb<0`8p+Ap z7WNqPwK$JwLtkm>%-Xg-eJiUlvl9Sx(pIKw+(A!oso@K~ER8*=AC4=^{A2)p^X;$M z%)3PW4zk(>tioeT1@dZOzN1e$whHiAH9V}1+I-XyFJ`2zbTTFz>S>*b> zNTvDREW5n;AZqQ=b^3ZTJ7tR0L|_1*(^lCsTsECF^B&IB8UBg5xEoo!A>>(HW<{7Z z@9Z%RR?s{B)N>jIfbp}4ihz>Tb9aEuUE@AY0?ueZ9EkN*5ypnfo!59}L2F9Y*nTbD ztawCmZ}<12v9}_m&*FRllW-3%e}XKhl$Yd51)!kLz|x*V4>*9;3Y!&Pl5cA)m=T*< zajq?{b;hX~ChE9j(%{mlaWHx0HcBv3VRG{$6ju>`uu)7}Xa~1Q4o7>w~ZIfvRvP% zwhHcL(9Nx)R_%P72?RRq3o*FG<&bflb+p3LtI)efK8EWM+Ax=mIy_vv zlYwRHk$WvL1&K0Bm$^4N(q^*me2SEd`QV>QOu85Gu*RI7RfbMNx^hkAB%K0wYGj`b_hnl z$jauGuw>UCstlh<7srvTk6uF1!WFm6*04GNSGc_;6hbd5sLOdjCBi77Gw-T5IK+dEYTWp)>0i6x=f#E@b|c-4V#nmQy(>rc_^=9#+X7=>4x%lOo^I`! zsw252-oV@;glpR*ubB#TJ)iUo!EmCNHb_w+mJA#mR>CK@7A&S-ZXU~UCt(JOC0m&) zM!ZHhD#*UPgrDy-uC#UUc3xH_{vf5^8LD)y>dl+iZw!4`;&-L4xSvBgHISrsT`Np^ ziICLmeo2Bb+_(AGFmO}a`lP_#y^JDQEboVvyrc(1Hd&aufQA#srX6buDO$7q>BkeU zR4#?uR8CH}boa{{b!F-rWN~HbhblPt>z9_bb{JndIQ`Npb2`I|=Ro7QiRdx*I`X82 zdtTDe!;21t{M56i4?e6p8nthA&;8>hOUE1}>)=o9$r`>({xYvK>v9+|aKGKfET=d@ zDpvMxH{pnt=US&4mE>OW{B{ttF@FF__mxMriFEC$)$LY`HKN?pMs@LxKsgTi{-Nlm z@V4i|$y<1KiO`~L`nk{#H3r90WCDNYt1Ld*tXx{MId1qwxB~v=B(M5>85l~sqsA2W zHuh=_`EscCR4VaQg748JyPd~eYWTCW+{&b?M{*f*F2+W; z@v7J30Cd+=loXdA+?CyP*tj&iH|WsojLghw3WtWQVykl5+p_&{A$$+cjv(3vf}~iL*!nHVirc=3mPzg zUXaW`Q{mE|j?Eh=BRq`Y?4NY(__vGm{VufvyI)gEQ=i8iYun0Z&+UId>54a@h?tR# zACKGV_kf)Xkga zS;3KJDUk=RiRpEGd}v3i`?>nIYg^nt!Tf=>;v1Pd%L0#O@Fofvbh}E5wl-zQ;nUY8abyTm(pxbYMZhkzeE}|GEBb=2S zsKw3C-^=GSn7To(W6z_U!T;Gbt!o=5(?iEw@k0^Qm6S$&i{=D2?Vpd%_y5Ek+g}0w zM)y-bei!v7i%q@JNYS$xwiG6C|I#H=h6L=5)|Ci0g0~s4z)N{SpiKjn$u;%dhhjdO zjvJWkZF|3c@zrEGW0xy`P}rS{(V{R<9Xix^lbnIT z`>9WZC3owuR&KW7dt~$OMfF9Fg{Ex(suPclBe^^lfQjAX$5n9q@(07b0Hqkm8ov&n zo0Q&D8@3-G#eeoD8$s&gO^O{!goOc%4LZN4eEWBu=8O|IC%^JZF=`J^Kh$&eMS^JA za^ST~lG!4VQIf_u;so?t){_HGC|~!W1YL!q=+5u}&WY_dmApXf5TFO%L{cfgXp{&@ zq4A9U#{(%GG>X-t|L@dZ6*ED=rTIP`zWZhJxi)DY2^=x5z7`FV2@Pybzn=+mC$~UQbPX4Ne3SBF}j{YFK$#XUn2_C z3IV}CREFSrDg*pX5e+&AtzbtNsE#~T1@(@Xs4^QgJQn-FLXw^a>NP9JDVl3W41iaZ zYUfGIZzPxtM^GSmw-;A^ANaLMQ=Nida0sYWO?p1}X1n9-~U(180vSmi-y5TH8?UP#23 zLIKu|h3YeTnd&n_*-b-1re$R*98y@^5M1OXnp7l0p-It@E_I@}BOCU;bABcX*fg~%MHI=GHp8<$Ud`5DPYbPMd zVur~Ip@EuOq^Z#W=uw)Q`!qG*p_)*dn)6g|(wUiortUMOKs6)tkv#XVxl0DOf?uA^hxcP&u$x}Ko})%*Iz)1KMVZ%{G8b7LCC z4pOKkK&9-5Jeq%b4)dYF&~9fgJvgh;Z1euF6my7yW2p~NaWgfj1MCOr-xJ?c*eI%l zi$b8@6vpP#d7k=4qz39YIKW8hcXgCdGV90~k}As`X0TwcnU2+K`_E+=SqMTJf?)pZ z&dbAez#V~=@)wqj8v__uhf?Pu^Z`Uqni4{>gOrs2G*cwEK|fDk23bG^tYv z1>t%9R|=ur&`bxze{JcVgVrT+*Rk!;pE9H@GW-RtxBV&D_ zb`}mjCb+<(=YkaWEKE6pVQ)CD=$nLr;5rP?hU?$(SnmTIPomXP%FsB~9YH?^z#sxm zwLrxKNX~IkvE>tmJ+5AVg*-U`$8#to1>@z6luk51oNPHe#RdOIC=6gnpwQQ)zqY5K zZB3yFx}q~Y^&gN#ptqO-vmp$H0)$CZ!wS_LfNH+|wQUV;r$GpsV}I53!D-z0O*ma{ zY2_GxoAVscNVcR@FWkQ-{b-|H3osc7J^+FCL1Vtq*slFs+88Y*xao%frt41pL9<_2 zda`R*S+6H6^q`Cxh$HB=Qdpy(z&!&ZDFV`DE*)VM^*e>mi~WHFp!_00--cK;mz=OB zqJ>EKU7=r>cH47BclUuF?K?;eM%Yjwh_hK-u&Ef8eVsZM>QB;vJ;!j@(XqDlI;#7T zdPIGICUJUy_d6l4S!K*Wefor8aopk%&I_!+0A(bfU$3tpDf`^E>u^*szuzT)|HlS( zhz%y>B~)l=OHXrTfgV~t*esrQBgCsQxwLem@lIaPwYl;SnMjDSD$rUrv_?GbnQ{MA zeRj_^N77S#_ArF7zyc)?!C>bVqi8rcg5X#E>+5I}Tl#M5s9a!20bc`g+Rp9@(cxUm zI$Am}hnlOAV{~948>-%}OZz>f3O$9JwQKYE*-t?brOG~lrGFx~M{^p644I4R$?#kt znAwI)j|eA3WR7CmQHLj``)ICjC$Agy)DEktm*>+QA7%#i`=Ej!1#e6x4e`kf+lIMj zIbOsZil7h-QXm7c(tzV`P`VJjs8|s&$`*ZC($jWbdEefjNBEYT%C8 zyk6HROLjou{?YQc@9H*UOs3BgNfxYa-t1(=~Tn#c|k4#k(u0R0F~O^7}<0QSS0#`dakoT3BK1+dECQd0l} z&o%B(6@*Kbrk1$|LcSvik0YCi2)7<|N>I&@swDI=JkvWNP1Rk<0kF>}Y!FHDgHR!A zm?}96Jvp*S^+fzAB=57JDTI}JWXYMzE%o7oN*cCuu1=TLMd=FskUpF9k@jJ)y&q2a zX@4MV7_x!?Xp|g92S3m~xG#yqad(9Q)T1oaQTUE6rk~JEe-xaR3XAG$!tY?R^g~Aa?z{wCBzc@7#BkWKzpv{iQa zT@HMa4Jp*E0tMyaQU?VCAchn;KS9I`hv4zKo;VT+Y#lSezFShJ3PwOdiFPWP0?Ca_ zN>nm*#1~H_50vEC))3_yB9QR}^?JCOb$F<3IP7WAu7}+PZ;@UADQa4FppR#x6R2rc zfl0i&QF{QI{R$5%qdApbkc2KlC&!=VQ*%Lvxp=)ujV@Au(ela>bfe7+WL8F41at5u z?~)DIcdy~R{+_B1uOlEE4S;*|Dw5u47nt66W|4Fo-73*=1`upCi}=6Kzl-?))}p&q z%imfQ1jf{O{DMyY^QV$hrZ3P|RY`zZp@Y>)x3BTQh2>^vAQ38HuFx`T@tqW@k z3;n>(9Qo5DKfI~Y*Q^Imz$8CyO{~|Z4VvEHUv(6$L!g#rH4D#E$h|6}ox9)9^yS&s zt{OZ0dmGTbKrMv8VSEpB2qoxuGwJtKH&GckwR{afO_KJhP5XV1ED^ZOLL_ z^8&&IdV+@B&%(Rsspr~4>Ey4Po{%ceAt}#coYhV};XbFneGc$osHfnZM~a2ARqxgi zVJgwyquSL{60?cbLsUL_?^s!$x3#sw76%0w;G85l6F)Z>Y4p~J;#|`L8)MLhtx8(= z6~PZ%OgtR`a(7m$74`$Cl3sgR00cQ3k`z00Q1z-NV`Z>WCOYUx|2v!!J=#eG@&ZPV zp80uj*fCEutI(z?4QDJB2FdnCk4b})JUD_fgShe3aUb}ZgYE7kXpr+6fyjl)Om3(r z^_Z+1fR5E`dFA$JBG-TE7OHKfHbTs^Z8N&>+AI!b$BX!cZ`j&t~9_$zwV+M$Jbn zwD<%}1Wrn-v2wj$;EXtMf;VK>I9O5$eEZ4#j+fEh2>^XPGG!Y~{&5>6 zcxrOasTpcBE{HihBfSoYod;NP^V=F`i z-kG`Jzb2V>|~@f0IkrcS11zp;)_TKlb-4( zZ?<)qyLN*Do0hSf*b4bRgq|H9(D|X(s;k4?y&DK>UAHESRfIx%|7a*_>sJ1e)yvrY z8cRwrielP1rF8(g#9N+0w>O_QaU4spv1?_+miBVzNQ)XB0zfnD#pcW{_CR}H6goCC zQDMw9cF^pBF?34JG*f+bn5Uhk@c`(}dtezAL#`;Jx|J(?+6%ddOd&){0?iv)H#UoK zZWwssWqpqj-DA|aJ?h4U9UAH}4Obkim4RuGt+0ppT@+u73vpgE2IL<-F%2#xN-WmJfXvi&dO)hC6rjbou`L4u0NFZvnh=s zwf6s_EHwy$thekhIYWfe`_t=&hbkVrzIA!!3ZijY9k{xxEe*?D@ zviE}I~|) zxG9*QO)m&fMS=0Mtd>>EX?Tgd|A1s9rJz5!PX97ASDSJ`Qq;dV-VqjNNTHzxad>5% zv(zj-6XIR8Ue<`c?@+@Az~KblMcl?U#~RIVD9eQ}k+Z#j#!AhZ;FbF0p5PDo2kxeM z30Y@>W+$2u`bx~hbUy*QkuFnNT4(zuDz02#7$zzA$kwwnLU6(_agvWAJ;q7J-vWpK zm<)F7$!V7p}_)LU9kI~V!#H!CQdaLWXJCW9PspGKKN&LKH?I59Mi6Z9Lq)Lw! z{SueXBj&8RRSECZVq%PK*W)=INl}{T!QR>*g%{%eqHx-sgk2!b^3IpVZKhrPS9avR zLzsXT_spE{5ANjzdov!`t00p)H&C+lQ6F#i>G&g!x&qnEHyNbhqWc;Wr(qr0O_sr` zC-l1iobSMOqN3LVjj(;^CIU6LY)sbSd!5M&-=d*}-cJ_Vh4eCa)^;*%)eHUgJ4^K8 z@`*m|-CyoLSD$G>NuJcbK?LE4XJ&kxxt9a9rjD`#up}Mdv)vi6apedcJKTS!%hlvk zn@3@8$6v@!#*r>a-upIgZXX5Mx_=anx5VCSop}o1J$X@k>U+)TW8*>TIJWv{|D?nz zOH8eunBNVs^Whzs#~0+~NG$G|sQUg_l6Qxn566}Ww`{}h%t=Rq)7xw2wzKq~jE7Ej z!QA@$v)7qrMM;;+(@D7$2Q%gZWV`k#bNh+lj`VvW3%CbWNK!uSJPhR~!*@AbOK|1Wk|0sd}rK z5aI&2HC&BPZ#13o*jz~9^#SUw_l)m96n&77yJ-gN1bx5N!>}WRelW_X693tr$?~(M zPh&%;>tK+&$rx;v0ymaQj(=%+=(}+x1qQ3Xdb9A`1KrI#g~IT&Hlj3&T`f*(W}v*N z(d)QK^==Sbl1{AK_4Z9oo$wSJwUH?L!OYNBOPP<9fG4dJDYnF>r9YYe!ZD1kjYxk%9?yN)*D082!Zz_G7-)3KxJ)&M7pBGqUxe*S=>_UrIkj^KQr$3L?W);8 zZ1?wmvXs@g(_*?FNJG+C!A-;YmT6N`_JltiSefy~LzV|9w>qZ~10&)oQIZ|!c0EYF zMT8Ak<8Moc2&B)&Fh?HND%IUnB1Tj7$@>c83eN;a@|@+ihHt*zy189t7;tVJ&T?3``hSv=hU5l&>)g|= z$vT}n1UT7L7u=My;Fp@;9&qOAb@IaK816X$3llci+>YfV(@TpF98 zJmuB5R{{ewl`nG-$boQLJ549$d8>xlHxJnJD9GurzSV4=_{!VUXQfK2;l(8;?7nxe z-J5ZFRqrgM9m%SlJhV-ji=FLIRHQrj;gZ}Jy=Lg5!_!%Mr~YO&uMp~EK= ziK5@!kS^9&e9vW76b(PDthbi8^fNuh7(G9PG{{=LZK#+H%PUOKp1h$|C!94?egZIK zPE2Vv{gnK&&ej^iUxGBfhBb@Op)VngqpwA4bVC!26PLx3rJBlNqpo|K_WcI!DAgkN z8AY%4nLk_kD1urL7DUG;F1M#*T-H-u@@mkAznkseSl@+xm0;&ClTE3548NjW%X*?# z%LnQJHh9JijyTw1y0gn4K9w~QPnWmx3bh{CuoS|M7QPZIzN+5Nlk@IQj$)?1@xX9J z_JEg-+C{KH?MiwuT7FVO?bxjxM#HkEuL_!OkFB{y6>p{Y#aidyb)Y1i>CsYZuo(n` z_V3o_`x|>_DE}&2Dca!zBw01R>zns-Cn{T&2;n~yGvklt6KBk72b}vhD0)ibRy%z) RUU0(-wA8Mu;xAb~`akN}F=YS% literal 0 HcmV?d00001 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..05b2062 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,10 @@ +const raycastConfig = require("@raycast/eslint-config"); + +module.exports = [ + ...raycastConfig.flat(), + { + rules: { + "@raycast/prefer-title-case": "off", + }, + }, +]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7518a57 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3019 @@ +{ + "name": "velum", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "velum", + "license": "MIT", + "dependencies": { + "@raycast/api": "^1.104.17", + "@raycast/utils": "^2.2.5", + "marked": "^18.0.3" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.0.4", + "@types/node": "^22.15.3", + "@types/react": "^19.1.2", + "eslint": "^9.26.0", + "prettier": "^3.5.3", + "typescript": "^5.8.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@oclif/core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.11.1.tgz", + "integrity": "sha512-+N5yqeoOKPnT0p+ZJiNutMILsZukZrEpsVup24XERla594EkGSWS9tiCqRfvzr1xfvf/AhM9pb0yPaf8L3Y9Uw==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "ansis": "^3.17.0", + "clean-stack": "^3.0.1", + "cli-spinners": "^2.9.2", + "debug": "^4.4.3", + "ejs": "^3.1.10", + "get-package-type": "^0.1.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "lilconfig": "^3.1.3", + "minimatch": "^10.2.5", + "semver": "^7.7.3", + "string-width": "^4.2.3", + "supports-color": "^8", + "tinyglobby": "^0.2.14", + "widest-line": "^3.1.0", + "wordwrap": "^1.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.47.tgz", + "integrity": "sha512-Rm8xvKj18T4MBbFdd5HLCQNqojYOcOvwtZxWZ7FE8mPFMjPgj1WKDHeDm8RyWGS3PxV6mdRPUKE5DXsZK9nPoA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4", + "ansis": "^3.16.0", + "debug": "^4.4.1", + "ejs": "^3.1.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "6.2.46", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.46.tgz", + "integrity": "sha512-KmuMFt/fURCVxor0rrRjEqs2nLN0Y3ixcixo/M5VjKcN920gbuw5T+AF23FBeyUDuW/Dg79YPcTWy/Rtz0Dg/A==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-not-found": { + "version": "3.2.82", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.82.tgz", + "integrity": "sha512-6heNFE2gadcDYijWy4XJc6ZLzPd1qKe0i8sb8uyrR3mX0o5IFA+5KSAx/BFBkGS8j/tKOsCYvvmMKVdReeb1Gg==", + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.10.1", + "@oclif/core": "^4.11.0", + "ansis": "^3.17.0", + "fast-levenshtein": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@raycast/api": { + "version": "1.104.17", + "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.104.17.tgz", + "integrity": "sha512-31yGTbb0HMHKWqxwZZl2rzaUau/Yf+FX6pO1DK6mobpjUMqDb0h0FGkDSwr0MNwsujT4K3DbAYgh3sHhGkxWfw==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4.8.4", + "@oclif/plugin-autocomplete": "^3.2.40", + "@oclif/plugin-help": "^6.2.37", + "@oclif/plugin-not-found": "^3.2.74", + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "esbuild": "^0.27.3", + "react": "19.0.0" + }, + "bin": { + "ray": "bin/run.js" + }, + "engines": { + "node": ">=22.22.2" + }, + "peerDependencies": { + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "react-devtools": "6.1.1" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react-devtools": { + "optional": true + } + } + }, + "node_modules/@raycast/api/node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@raycast/eslint-config": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@raycast/eslint-config/-/eslint-config-2.1.1.tgz", + "integrity": "sha512-W0kxF+FJ+BYQn0EKIV739j2ZrHEtjo/LclsoZgUWg3t364Dq75XKcjqYFYx+59/DBaamY0amdajlfuDAf6veAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/js": "^9.36.0", + "@raycast/eslint-plugin": "^2.1.1", + "eslint-config-prettier": "^10.1.8", + "globals": "^16.4.0", + "typescript-eslint": "^8.45.0" + }, + "peerDependencies": { + "eslint": ">=8.23.0", + "prettier": ">=2", + "typescript": ">=4" + } + }, + "node_modules/@raycast/eslint-plugin": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@raycast/eslint-plugin/-/eslint-plugin-2.1.1.tgz", + "integrity": "sha512-r2gs8uIlNp6I2mLOyN/kReGlvigzEeuyQPl4yw7nwLy8Zxjfjhg8txMViaBux8juBWBxbSWq/IfW6ZA50oeOHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.26.1" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/@raycast/utils": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@raycast/utils/-/utils-2.2.5.tgz", + "integrity": "sha512-1o6zGRECh1KNe6CBx68iMPOrNYbV56opqs4zUtDr6Wmq4F7Ww3d8uLTXKVzXlGyJmpAys/Eliw6cKIP7dPdFfw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "@raycast/api": ">=1.99.4", + "react": ">=19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/type-utils": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/marked": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz", + "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", + "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.2", + "@typescript-eslint/parser": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..40c1eb0 --- /dev/null +++ b/package.json @@ -0,0 +1,253 @@ +{ + "$schema": "https://www.raycast.com/schemas/extension.json", + "name": "velum", + "title": "Velum", + "description": "Text per Velum-API pseudonymisieren und wiederherstellen.", + "icon": "extension-icon.png", + "author": "raphael", + "license": "MIT", + "categories": [ + "Productivity", + "Developer Tools" + ], + "commands": [ + { + "name": "summarize-email", + "title": "Email-Konversation zusammenfassen", + "description": "Markierte Email pseudonymisieren, per Raycast-KI zusammenfassen und wiederherstellen.", + "mode": "view" + }, + { + "name": "reply-email", + "title": "Email-Antwort generieren", + "description": "Antwort auf eine markierte Email/einen Mailverlauf per Raycast-KI verfassen — pseudonymisiert.", + "mode": "view" + }, + { + "name": "briefing-from-notes", + "title": "Briefing aus Notizen", + "description": "Aus Notizen, Stichpunkten oder einem Transkript ein strukturiertes Briefing per Raycast-KI erstellen — pseudonymisiert.", + "mode": "view" + }, + { + "name": "extract-action-items", + "title": "Action Items extrahieren", + "description": "Action Items aus einem Transkript, Thread oder Notizen als Markdown-Tabelle extrahieren — pseudonymisiert.", + "mode": "view" + }, + { + "name": "extract-structured-data", + "title": "Strukturierte Daten extrahieren", + "description": "Strukturierte Daten (JSON oder Tabelle) aus Freitext gemäß einem Schema extrahieren — pseudonymisiert.", + "mode": "view" + }, + { + "name": "pseudonymize-text", + "title": "Text pseudonymisieren", + "description": "Eingegebenen, markierten oder Zwischenablage-Text pseudonymisieren.", + "mode": "view" + }, + { + "name": "pseudonymize-selected-text", + "title": "Markierten Text pseudonymisieren", + "description": "Aktuell markierten Text pseudonymisieren und das Ergebnis kopieren oder einfügen.", + "mode": "no-view" + }, + { + "name": "pseudonymize-clipboard", + "title": "Zwischenablage pseudonymisieren", + "description": "Aktuellen Zwischenablage-Text pseudonymisieren und das Ergebnis kopieren oder einfügen.", + "mode": "no-view" + }, + { + "name": "depseudonymize-text", + "title": "Text wiederherstellen", + "description": "Platzhalter mit einer gespeicherten Velum-Zuordnung wiederherstellen.", + "mode": "view" + }, + { + "name": "depseudonymize-selected-text", + "title": "Markierten Text wiederherstellen", + "description": "Aktuell markierten Text mit der aktiven Sitzung wiederherstellen und das Ergebnis kopieren oder einfügen.", + "mode": "no-view" + }, + { + "name": "depseudonymize-clipboard", + "title": "Zwischenablage wiederherstellen", + "description": "Aktuellen Zwischenablage-Text mit der aktiven Sitzung wiederherstellen und das Ergebnis kopieren oder einfügen.", + "mode": "no-view" + }, + { + "name": "manage-sessions", + "title": "Sitzungen verwalten", + "description": "Velum-Sitzungen mit Zuordnungen anlegen, aktivieren, ansehen und löschen.", + "mode": "view" + } + ], + "preferences": [ + { + "name": "velumBaseUrl", + "title": "Velum Basis-URL", + "description": "Basis-URL der Velum-Installation, z. B. https://velum.example.com.", + "type": "textfield", + "required": true, + "placeholder": "https://velum.example.com" + }, + { + "name": "authentikTokenUrl", + "title": "Authentik Token-URL", + "description": "OAuth2-Token-Endpunkt, üblicherweise https://auth.example.com/application/o/token/.", + "type": "textfield", + "required": true, + "placeholder": "https://auth.example.com/application/o/token/" + }, + { + "name": "clientId", + "title": "OAuth Client-ID", + "description": "Client-ID des Authentik-Providers, der Tokens für Velum ausstellt.", + "type": "textfield", + "required": true + }, + { + "name": "serviceAccountUsername", + "title": "Dienstkonto-Benutzername", + "description": "Benutzername des Authentik-Dienstkontos, z. B. svc-velum-raycast.", + "type": "textfield", + "required": true + }, + { + "name": "serviceAccountPassword", + "title": "Dienstkonto App-Passwort", + "description": "App-Passwort/Token des Authentik-Dienstkontos.", + "type": "password", + "required": true + }, + { + "name": "scope", + "title": "OAuth Scope", + "description": "Scopes, die von Authentik angefordert werden.", + "type": "textfield", + "required": false, + "default": "profile" + }, + { + "name": "sessionMode", + "title": "Standard-Sitzungsmodus", + "description": "Bestimmt, wie Schnellbefehle eine Zuordnungs-Sitzung wählen.", + "type": "dropdown", + "required": true, + "default": "reuse-active", + "data": [ + { + "title": "Aktive Sitzung wiederverwenden", + "value": "reuse-active" + }, + { + "title": "Neue Sitzung pro Anfrage", + "value": "new-each-request" + }, + { + "title": "Tagessitzung", + "value": "daily" + } + ] + }, + { + "name": "quickOutput", + "title": "Ausgabe der Schnellbefehle", + "description": "Was die Befehle für markierten Text und Zwischenablage mit dem Ergebnis machen.", + "type": "dropdown", + "required": true, + "default": "copy", + "data": [ + { + "title": "In die Zwischenablage kopieren", + "value": "copy" + }, + { + "title": "Am Cursor einfügen", + "value": "paste" + } + ] + }, + { + "name": "userFullName", + "title": "Eigener Name", + "description": "Dein Name in der Signatur generierter Email-Antworten (z. B. „Raphael“). Wird im Antwort-Befehl vorbefüllt und kann pro Aufruf überschrieben werden.", + "type": "textfield", + "required": true, + "placeholder": "Raphael" + }, + { + "name": "summaryModel", + "title": "Standard-Modell für Zusammenfassungen", + "description": "Raycast-KI-Modell für „Email-Konversation zusammenfassen“. Benötigt Raycast Pro.", + "type": "dropdown", + "required": true, + "default": "anthropic-claude-sonnet-4-6", + "data": [ + { + "title": "Claude 4.6 Sonnet", + "value": "anthropic-claude-sonnet-4-6" + }, + { + "title": "Claude 4.7 Opus", + "value": "anthropic-claude-opus-4-7" + }, + { + "title": "Claude 4.5 Haiku", + "value": "anthropic-claude-4-5-haiku" + }, + { + "title": "OpenAI GPT-5.3 Instant", + "value": "openai-gpt-5.3-instant" + }, + { + "title": "OpenAI GPT-4.1", + "value": "openai-gpt-4.1" + }, + { + "title": "OpenAI GPT-4o mini", + "value": "openai-gpt-4o-mini" + } + ] + }, + { + "name": "maxSessions", + "title": "Maximale Anzahl gespeicherter Sitzungen", + "description": "Älteste Sitzungen werden entfernt, wenn dieses Limit überschritten wird.", + "type": "textfield", + "required": true, + "default": "20" + }, + { + "name": "closeAfterAction", + "title": "Verhalten nach Abschluss", + "label": "Raycast nach Kopieren/Einfügen schließen", + "description": "Schließt das Raycast-Fenster und kehrt zum Root-Search zurück, sobald in einem AI-Workflow Kopieren oder Einfügen ausgelöst wurde.", + "type": "checkbox", + "required": false, + "default": true + } + ], + "scripts": { + "build": "ray build -e dist", + "dev": "ray develop", + "fix-lint": "ray lint --fix", + "lint": "ray lint", + "publish": "ray publish" + }, + "dependencies": { + "@raycast/api": "^1.104.17", + "@raycast/utils": "^2.2.5", + "marked": "^18.0.3" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.0.4", + "@types/node": "^22.15.3", + "@types/react": "^19.1.2", + "eslint": "^9.26.0", + "prettier": "^3.5.3", + "typescript": "^5.8.3" + } +} diff --git a/src/ai-views.tsx b/src/ai-views.tsx new file mode 100644 index 0000000..2c5d030 --- /dev/null +++ b/src/ai-views.tsx @@ -0,0 +1,341 @@ +import { + Action, + ActionPanel, + AI, + Clipboard, + closeMainWindow, + Detail, + Form, + Icon, + PopToRootType, + showHUD, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { marked } from "marked"; +import { type ReactNode, useEffect, useRef, useState } from "react"; +import type { Creativity } from "./ai"; +import { getPreferences } from "./preferences"; +import type { VelumSession } from "./types"; +import { markdownCodeBlock, mappingDetailTable, sessionSubtitle } from "./ui"; +import { localDepseudonymize } from "./velum"; + +export async function maybeCloseRaycast(): Promise { + if (getPreferences().closeAfterAction) { + await closeMainWindow({ popToRootType: PopToRootType.Immediate }); + } +} + +const execFileAsync = promisify(execFile); + +// Raycast 2.0 Beta's Clipboard.copy with { html, text } only writes plain text +// to the system pasteboard. We bypass it via osascript / JXA, which sets both +// the HTML and plain-text pasteboard types directly on NSPasteboard. Content +// is passed via environment variables to avoid shell-quoting hazards. +const RICH_COPY_SCRIPT = [ + "ObjC.import('AppKit');", + "const env = $.NSProcessInfo.processInfo.environment;", + "const html = env.objectForKey('VELUM_HTML');", + "const text = env.objectForKey('VELUM_TEXT');", + "const pb = $.NSPasteboard.generalPasteboard;", + "pb.clearContents;", + "pb.setStringForType(html, 'public.html');", + "pb.setStringForType(text, 'public.utf8-plain-text');", +].join("\n"); + +export async function copyRichText(html: string, text: string): Promise { + await execFileAsync( + "osascript", + ["-l", "JavaScript", "-e", RICH_COPY_SCRIPT], + { + env: { ...process.env, VELUM_HTML: html, VELUM_TEXT: text }, + }, + ); +} + +export type AiCommandLabels = { + pageHeading: string; + runningLabel: string; + doneLabel?: string; + failureTitle: string; + copyTitle: string; + pasteTitle: string; +}; + +export type PseudoStageProps = { + session: VelumSession; + pseudonymizedText: string; + model: string; + creativity: Creativity; + instructions: string; + buildPrompt: (pseudonymizedText: string, instructions: string) => string; + labels: AiCommandLabels; + extraResultActions?: (restored: string) => ReactNode; +}; + +export function PseudonymizationConfirm(props: PseudoStageProps) { + const [editedText, setEditedText] = useState(props.pseudonymizedText); + const [editedInstructions, setEditedInstructions] = useState( + props.instructions, + ); + const { push } = useNavigation(); + + async function handleSubmit() { + if (!editedText.trim()) { + await showToast({ + style: Toast.Style.Failure, + title: "Pseudonymisierter Text ist leer", + }); + return; + } + push( + , + ); + } + + return ( +
+ + + + } + > + + + + + ); +} + +type Phase = "running" | "restoring" | "done" | "error"; + +export function AiStreamResult(props: PseudoStageProps) { + const { + session, + pseudonymizedText, + model, + creativity, + instructions, + buildPrompt, + labels, + extraResultActions, + } = props; + const [aiBuffer, setAiBuffer] = useState(""); + const [restored, setRestored] = useState(null); + const [replacementsMade, setReplacementsMade] = useState(0); + const [phase, setPhase] = useState("running"); + const [error, setError] = useState(null); + const controllerRef = useRef(null); + + useEffect(() => { + const controller = new AbortController(); + controllerRef.current = controller; + let cancelled = false; + + async function run() { + const toast = await showToast({ + style: Toast.Style.Animated, + title: labels.runningLabel, + }); + + try { + const prompt = buildPrompt(pseudonymizedText, instructions); + const stream = AI.ask(prompt, { + model: model as AI.Model, + creativity, + signal: controller.signal, + }); + stream.on("data", (chunk) => { + if (cancelled) return; + setAiBuffer((prev) => prev + chunk); + }); + const aiText = await stream; + if (cancelled) return; + + toast.title = "Stelle wieder her…"; + setPhase("restoring"); + + const restoreResult = localDepseudonymize(aiText, session.mapping); + if (cancelled) return; + + setRestored(restoreResult.original_text); + setReplacementsMade(restoreResult.replacements_made); + setPhase("done"); + + toast.style = Toast.Style.Success; + toast.title = labels.doneLabel ?? "Fertig"; + toast.message = `${restoreResult.replacements_made} Ersetzungen`; + } catch (err) { + if (cancelled || controller.signal.aborted) return; + const message = err instanceof Error ? err.message : String(err); + setError(message); + setPhase("error"); + toast.style = Toast.Style.Failure; + toast.title = labels.failureTitle; + toast.message = message; + } + } + + run(); + + return () => { + cancelled = true; + controller.abort(); + }; + }, [ + pseudonymizedText, + model, + creativity, + instructions, + session.mapping, + buildPrompt, + labels.runningLabel, + labels.doneLabel, + labels.failureTitle, + ]); + + const phaseLabel = + phase === "running" + ? labels.runningLabel + : phase === "restoring" + ? "Platzhalter werden ersetzt …" + : phase === "error" + ? "Fehler" + : (labels.doneLabel ?? "Fertig"); + + const body = restored ?? aiBuffer; + + const markdown = [ + `# ${labels.pageHeading} — ${session.name}`, + sessionSubtitle(session), + `*Status:* ${phaseLabel}`, + "", + body.trim() ? body : "_Noch keine Inhalte — warte auf das Modell …_", + "", + restored ? `*${replacementsMade} Platzhalter ersetzt.*` : "", + "", + "## Pseudonymisierte Eingabe", + markdownCodeBlock(pseudonymizedText), + "", + "## Pseudonymisierte KI-Ausgabe", + aiBuffer.trim() + ? markdownCodeBlock(aiBuffer) + : "_Noch nichts vom Modell empfangen._", + "", + "## Zuordnung", + mappingDetailTable(session.mapping), + error ? `\n> Fehler: ${error}` : "", + ].join("\n"); + + const isLoading = phase === "running" || phase === "restoring"; + + return ( + + {restored ? ( + <> + {extraResultActions ? extraResultActions(restored) : null} + { + try { + await copyRichText(markdownToHtml(restored), restored); + await showHUD("Als Rich Text kopiert"); + } catch { + await Clipboard.copy(restored); + await showHUD("Kopiert (Plain Text Fallback)"); + } + await closeMainWindow({ + popToRootType: PopToRootType.Immediate, + }); + }} + /> + { + await Clipboard.copy(restored); + await showHUD("Als Markdown kopiert"); + await closeMainWindow({ + popToRootType: PopToRootType.Immediate, + }); + }} + /> + { + await Clipboard.paste({ + html: markdownToHtml(restored), + text: restored, + }); + await closeMainWindow({ + popToRootType: PopToRootType.Immediate, + }); + }} + /> + + ) : null} + {aiBuffer ? ( + + ) : null} + + + } + /> + ); +} + +function stripOuterFence(text: string): string { + const trimmed = text.trim(); + const match = trimmed.match(/^```[a-zA-Z0-9-]*\s*\n([\s\S]*?)\n```\s*$/); + return match ? match[1].trim() : trimmed; +} + +export function markdownToHtml(markdownText: string): string { + return marked.parse(stripOuterFence(markdownText), { + async: false, + gfm: true, + breaks: false, + }) as string; +} diff --git a/src/ai.ts b/src/ai.ts new file mode 100644 index 0000000..d7e2b9d --- /dev/null +++ b/src/ai.ts @@ -0,0 +1,22 @@ +export type Creativity = "none" | "low" | "medium" | "high"; + +export const CREATIVITY_OPTIONS: Creativity[] = [ + "none", + "low", + "medium", + "high", +]; + +export const MODEL_OPTIONS: Array<{ value: string; title: string }> = [ + { value: "anthropic-claude-sonnet-4-6", title: "Claude 4.6 Sonnet" }, + { value: "anthropic-claude-opus-4-7", title: "Claude 4.7 Opus" }, + { value: "anthropic-claude-4-5-haiku", title: "Claude 4.5 Haiku" }, + { value: "openai-gpt-5.3-instant", title: "OpenAI GPT-5.3 Instant" }, + { value: "openai-gpt-4.1", title: "OpenAI GPT-4.1" }, + { value: "openai-gpt-4o-mini", title: "OpenAI GPT-4o mini" }, +]; + +export const STRICT_PLACEHOLDER_RULE = [ + "STRENGE REGEL: Gib jeden Platzhalter zeichengetreu (inklusive spitzer Klammern, Großbuchstaben und Unterstrich + Nummer) zurück.", + "Du darfst Platzhalter NIEMALS auflösen, raten, übersetzen oder mit erfundenen Namen ersetzen. Schreibe sie exakt so, wie sie in der Eingabe stehen.", +].join("\n"); diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..a3df684 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,133 @@ +import { LocalStorage } from "@raycast/api"; +import { getPreferences } from "./preferences"; + +const TOKEN_STORAGE_KEY = "velum.auth.token.v1"; +const TOKEN_EXPIRY_SKEW_MS = 30_000; + +type StoredToken = { + accessToken: string; + tokenType: string; + expiresAt: number; + authKey: string; +}; + +type TokenResponse = { + access_token: string; + token_type?: string; + expires_in?: number; +}; + +function currentAuthKey(): string { + const preferences = getPreferences(); + return [ + preferences.authentikTokenUrl, + preferences.clientId, + preferences.serviceAccountUsername, + ].join("|"); +} + +async function getStoredToken(): Promise { + const raw = await LocalStorage.getItem(TOKEN_STORAGE_KEY); + if (!raw) { + return undefined; + } + + try { + const token = JSON.parse(raw) as StoredToken; + if (token.authKey !== currentAuthKey()) { + return undefined; + } + if ( + !token.accessToken || + Date.now() + TOKEN_EXPIRY_SKEW_MS >= token.expiresAt + ) { + return undefined; + } + return token; + } catch { + await clearAccessToken(); + return undefined; + } +} + +export async function clearAccessToken(): Promise { + await LocalStorage.removeItem(TOKEN_STORAGE_KEY); +} + +export async function getAccessToken(): Promise { + const stored = await getStoredToken(); + if (stored) { + return stored.accessToken; + } + + return refreshAccessToken(); +} + +export async function refreshAccessToken(): Promise { + const preferences = getPreferences(); + const body = new URLSearchParams({ + grant_type: "client_credentials", + client_id: preferences.clientId, + username: preferences.serviceAccountUsername, + password: preferences.serviceAccountPassword, + scope: preferences.scope, + }); + + const response = await fetch(preferences.authentikTokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body, + }); + + if (!response.ok) { + const details = await response.text().catch(() => response.statusText); + throw new Error( + `Authentik token request failed: ${response.status} ${response.statusText}: ${details}`, + ); + } + + const token = (await response.json()) as TokenResponse; + if (!token.access_token) { + throw new Error("Authentik token response did not include access_token."); + } + + const stored: StoredToken = { + accessToken: token.access_token, + tokenType: token.token_type || "Bearer", + expiresAt: Date.now() + Math.max(30, token.expires_in ?? 300) * 1000, + authKey: currentAuthKey(), + }; + + await LocalStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(stored)); + return stored.accessToken; +} + +export async function fetchWithAuth( + input: string, + init: RequestInit = {}, + retry = true, +): Promise { + const token = await getAccessToken(); + const headers = new Headers(init.headers); + headers.set("Authorization", `Bearer ${token}`); + + const response = await fetch(input, { + ...init, + headers, + }); + + if ((response.status === 401 || response.status === 403) && retry) { + await clearAccessToken(); + const freshToken = await refreshAccessToken(); + const retryHeaders = new Headers(init.headers); + retryHeaders.set("Authorization", `Bearer ${freshToken}`); + return fetch(input, { + ...init, + headers: retryHeaders, + }); + } + + return response; +} diff --git a/src/briefing-from-notes.tsx b/src/briefing-from-notes.tsx new file mode 100644 index 0000000..80dd3d4 --- /dev/null +++ b/src/briefing-from-notes.tsx @@ -0,0 +1,297 @@ +import { + Action, + ActionPanel, + Clipboard, + Form, + Icon, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { useEffect, useMemo, useState } from "react"; +import { + type Creativity, + CREATIVITY_OPTIONS, + MODEL_OPTIONS, + STRICT_PLACEHOLDER_RULE, +} from "./ai"; +import { PseudonymizationConfirm } from "./ai-views"; +import { getPreferences } from "./preferences"; +import { getSelectedTextSafely } from "./selection"; +import { + createSession, + getActiveSessionId, + getSession, + listSessions, + setActiveSession, + updateSessionMapping, +} from "./sessions"; +import type { EntityType, VelumSession } from "./types"; +import { NEW_SESSION_ID } from "./ui"; +import { + getEntityTypes, + normalizePseudonymizeResponse, + pseudonymize, +} from "./velum"; + +function buildBriefingPrompt( + pseudonymizedText: string, + instructions: string, +): string { + const extra = instructions.trim() + ? `\n\nZusätzliche Anweisungen des Nutzers:\n${instructions.trim()}` + : ""; + + return [ + "Du erhältst pseudonymisierten Text (Stichpunkte, Notizen, Transkript, Whiteboard-Mitschrift).", + "Personenbezogene Daten wurden durch Platzhalter wie , , ersetzt.", + "", + STRICT_PLACEHOLDER_RULE, + "", + "Aufgabe: Erstelle ein strukturiertes deutschsprachiges Briefing in Markdown mit folgenden Abschnitten:", + "- **Kontext**: Worum geht es? Kurzer Absatz.", + "- **Teilnehmer**: Beteiligte Platzhalter mit Rolle (falls aus dem Text ableitbar).", + "- **Entscheidungen**: Welche Entscheidungen wurden getroffen? Stichpunktliste, jeweils 1 Zeile.", + "- **Action Items**: Was ist zu tun? Stichpunktliste mit Verantwortlichem (Platzhalter) und Deadline (falls genannt).", + "- **Offene Punkte**: Was bleibt zu klären? Stichpunktliste.", + "", + 'Wenn ein Abschnitt nicht im Text vorkommt, schreibe „—" als einzigen Inhalt darunter.', + "Antworte ausschließlich in Markdown, ohne einleitende Floskeln und ohne Code-Fences (keine ``` Zeilen am Anfang oder Ende).", + extra, + "", + "--- Notizen (pseudonymisiert) ---", + pseudonymizedText, + "--- Ende ---", + ].join("\n"); +} + +type FormValues = { + text: string; + sessionId: string; + entityTypes: string[]; + model: string; + creativity: Creativity; + instructions: string; +}; + +export default function Command() { + const preferences = getPreferences(); + const [text, setText] = useState(""); + const [sessions, setSessions] = useState([]); + const [activeSessionId, setActiveSessionId] = useState(); + const [entityTypes, setEntityTypes] = useState([]); + const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { push } = useNavigation(); + + useEffect(() => { + async function load() { + const [loadedSessions, activeId] = await Promise.all([ + listSessions(), + getActiveSessionId(), + ]); + setSessions(loadedSessions); + setActiveSessionId(activeId); + + const selection = await getSelectedTextSafely(); + if (selection && selection.trim()) { + setText(selection); + } + + try { + const loadedEntityTypes = await getEntityTypes(); + setEntityTypes(loadedEntityTypes); + setSelectedEntityTypes(loadedEntityTypes); + } catch { + setEntityTypes([]); + } finally { + setIsLoading(false); + } + } + load(); + }, []); + + const defaultSessionId = useMemo( + () => activeSessionId || sessions[0]?.id || NEW_SESSION_ID, + [activeSessionId, sessions], + ); + + async function loadSelectedText() { + const selected = await getSelectedTextSafely(); + if (!selected) { + await showToast({ + style: Toast.Style.Failure, + title: "Kein markierter Text gefunden", + }); + return; + } + setText(selected); + } + + async function loadClipboardText() { + const clipboardText = await Clipboard.readText(); + if (!clipboardText) { + await showToast({ + style: Toast.Style.Failure, + title: "Zwischenablage enthält keinen Text", + }); + return; + } + setText(clipboardText); + } + + async function handleSubmit(values: FormValues) { + const input = values.text.trim(); + if (!input) { + await showToast({ + style: Toast.Style.Failure, + title: "Notizen eingeben", + }); + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Pseudonymisiere…", + }); + + try { + const session = + values.sessionId === NEW_SESSION_ID + ? await createSession() + : ((await getSession(values.sessionId)) ?? (await createSession())); + await setActiveSession(session.id); + + const rawResult = await pseudonymize( + input, + session.mapping, + values.entityTypes, + ); + const pseudoResult = normalizePseudonymizeResponse(rawResult); + const updatedSession = await updateSessionMapping( + session.id, + pseudoResult.mapping, + pseudoResult.selected_entity_types, + ); + + toast.style = Toast.Style.Success; + toast.title = "Pseudonymisiert"; + toast.message = `${pseudoResult.entity_count} Zuordnungen`; + + push( + , + ); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Pseudonymisierung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } + } + + return ( +
+ + + + + } + > + + + + {sessions.map((session) => ( + + ))} + + {entityTypes.length > 0 ? ( + + {entityTypes.map((entityType) => ( + + ))} + + ) : null} + + {MODEL_OPTIONS.map((option) => ( + + ))} + + + {CREATIVITY_OPTIONS.map((value) => ( + + ))} + + + + ); +} diff --git a/src/depseudonymize-clipboard.ts b/src/depseudonymize-clipboard.ts new file mode 100644 index 0000000..d779224 --- /dev/null +++ b/src/depseudonymize-clipboard.ts @@ -0,0 +1,7 @@ +import { Clipboard } from "@raycast/api"; +import { depseudonymizeQuickText } from "./quick"; + +export default async function Command() { + const clipboardText = await Clipboard.readText(); + await depseudonymizeQuickText(clipboardText ?? "", "Zwischenablage"); +} diff --git a/src/depseudonymize-selected-text.ts b/src/depseudonymize-selected-text.ts new file mode 100644 index 0000000..4a9143f --- /dev/null +++ b/src/depseudonymize-selected-text.ts @@ -0,0 +1,15 @@ +import { showToast, Toast } from "@raycast/api"; +import { depseudonymizeQuickText } from "./quick"; +import { getSelectedTextSafely } from "./selection"; + +export default async function Command() { + const selectedText = await getSelectedTextSafely(); + if (!selectedText) { + await showToast({ + style: Toast.Style.Failure, + title: "Kein markierter Text gefunden", + }); + return; + } + await depseudonymizeQuickText(selectedText, "Selektion"); +} diff --git a/src/depseudonymize-text.tsx b/src/depseudonymize-text.tsx new file mode 100644 index 0000000..bfb969b --- /dev/null +++ b/src/depseudonymize-text.tsx @@ -0,0 +1,176 @@ +import { + Action, + ActionPanel, + Clipboard, + Detail, + Form, + Icon, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { useEffect, useMemo, useState } from "react"; +import { depseudonymize } from "./velum"; +import { getActiveSessionId, getSession, listSessions } from "./sessions"; +import type { DepseudonymizeResponse, VelumSession } from "./types"; +import { markdownCodeBlock, sessionSubtitle } from "./ui"; + +type FormValues = { + text: string; + sessionId: string; +}; + +export default function Command() { + const [text, setText] = useState(""); + const [sessions, setSessions] = useState([]); + const [activeSessionId, setActiveSessionId] = useState(); + const [isLoading, setIsLoading] = useState(true); + const { push } = useNavigation(); + + useEffect(() => { + async function load() { + const [loadedSessions, activeId] = await Promise.all([ + listSessions(), + getActiveSessionId(), + ]); + setSessions(loadedSessions); + setActiveSessionId(activeId); + setIsLoading(false); + } + load(); + }, []); + + const defaultSessionId = useMemo( + () => activeSessionId || sessions[0]?.id, + [activeSessionId, sessions], + ); + + async function loadClipboardText() { + const clipboardText = await Clipboard.readText(); + if (!clipboardText) { + await showToast({ + style: Toast.Style.Failure, + title: "Zwischenablage enthält keinen Text", + }); + return; + } + setText(clipboardText); + } + + async function handleSubmit(values: FormValues) { + const input = values.text.trim(); + if (!input || !values.sessionId) { + await showToast({ + style: Toast.Style.Failure, + title: "Sitzung wählen und Text eingeben", + }); + return; + } + + const session = await getSession(values.sessionId); + if (!session) { + await showToast({ + style: Toast.Style.Failure, + title: "Sitzung nicht gefunden", + }); + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Stelle wieder her…", + }); + try { + const result = await depseudonymize(input, session.mapping); + toast.style = Toast.Style.Success; + toast.title = "Wiederhergestellt"; + toast.message = `${result.replacements_made} Ersetzungen`; + push(); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Wiederherstellung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } + } + + return ( +
+ + + + } + > + + + {sessions.map((session) => ( + + ))} + + + ); +} + +function DepseudonymizeResult({ + result, + session, +}: { + result: DepseudonymizeResponse; + session: VelumSession; +}) { + const markdown = [ + `# ${session.name}`, + sessionSubtitle(session), + "", + "## Wiederhergestellter Text", + markdownCodeBlock(result.original_text), + "", + `Ersetzungen: ${result.replacements_made}`, + ].join("\n"); + + return ( + + + Clipboard.paste(result.original_text)} + /> + + } + /> + ); +} diff --git a/src/extract-action-items.tsx b/src/extract-action-items.tsx new file mode 100644 index 0000000..bdc094c --- /dev/null +++ b/src/extract-action-items.tsx @@ -0,0 +1,302 @@ +import { + Action, + ActionPanel, + Clipboard, + Form, + Icon, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { useEffect, useMemo, useState } from "react"; +import { + type Creativity, + CREATIVITY_OPTIONS, + MODEL_OPTIONS, + STRICT_PLACEHOLDER_RULE, +} from "./ai"; +import { PseudonymizationConfirm } from "./ai-views"; +import { getPreferences } from "./preferences"; +import { getSelectedTextSafely } from "./selection"; +import { + createSession, + getActiveSessionId, + getSession, + listSessions, + setActiveSession, + updateSessionMapping, +} from "./sessions"; +import type { EntityType, VelumSession } from "./types"; +import { NEW_SESSION_ID } from "./ui"; +import { + getEntityTypes, + normalizePseudonymizeResponse, + pseudonymize, +} from "./velum"; + +function buildActionItemsPrompt( + pseudonymizedText: string, + instructions: string, +): string { + const extra = instructions.trim() + ? `\n\nZusätzliche Anweisungen des Nutzers:\n${instructions.trim()}` + : ""; + + return [ + "Du erhältst pseudonymisierten Text (Meeting-Notizen, Email-Thread, Transkript).", + "Personenbezogene Daten wurden durch Platzhalter wie , , ersetzt.", + "", + STRICT_PLACEHOLDER_RULE, + "", + "Aufgabe: Extrahiere alle Action Items aus dem Text als deutschsprachige Markdown-Tabelle.", + "", + "Format:", + "| Aufgabe | Verantwortlich | Deadline | Status |", + "| --- | --- | --- | --- |", + "", + "Regeln:", + "- Liste nur konkret formulierte Aufgaben/Zusagen, keine vagen Absichten.", + '- Verantwortliche werden als Platzhalter referenziert (z. B. ); wenn unklar, schreibe „—".', + '- Deadline: Datum oder Zeitraum aus dem Text. Wenn nichts genannt, „—".', + '- Status: „Offen", „In Arbeit" oder „Erledigt" — wenn nicht erkennbar, „Offen".', + '- Wenn der Text keine Action Items enthält, antworte mit Kopfzeile, Trennzeile und einer Zeile „| Keine Action Items | — | — | — |".', + "", + "Antworte ausschließlich mit der Tabelle, ohne einleitende Floskeln und ohne Code-Fences (keine ``` Zeilen).", + extra, + "", + "--- Text (pseudonymisiert) ---", + pseudonymizedText, + "--- Ende ---", + ].join("\n"); +} + +type FormValues = { + text: string; + sessionId: string; + entityTypes: string[]; + model: string; + creativity: Creativity; + instructions: string; +}; + +export default function Command() { + const preferences = getPreferences(); + const [text, setText] = useState(""); + const [sessions, setSessions] = useState([]); + const [activeSessionId, setActiveSessionId] = useState(); + const [entityTypes, setEntityTypes] = useState([]); + const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { push } = useNavigation(); + + useEffect(() => { + async function load() { + const [loadedSessions, activeId] = await Promise.all([ + listSessions(), + getActiveSessionId(), + ]); + setSessions(loadedSessions); + setActiveSessionId(activeId); + + const selection = await getSelectedTextSafely(); + if (selection && selection.trim()) { + setText(selection); + } + + try { + const loadedEntityTypes = await getEntityTypes(); + setEntityTypes(loadedEntityTypes); + setSelectedEntityTypes(loadedEntityTypes); + } catch { + setEntityTypes([]); + } finally { + setIsLoading(false); + } + } + load(); + }, []); + + const defaultSessionId = useMemo( + () => activeSessionId || sessions[0]?.id || NEW_SESSION_ID, + [activeSessionId, sessions], + ); + + async function loadSelectedText() { + const selected = await getSelectedTextSafely(); + if (!selected) { + await showToast({ + style: Toast.Style.Failure, + title: "Kein markierter Text gefunden", + }); + return; + } + setText(selected); + } + + async function loadClipboardText() { + const clipboardText = await Clipboard.readText(); + if (!clipboardText) { + await showToast({ + style: Toast.Style.Failure, + title: "Zwischenablage enthält keinen Text", + }); + return; + } + setText(clipboardText); + } + + async function handleSubmit(values: FormValues) { + const input = values.text.trim(); + if (!input) { + await showToast({ + style: Toast.Style.Failure, + title: "Text zum Extrahieren eingeben", + }); + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Pseudonymisiere…", + }); + + try { + const session = + values.sessionId === NEW_SESSION_ID + ? await createSession() + : ((await getSession(values.sessionId)) ?? (await createSession())); + await setActiveSession(session.id); + + const rawResult = await pseudonymize( + input, + session.mapping, + values.entityTypes, + ); + const pseudoResult = normalizePseudonymizeResponse(rawResult); + const updatedSession = await updateSessionMapping( + session.id, + pseudoResult.mapping, + pseudoResult.selected_entity_types, + ); + + toast.style = Toast.Style.Success; + toast.title = "Pseudonymisiert"; + toast.message = `${pseudoResult.entity_count} Zuordnungen`; + + push( + , + ); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Pseudonymisierung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } + } + + return ( +
+ + + + + } + > + + + + {sessions.map((session) => ( + + ))} + + {entityTypes.length > 0 ? ( + + {entityTypes.map((entityType) => ( + + ))} + + ) : null} + + {MODEL_OPTIONS.map((option) => ( + + ))} + + + {CREATIVITY_OPTIONS.map((value) => ( + + ))} + + + + ); +} diff --git a/src/extract-structured-data.tsx b/src/extract-structured-data.tsx new file mode 100644 index 0000000..b910a87 --- /dev/null +++ b/src/extract-structured-data.tsx @@ -0,0 +1,362 @@ +import { + Action, + ActionPanel, + Clipboard, + Form, + Icon, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { useEffect, useMemo, useState } from "react"; +import { + type Creativity, + CREATIVITY_OPTIONS, + MODEL_OPTIONS, + STRICT_PLACEHOLDER_RULE, +} from "./ai"; +import { PseudonymizationConfirm } from "./ai-views"; +import { getPreferences } from "./preferences"; +import { getSelectedTextSafely } from "./selection"; +import { + createSession, + getActiveSessionId, + getSession, + listSessions, + setActiveSession, + updateSessionMapping, +} from "./sessions"; +import type { EntityType, VelumSession } from "./types"; +import { NEW_SESSION_ID } from "./ui"; +import { + getEntityTypes, + normalizePseudonymizeResponse, + pseudonymize, +} from "./velum"; + +type OutputFormat = "json" | "markdown-table"; + +const FORMAT_OPTIONS: Array<{ value: OutputFormat; title: string }> = [ + { value: "json", title: "JSON" }, + { value: "markdown-table", title: "Markdown-Tabelle" }, +]; + +function buildStructuredDataPrompt(args: { + pseudonymizedText: string; + instructions: string; + schema: string; + format: OutputFormat; +}): string { + const extra = args.instructions.trim() + ? `\n\nZusätzliche Anweisungen des Nutzers:\n${args.instructions.trim()}` + : ""; + + const formatInstructions = + args.format === "json" + ? [ + "Format: gib genau einen Code-Block zurück, der valides JSON enthält.", + "Verwende für nicht gefundene Felder den Wert null. Bei Listen: leeres Array `[]`.", + "Antworte mit dem Code-Block und nichts anderem.", + ].join("\n") + : [ + "Format: gib genau eine Markdown-Tabelle zurück. Eine Zeile pro Datensatz.", + 'Verwende für nicht gefundene Felder „—".', + "Antworte mit der Tabelle und nichts anderem.", + ].join("\n"); + + return [ + "Du erhältst pseudonymisierten Text und ein Schema. Personenbezogene Daten wurden durch Platzhalter wie , , ersetzt.", + "", + STRICT_PLACEHOLDER_RULE, + "", + "Aufgabe: Extrahiere die im Schema beschriebenen Daten aus dem Text.", + "", + "Regeln:", + "- Verwende ausschließlich Informationen, die im Text vorkommen. Erfinde nichts.", + "- Personen, Organisationen etc. erscheinen als Platzhalter (z. B. ) — übernimm sie zeichengetreu.", + "- Keine einleitenden Floskeln, keine Erklärungen — nur das angeforderte Ausgabeformat.", + "", + formatInstructions, + extra, + "", + "--- Schema ---", + args.schema.trim(), + "--- Ende Schema ---", + "", + "--- Text (pseudonymisiert) ---", + args.pseudonymizedText, + "--- Ende Text ---", + ].join("\n"); +} + +type FormValues = { + text: string; + schema: string; + format: OutputFormat; + sessionId: string; + entityTypes: string[]; + model: string; + creativity: Creativity; + instructions: string; +}; + +export default function Command() { + const preferences = getPreferences(); + const [text, setText] = useState(""); + const [sessions, setSessions] = useState([]); + const [activeSessionId, setActiveSessionId] = useState(); + const [entityTypes, setEntityTypes] = useState([]); + const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { push } = useNavigation(); + + useEffect(() => { + async function load() { + const [loadedSessions, activeId] = await Promise.all([ + listSessions(), + getActiveSessionId(), + ]); + setSessions(loadedSessions); + setActiveSessionId(activeId); + + const selection = await getSelectedTextSafely(); + if (selection && selection.trim()) { + setText(selection); + } + + try { + const loadedEntityTypes = await getEntityTypes(); + setEntityTypes(loadedEntityTypes); + setSelectedEntityTypes(loadedEntityTypes); + } catch { + setEntityTypes([]); + } finally { + setIsLoading(false); + } + } + load(); + }, []); + + const defaultSessionId = useMemo( + () => activeSessionId || sessions[0]?.id || NEW_SESSION_ID, + [activeSessionId, sessions], + ); + + async function loadSelectedText() { + const selected = await getSelectedTextSafely(); + if (!selected) { + await showToast({ + style: Toast.Style.Failure, + title: "Kein markierter Text gefunden", + }); + return; + } + setText(selected); + } + + async function loadClipboardText() { + const clipboardText = await Clipboard.readText(); + if (!clipboardText) { + await showToast({ + style: Toast.Style.Failure, + title: "Zwischenablage enthält keinen Text", + }); + return; + } + setText(clipboardText); + } + + async function handleSubmit(values: FormValues) { + const input = values.text.trim(); + if (!input) { + await showToast({ + style: Toast.Style.Failure, + title: "Text zum Extrahieren eingeben", + }); + return; + } + if (!values.schema.trim()) { + await showToast({ + style: Toast.Style.Failure, + title: "Schema beschreiben", + }); + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Pseudonymisiere…", + }); + + try { + const session = + values.sessionId === NEW_SESSION_ID + ? await createSession() + : ((await getSession(values.sessionId)) ?? (await createSession())); + await setActiveSession(session.id); + + const rawResult = await pseudonymize( + input, + session.mapping, + values.entityTypes, + ); + const pseudoResult = normalizePseudonymizeResponse(rawResult); + const updatedSession = await updateSessionMapping( + session.id, + pseudoResult.mapping, + pseudoResult.selected_entity_types, + ); + + toast.style = Toast.Style.Success; + toast.title = "Pseudonymisiert"; + toast.message = `${pseudoResult.entity_count} Zuordnungen`; + + const schema = values.schema; + const format = values.format; + const buildPrompt = (pseudonymizedText: string, instructions: string) => + buildStructuredDataPrompt({ + pseudonymizedText, + instructions, + schema, + format, + }); + + push( + , + ); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Pseudonymisierung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } + } + + return ( +
+ + + + + } + > + + + + {FORMAT_OPTIONS.map((option) => ( + + ))} + + + + {sessions.map((session) => ( + + ))} + + {entityTypes.length > 0 ? ( + + {entityTypes.map((entityType) => ( + + ))} + + ) : null} + + {MODEL_OPTIONS.map((option) => ( + + ))} + + + {CREATIVITY_OPTIONS.map((value) => ( + + ))} + + + + ); +} diff --git a/src/manage-sessions.tsx b/src/manage-sessions.tsx new file mode 100644 index 0000000..12630b6 --- /dev/null +++ b/src/manage-sessions.tsx @@ -0,0 +1,260 @@ +import { + Action, + ActionPanel, + Alert, + confirmAlert, + Detail, + Form, + Icon, + List, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { useEffect, useState } from "react"; +import { + clearSessionMapping, + countMappingEntries, + createSession, + deleteAllSessions, + deleteSession, + getActiveSessionId, + listSessions, + renameSession, + setActiveSession, +} from "./sessions"; +import type { VelumSession } from "./types"; +import { mappingSummary, sessionSubtitle } from "./ui"; + +export default function Command() { + const [sessions, setSessions] = useState([]); + const [activeSessionId, setActiveSessionId] = useState(); + const [isLoading, setIsLoading] = useState(true); + + async function reload() { + const [loadedSessions, activeId] = await Promise.all([ + listSessions(), + getActiveSessionId(), + ]); + setSessions(loadedSessions); + setActiveSessionId(activeId); + setIsLoading(false); + } + + useEffect(() => { + reload(); + }, []); + + async function activate(session: VelumSession) { + await setActiveSession(session.id); + await reload(); + await showToast({ + style: Toast.Style.Success, + title: "Aktive Sitzung geändert", + message: session.name, + }); + } + + async function clearMapping(session: VelumSession) { + const confirmed = await confirmAlert({ + title: "Zuordnung leeren?", + message: `${session.name} behält den Namen, verliert aber alle Platzhalter-Zuordnungen.`, + primaryAction: { + title: "Zuordnung leeren", + style: Alert.ActionStyle.Destructive, + }, + }); + if (!confirmed) { + return; + } + + await clearSessionMapping(session.id); + await reload(); + await showToast({ + style: Toast.Style.Success, + title: "Zuordnung geleert", + message: session.name, + }); + } + + async function remove(session: VelumSession) { + const confirmed = await confirmAlert({ + title: "Sitzung löschen?", + message: `${session.name} und ihre Zuordnung werden gelöscht.`, + primaryAction: { + title: "Sitzung löschen", + style: Alert.ActionStyle.Destructive, + }, + }); + if (!confirmed) { + return; + } + + await deleteSession(session.id); + await reload(); + await showToast({ + style: Toast.Style.Success, + title: "Sitzung gelöscht", + message: session.name, + }); + } + + async function removeAll() { + const confirmed = await confirmAlert({ + title: "Alle Sitzungen löschen?", + message: + "Alle gespeicherten Velum-Zuordnungen dieser Raycast-Extension werden gelöscht.", + primaryAction: { + title: "Alle löschen", + style: Alert.ActionStyle.Destructive, + }, + }); + if (!confirmed) { + return; + } + + await deleteAllSessions(); + await reload(); + await showToast({ + style: Toast.Style.Success, + title: "Alle Sitzungen gelöscht", + }); + } + + return ( + + + } + /> + + } + /> + {sessions.map((session) => ( + + activate(session)} + /> + } + /> + } + /> + } + /> + clearMapping(session)} + /> + remove(session)} + /> + + + } + /> + ))} + + ); +} + +function SessionNameForm({ + session, + onSaved, +}: { + session?: VelumSession; + onSaved: () => Promise; +}) { + const { pop } = useNavigation(); + + async function handleSubmit(values: { name: string }) { + if (session) { + await renameSession(session.id, values.name); + await showToast({ + style: Toast.Style.Success, + title: "Sitzung umbenannt", + }); + } else { + await createSession(values.name.trim() || undefined); + await showToast({ + style: Toast.Style.Success, + title: "Sitzung erstellt", + }); + } + + await onSaved(); + pop(); + } + + return ( +
+ + + } + > + + + ); +} + +function SessionDetail({ session }: { session: VelumSession }) { + const markdown = [ + `# ${session.name}`, + sessionSubtitle(session), + "", + `Erstellt: ${new Date(session.createdAt).toLocaleString()}`, + "", + "## Zuordnung", + mappingSummary(session.mapping), + "", + "## Platzhalter", + ...Object.entries(session.mapping) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([placeholder, entry]) => `- \`${placeholder}\` · ${entry.type}`), + ].join("\n"); + + return ; +} diff --git a/src/preferences.ts b/src/preferences.ts new file mode 100644 index 0000000..b00a99e --- /dev/null +++ b/src/preferences.ts @@ -0,0 +1,46 @@ +import { getPreferenceValues } from "@raycast/api"; +import type { ExtensionPreferences, QuickOutput, SessionMode } from "./types"; + +export type NormalizedPreferences = { + velumBaseUrl: string; + authentikTokenUrl: string; + clientId: string; + serviceAccountUsername: string; + serviceAccountPassword: string; + scope: string; + sessionMode: SessionMode; + quickOutput: QuickOutput; + summaryModel: string; + userFullName: string; + maxSessions: number; + closeAfterAction: boolean; +}; + +function trimTrailingSlash(value: string): string { + return value.trim().replace(/\/+$/, ""); +} + +function parsePositiveInteger(value: string, fallback: number): number { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +export function getPreferences(): NormalizedPreferences { + const preferences = getPreferenceValues(); + + return { + velumBaseUrl: trimTrailingSlash(preferences.velumBaseUrl), + authentikTokenUrl: preferences.authentikTokenUrl.trim(), + clientId: preferences.clientId.trim(), + serviceAccountUsername: preferences.serviceAccountUsername.trim(), + serviceAccountPassword: preferences.serviceAccountPassword, + scope: preferences.scope?.trim() || "profile", + sessionMode: preferences.sessionMode, + quickOutput: preferences.quickOutput, + summaryModel: + preferences.summaryModel?.trim() || "anthropic-claude-sonnet-4-6", + userFullName: preferences.userFullName?.trim() ?? "", + maxSessions: parsePositiveInteger(preferences.maxSessions, 20), + closeAfterAction: preferences.closeAfterAction ?? true, + }; +} diff --git a/src/pseudonymize-clipboard.ts b/src/pseudonymize-clipboard.ts new file mode 100644 index 0000000..649f49c --- /dev/null +++ b/src/pseudonymize-clipboard.ts @@ -0,0 +1,7 @@ +import { Clipboard } from "@raycast/api"; +import { pseudonymizeQuickText } from "./quick"; + +export default async function Command() { + const clipboardText = await Clipboard.readText(); + await pseudonymizeQuickText(clipboardText ?? "", "Zwischenablage"); +} diff --git a/src/pseudonymize-selected-text.ts b/src/pseudonymize-selected-text.ts new file mode 100644 index 0000000..a433828 --- /dev/null +++ b/src/pseudonymize-selected-text.ts @@ -0,0 +1,15 @@ +import { showToast, Toast } from "@raycast/api"; +import { pseudonymizeQuickText } from "./quick"; +import { getSelectedTextSafely } from "./selection"; + +export default async function Command() { + const selectedText = await getSelectedTextSafely(); + if (!selectedText) { + await showToast({ + style: Toast.Style.Failure, + title: "Kein markierter Text gefunden", + }); + return; + } + await pseudonymizeQuickText(selectedText, "Selektion"); +} diff --git a/src/pseudonymize-text.tsx b/src/pseudonymize-text.tsx new file mode 100644 index 0000000..6b7ab04 --- /dev/null +++ b/src/pseudonymize-text.tsx @@ -0,0 +1,248 @@ +import { + Action, + ActionPanel, + Clipboard, + Detail, + Form, + Icon, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { getSelectedTextSafely } from "./selection"; +import { useEffect, useMemo, useState } from "react"; +import { + createSession, + getActiveSessionId, + getSession, + listSessions, + setActiveSession, + updateSessionMapping, +} from "./sessions"; +import type { EntityType, PseudonymizeResponse, VelumSession } from "./types"; +import { + markdownCodeBlock, + mappingSummary, + NEW_SESSION_ID, + sessionSubtitle, +} from "./ui"; +import { getEntityTypes, pseudonymize } from "./velum"; + +type FormValues = { + text: string; + sessionId: string; + entityTypes: string[]; +}; + +export default function Command() { + const [text, setText] = useState(""); + const [sessions, setSessions] = useState([]); + const [activeSessionId, setActiveSessionId] = useState(); + const [entityTypes, setEntityTypes] = useState([]); + const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { push } = useNavigation(); + + useEffect(() => { + async function load() { + const [loadedSessions, activeId] = await Promise.all([ + listSessions(), + getActiveSessionId(), + ]); + setSessions(loadedSessions); + setActiveSessionId(activeId); + + try { + const loadedEntityTypes = await getEntityTypes(); + setEntityTypes(loadedEntityTypes); + setSelectedEntityTypes(loadedEntityTypes); + } catch { + setEntityTypes([]); + } finally { + setIsLoading(false); + } + } + load(); + }, []); + + const defaultSessionId = useMemo( + () => activeSessionId || sessions[0]?.id || NEW_SESSION_ID, + [activeSessionId, sessions], + ); + + async function loadSelectedText() { + const selected = await getSelectedTextSafely(); + if (!selected) { + await showToast({ + style: Toast.Style.Failure, + title: "Kein markierter Text gefunden", + }); + return; + } + setText(selected); + } + + async function loadClipboardText() { + const clipboardText = await Clipboard.readText(); + if (!clipboardText) { + await showToast({ + style: Toast.Style.Failure, + title: "Zwischenablage enthält keinen Text", + }); + return; + } + setText(clipboardText); + } + + async function handleSubmit(values: FormValues) { + const input = values.text.trim(); + if (!input) { + await showToast({ + style: Toast.Style.Failure, + title: "Text zum Pseudonymisieren eingeben", + }); + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Pseudonymisiere…", + }); + + try { + const session = + values.sessionId === NEW_SESSION_ID + ? await createSession() + : ((await getSession(values.sessionId)) ?? (await createSession())); + await setActiveSession(session.id); + const result = await pseudonymize( + input, + session.mapping, + values.entityTypes, + ); + const updatedSession = await updateSessionMapping( + session.id, + result.mapping, + result.selected_entity_types, + ); + + toast.style = Toast.Style.Success; + toast.title = "Pseudonymisiert"; + toast.message = `${result.entity_count} Zuordnungen in ${updatedSession.name}`; + + push(); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Pseudonymisierung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } + } + + return ( +
+ + + + + } + > + + + + {sessions.map((session) => ( + + ))} + + {entityTypes.length > 0 ? ( + + {entityTypes.map((entityType) => ( + + ))} + + ) : null} + + ); +} + +function PseudonymizeResult({ + result, + session, +}: { + result: PseudonymizeResponse; + session: VelumSession; +}) { + const markdown = [ + `# ${session.name}`, + sessionSubtitle(session), + "", + "## Pseudonymisierter Text", + markdownCodeBlock(result.pseudonymized_text), + "", + "## Zuordnung", + mappingSummary(result.mapping), + ].join("\n"); + + return ( + + + Clipboard.paste(result.pseudonymized_text)} + /> + + } + /> + ); +} diff --git a/src/quick.ts b/src/quick.ts new file mode 100644 index 0000000..16c4083 --- /dev/null +++ b/src/quick.ts @@ -0,0 +1,121 @@ +import { Clipboard, showHUD, showToast, Toast } from "@raycast/api"; +import { getPreferences } from "./preferences"; +import { + getActiveSession, + resolveDefaultSession, + updateSessionMapping, +} from "./sessions"; +import type { VelumSession } from "./types"; +import { localDepseudonymize, pseudonymize } from "./velum"; + +export async function pseudonymizeQuickText( + text: string, + source: string, +): Promise { + const trimmed = text.trim(); + if (!trimmed) { + await showToast({ + style: Toast.Style.Failure, + title: `Kein Text in ${source} gefunden`, + }); + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Pseudonymisiere…", + }); + + try { + const session = await resolveDefaultSession(); + const result = await pseudonymize( + trimmed, + session.mapping, + session.entityTypes, + ); + const updatedSession = await updateSessionMapping( + session.id, + result.mapping, + result.selected_entity_types, + ); + await writeQuickOutput( + result.pseudonymized_text, + updatedSession, + "Pseudonymisierten Text", + ); + + toast.style = Toast.Style.Success; + toast.title = "Pseudonymisiert"; + toast.message = `${result.entity_count} Zuordnungen in ${updatedSession.name}`; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Pseudonymisierung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } +} + +export async function depseudonymizeQuickText( + text: string, + source: string, +): Promise { + const trimmed = text.trim(); + if (!trimmed) { + await showToast({ + style: Toast.Style.Failure, + title: `Kein Text in ${source} gefunden`, + }); + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Stelle wieder her…", + }); + + try { + const session = await getActiveSession(); + if (Object.keys(session.mapping).length === 0) { + toast.style = Toast.Style.Failure; + toast.title = "Aktive Sitzung hat keine Zuordnungen"; + toast.message = session.name; + return; + } + + const result = localDepseudonymize(trimmed, session.mapping); + if (result.replacements_made === 0) { + toast.style = Toast.Style.Failure; + toast.title = "Keine Platzhalter ersetzt"; + toast.message = `Sitzung: ${session.name}`; + return; + } + + await writeQuickOutput( + result.original_text, + session, + "Wiederhergestellten Text", + ); + + toast.style = Toast.Style.Success; + toast.title = "Wiederhergestellt"; + toast.message = `${result.replacements_made} Ersetzungen in ${session.name}`; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Wiederherstellung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } +} + +async function writeQuickOutput( + text: string, + session: VelumSession, + textKind: string, +): Promise { + if (getPreferences().quickOutput === "paste") { + await Clipboard.paste(text); + await showHUD(`${textKind} eingefügt (${session.name})`); + return; + } + + await Clipboard.copy(text); + await showHUD(`${textKind} kopiert (${session.name})`); +} diff --git a/src/reply-email.tsx b/src/reply-email.tsx new file mode 100644 index 0000000..5f4ec74 --- /dev/null +++ b/src/reply-email.tsx @@ -0,0 +1,783 @@ +import { + Action, + ActionPanel, + AI, + Clipboard, + Detail, + Form, + Icon, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { getSelectedTextSafely } from "./selection"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { getPreferences } from "./preferences"; +import { + buildDisclosureContent, + buildReplyPrompt, + composeReplyEmail, + type DisclosureContent, + GREETING_OPTIONS, + loadReplyDefaults, + plainTextToHtmlEmail, + saveReplyDefaults, + SIGN_OFF_OPTIONS, +} from "./reply"; +import { + createSession, + getActiveSessionId, + getSession, + listSessions, + setActiveSession, + updateSessionMapping, +} from "./sessions"; +import type { Creativity } from "./summarize"; +import type { EntityType, VelumSession } from "./types"; +import { + markdownCodeBlock, + mappingDetailTable, + NEW_SESSION_ID, + sessionSubtitle, +} from "./ui"; +import { + getEntityTypes, + localDepseudonymize, + normalizePseudonymizeResponse, + pseudonymize, +} from "./velum"; + +type FormValues = { + text: string; + sessionId: string; + entityTypes: string[]; + instructions: string; +}; + +const CREATIVITY_OPTIONS: Creativity[] = ["none", "low", "medium", "high"]; + +const MODEL_OPTIONS: Array<{ value: string; title: string }> = [ + { value: "anthropic-claude-sonnet-4-6", title: "Claude 4.6 Sonnet" }, + { value: "anthropic-claude-opus-4-7", title: "Claude 4.7 Opus" }, + { value: "anthropic-claude-4-5-haiku", title: "Claude 4.5 Haiku" }, + { value: "openai-gpt-5.3-instant", title: "OpenAI GPT-5.3 Instant" }, + { value: "openai-gpt-4.1", title: "OpenAI GPT-4.1" }, + { value: "openai-gpt-4o-mini", title: "OpenAI GPT-4o mini" }, +]; + +export default function Command() { + const preferences = getPreferences(); + const [text, setText] = useState(""); + const [sessions, setSessions] = useState([]); + const [activeSessionId, setActiveSessionId] = useState(); + const [entityTypes, setEntityTypes] = useState([]); + const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [greeting, setGreeting] = useState("Lieber"); + const [signOff, setSignOff] = useState("Alles Liebe,"); + const [userFullName, setUserFullName] = useState(preferences.userFullName); + const [model, setModel] = useState(preferences.summaryModel); + const [creativity, setCreativity] = useState("medium"); + const [disclosure, setDisclosure] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const { push } = useNavigation(); + + useEffect(() => { + async function load() { + const [loadedSessions, activeId, defaults] = await Promise.all([ + listSessions(), + getActiveSessionId(), + loadReplyDefaults(), + ]); + setSessions(loadedSessions); + setActiveSessionId(activeId); + + if (defaults.greeting) setGreeting(defaults.greeting); + if (defaults.signOff) setSignOff(defaults.signOff); + if (defaults.model) setModel(defaults.model); + if (defaults.creativity) setCreativity(defaults.creativity as Creativity); + if (typeof defaults.disclosure === "boolean") + setDisclosure(defaults.disclosure); + + const selection = await getSelectedTextSafely(); + if (selection && selection.trim()) { + setText(selection); + } + + try { + const loadedEntityTypes = await getEntityTypes(); + setEntityTypes(loadedEntityTypes); + setSelectedEntityTypes(loadedEntityTypes); + } catch { + setEntityTypes([]); + } finally { + setIsLoading(false); + } + } + load(); + }, [preferences.summaryModel, preferences.userFullName]); + + function persist( + overrides: Partial<{ + greeting: string; + signOff: string; + model: string; + creativity: Creativity; + disclosure: boolean; + }>, + ) { + saveReplyDefaults({ + greeting: overrides.greeting ?? greeting, + signOff: overrides.signOff ?? signOff, + model: overrides.model ?? model, + creativity: overrides.creativity ?? creativity, + disclosure: overrides.disclosure ?? disclosure, + }).catch(() => { + // Best-effort persistence. + }); + } + + const defaultSessionId = useMemo( + () => activeSessionId || sessions[0]?.id || NEW_SESSION_ID, + [activeSessionId, sessions], + ); + + async function loadSelectedText() { + const selected = await getSelectedTextSafely(); + if (!selected) { + await showToast({ + style: Toast.Style.Failure, + title: "Kein markierter Text gefunden", + }); + return; + } + setText(selected); + } + + async function loadClipboardText() { + const clipboardText = await Clipboard.readText(); + if (!clipboardText) { + await showToast({ + style: Toast.Style.Failure, + title: "Zwischenablage enthält keinen Text", + }); + return; + } + setText(clipboardText); + } + + async function handleSubmit(values: FormValues) { + const input = values.text.trim(); + if (!input) { + await showToast({ + style: Toast.Style.Failure, + title: "Email zum Beantworten eingeben", + }); + return; + } + if (!userFullName.trim()) { + await showToast({ + style: Toast.Style.Failure, + title: "Eigener Name fehlt", + message: "Trage deinen Namen für die Signatur ein.", + }); + return; + } + + await saveReplyDefaults({ + greeting, + signOff, + model, + creativity, + disclosure, + }); + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Pseudonymisiere…", + }); + + try { + const session = + values.sessionId === NEW_SESSION_ID + ? await createSession() + : ((await getSession(values.sessionId)) ?? (await createSession())); + await setActiveSession(session.id); + + const rawResult = await pseudonymize( + input, + session.mapping, + values.entityTypes, + ); + const pseudoResult = normalizePseudonymizeResponse(rawResult); + const updatedSession = await updateSessionMapping( + session.id, + pseudoResult.mapping, + pseudoResult.selected_entity_types, + ); + + toast.style = Toast.Style.Success; + toast.title = "Pseudonymisiert"; + toast.message = `${pseudoResult.entity_count} Zuordnungen`; + + push( + , + ); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Pseudonymisierung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } + } + + return ( +
+ + + + + } + > + + { + setGreeting(v); + persist({ greeting: v }); + }} + > + {GREETING_OPTIONS.map((option) => ( + + ))} + + { + setSignOff(v); + persist({ signOff: v }); + }} + > + {SIGN_OFF_OPTIONS.map((option) => ( + + ))} + + + + { + setDisclosure(v); + persist({ disclosure: v }); + }} + info="Listet am Ende der Email auf, welche Originalbegriffe vor dem KI-Aufruf lokal durch Platzhalter ersetzt wurden." + /> + + + + {sessions.map((session) => ( + + ))} + + {entityTypes.length > 0 ? ( + + {entityTypes.map((entityType) => ( + + ))} + + ) : null} + { + setModel(v); + persist({ model: v }); + }} + > + {MODEL_OPTIONS.map((option) => ( + + ))} + + { + const next = v as Creativity; + setCreativity(next); + persist({ creativity: next }); + }} + > + {CREATIVITY_OPTIONS.map((value) => ( + + ))} + + + ); +} + +type StageProps = { + session: VelumSession; + pseudonymizedText: string; + model: string; + creativity: Creativity; + greeting: string; + signOff: string; + userFullName: string; + instructions: string; + disclosure: boolean; +}; + +function ConfirmReply(props: StageProps) { + const [editedText, setEditedText] = useState(props.pseudonymizedText); + const [editedInstructions, setEditedInstructions] = useState( + props.instructions, + ); + const [greeting, setGreeting] = useState(props.greeting); + const [signOff, setSignOff] = useState(props.signOff); + const [userFullName, setUserFullName] = useState(props.userFullName); + const [disclosure, setDisclosure] = useState(props.disclosure); + const { push } = useNavigation(); + + async function handleSubmit() { + if (!editedText.trim()) { + await showToast({ + style: Toast.Style.Failure, + title: "Pseudonymisierter Text ist leer", + }); + return; + } + if (!userFullName.trim()) { + await showToast({ + style: Toast.Style.Failure, + title: "Eigener Name fehlt", + message: "Trage deinen Namen für die Signatur ein.", + }); + return; + } + + await saveReplyDefaults({ + greeting, + signOff, + model: props.model, + creativity: props.creativity, + disclosure, + }); + + push( + , + ); + } + + return ( +
+ + + + } + > + + + { + setGreeting(v); + saveReplyDefaults({ + greeting: v, + signOff, + model: props.model, + creativity: props.creativity, + disclosure, + }).catch(() => {}); + }} + > + {GREETING_OPTIONS.map((option) => ( + + ))} + + { + setSignOff(v); + saveReplyDefaults({ + greeting, + signOff: v, + model: props.model, + creativity: props.creativity, + disclosure, + }).catch(() => {}); + }} + > + {SIGN_OFF_OPTIONS.map((option) => ( + + ))} + + + + { + setDisclosure(v); + saveReplyDefaults({ + greeting, + signOff, + model: props.model, + creativity: props.creativity, + disclosure: v, + }).catch(() => {}); + }} + /> + + + + ); +} + +function mappingDescriptionText(session: VelumSession): string { + const entries = Object.entries(session.mapping).sort(([a], [b]) => + a.localeCompare(b), + ); + if (entries.length === 0) { + return "Keine Einträge — nichts wurde pseudonymisiert."; + } + return entries + .map(([placeholder, entry]) => `${placeholder} → ${entry.original}`) + .join("\n"); +} + +type Phase = "drafting" | "restoring" | "done" | "error"; + +function ReplyResult(props: StageProps) { + const [aiBody, setAiBody] = useState(""); + const [restoredEmail, setRestoredEmail] = useState(null); + const [disclosureContent, setDisclosureContent] = + useState(null); + const [phase, setPhase] = useState("drafting"); + const [error, setError] = useState(null); + const controllerRef = useRef(null); + + useEffect(() => { + const controller = new AbortController(); + controllerRef.current = controller; + let cancelled = false; + + async function run() { + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Verfasse Antwort…", + }); + + try { + const prompt = buildReplyPrompt({ + pseudonymizedText: props.pseudonymizedText, + greeting: props.greeting, + instructions: props.instructions, + }); + const stream = AI.ask(prompt, { + model: props.model as AI.Model, + creativity: props.creativity, + signal: controller.signal, + }); + stream.on("data", (chunk) => { + if (cancelled) return; + setAiBody((prev) => prev + chunk); + }); + const aiText = await stream; + if (cancelled) return; + + toast.title = "Stelle wieder her…"; + setPhase("restoring"); + + const composed = composeReplyEmail({ + body: aiText, + signOff: props.signOff, + userFullName: props.userFullName, + }); + + const restoreResult = localDepseudonymize( + composed, + props.session.mapping, + ); + if (cancelled) return; + + const disclosure = props.disclosure + ? buildDisclosureContent( + props.pseudonymizedText, + props.session.mapping, + ) + : null; + + setRestoredEmail(restoreResult.original_text); + setDisclosureContent(disclosure); + setPhase("done"); + + toast.style = Toast.Style.Success; + toast.title = "Fertig"; + toast.message = `${restoreResult.replacements_made} Ersetzungen`; + } catch (err) { + if (cancelled || controller.signal.aborted) return; + const message = err instanceof Error ? err.message : String(err); + setError(message); + setPhase("error"); + toast.style = Toast.Style.Failure; + toast.title = "Antwort fehlgeschlagen"; + toast.message = message; + } + } + + run(); + + return () => { + cancelled = true; + controller.abort(); + }; + }, [ + props.pseudonymizedText, + props.model, + props.creativity, + props.instructions, + props.session.mapping, + props.greeting, + props.signOff, + props.userFullName, + props.disclosure, + ]); + + const phaseLabel = + phase === "drafting" + ? "KI verfasst Antwort …" + : phase === "restoring" + ? "Platzhalter werden ersetzt …" + : phase === "error" + ? "Fehler" + : "Fertig"; + + const finalEmailText = restoredEmail + ? disclosureContent + ? `${restoredEmail.trimEnd()}\n\n${disclosureContent.text}` + : restoredEmail + : null; + + const previewBody = + finalEmailText ?? + composeReplyEmail({ + body: aiBody || "_Noch keine Inhalte — warte auf das Modell …_", + signOff: props.signOff, + userFullName: props.userFullName, + }); + + const finalEmailHtml = restoredEmail + ? disclosureContent + ? `${plainTextToHtmlEmail(restoredEmail)}\n${disclosureContent.html}` + : plainTextToHtmlEmail(restoredEmail) + : null; + + const markdown = [ + "# Email-Antwort", + sessionSubtitle(props.session), + `*Status:* ${phaseLabel}`, + "", + "## Generierte Antwort (zum Versenden)", + markdownCodeBlock(previewBody), + "", + "---", + "", + "## Pseudonymisierte Eingabe", + markdownCodeBlock(props.pseudonymizedText), + "", + "## Pseudonymisierter KI-Body", + aiBody.trim() + ? markdownCodeBlock(aiBody) + : "_Noch nichts vom Modell empfangen._", + "", + "## Zuordnung", + mappingDetailTable(props.session.mapping), + error ? `\n> Fehler: ${error}` : "", + ].join("\n"); + + const isLoading = phase === "drafting" || phase === "restoring"; + + return ( + + {finalEmailText && finalEmailHtml ? ( + <> + + + Clipboard.paste({ + html: finalEmailHtml, + text: finalEmailText, + }) + } + /> + + + ) : null} + {aiBody ? ( + + ) : null} + + + } + /> + ); +} diff --git a/src/reply.ts b/src/reply.ts new file mode 100644 index 0000000..00f2c19 --- /dev/null +++ b/src/reply.ts @@ -0,0 +1,304 @@ +import { LocalStorage } from "@raycast/api"; +import type { PlaceholderMapping } from "./types"; + +const REPLY_DEFAULTS_KEY = "velum.reply.defaults.v1"; + +export type ReplyDefaults = { + greeting: string; + signOff: string; + model: string; + creativity: string; + disclosure: boolean; +}; + +export async function loadReplyDefaults(): Promise> { + const raw = await LocalStorage.getItem(REPLY_DEFAULTS_KEY); + if (!raw) return {}; + try { + return JSON.parse(raw) as Partial; + } catch { + return {}; + } +} + +export async function saveReplyDefaults(values: ReplyDefaults): Promise { + await LocalStorage.setItem(REPLY_DEFAULTS_KEY, JSON.stringify(values)); +} + +export const GREETING_OPTIONS: Array<{ value: string; title: string }> = [ + { value: "Lieber", title: "Lieber" }, + { value: "Liebe", title: "Liebe" }, + { value: "Hallo", title: "Hallo" }, + { value: "Hi", title: "Hi" }, + { value: "Sehr geehrter", title: "Sehr geehrter" }, + { value: "Sehr geehrte", title: "Sehr geehrte" }, +]; + +export const SIGN_OFF_OPTIONS: Array<{ value: string; title: string }> = [ + { value: "Alles Liebe,", title: "Alles Liebe," }, + { value: "Liebe Grüße,", title: "Liebe Grüße," }, + { value: "Viele Grüße,", title: "Viele Grüße," }, + { value: "Beste Grüße,", title: "Beste Grüße," }, + { value: "Mit freundlichen Grüßen,", title: "Mit freundlichen Grüßen," }, +]; + +export function listPersonPlaceholdersInText( + pseudonymizedText: string, + mapping: PlaceholderMapping, +): string[] { + return listPersonPlaceholders(mapping).filter((placeholder) => + pseudonymizedText.includes(placeholder), + ); +} + +export function guessRecipientPlaceholder( + pseudonymizedText: string, + mapping: PlaceholderMapping, +): string | null { + const persons = listPersonPlaceholdersInText(pseudonymizedText, mapping); + if (persons.length === 0) return null; + + const fromMatch = pseudonymizedText.match( + /(?:^|\n)\s*(?:Von|From|Absender)\s*:[^\n]*?()/i, + ); + if (fromMatch) return fromMatch[1]; + + const signOffMatch = pseudonymizedText.match( + /(?:Liebe Grüße|Liebe Gruesse|Beste Grüße|Beste Gruesse|Mit freundlichen Grüßen|Mit freundlichen Gruessen|Viele Grüße|Viele Gruesse|Alles Liebe|Herzliche Grüße|Herzliche Gruesse|MfG|LG|VG|Cheers|Best regards|Kind regards)[\s,!.\n]+()/i, + ); + if (signOffMatch) return signOffMatch[1]; + + const greetingMatch = pseudonymizedText.match( + /(?:Lieber|Liebe|Hallo|Hi|Hey|Sehr geehrter|Sehr geehrte|Guten Tag)\s+(?:Herr |Frau )?()/i, + ); + const greetingPlaceholder = greetingMatch ? greetingMatch[1] : null; + + for (const placeholder of persons) { + if (placeholder === greetingPlaceholder) continue; + return placeholder; + } + + return persons[0]; +} + +export function listPersonPlaceholders(mapping: PlaceholderMapping): string[] { + return Object.entries(mapping) + .filter(([, entry]) => entry.type.toUpperCase() === "PERSON") + .map(([placeholder]) => placeholder) + .sort((a, b) => { + const numA = Number.parseInt(a.replace(/\D/g, ""), 10); + const numB = Number.parseInt(b.replace(/\D/g, ""), 10); + if (Number.isFinite(numA) && Number.isFinite(numB)) { + return numA - numB; + } + return a.localeCompare(b); + }); +} + +export function extractPersonPlaceholdersFromText( + pseudonymizedText: string, +): string[] { + const matches = pseudonymizedText.match(//g) ?? []; + const unique = Array.from(new Set(matches)); + return unique.sort((a, b) => { + const numA = Number.parseInt(a.replace(/\D/g, ""), 10); + const numB = Number.parseInt(b.replace(/\D/g, ""), 10); + return numA - numB; + }); +} + +export function buildReplyPrompt(options: { + pseudonymizedText: string; + greeting: string; + instructions?: string; +}): string { + const extra = options.instructions?.trim() + ? `\n\nZusätzliche Anweisungen des Nutzers:\n${options.instructions.trim()}` + : ""; + + const availablePersons = extractPersonPlaceholdersFromText( + options.pseudonymizedText, + ); + const allowlistText = + availablePersons.length > 0 + ? `Erlaubte PERSON-Platzhalter (du darfst NUR genau einen aus dieser Liste verwenden, zeichengetreu inkl. spitzer Klammern): ${availablePersons.join(", ")}.` + : "In der Konversation kommt kein PERSON-Platzhalter vor. Verzichte in diesem Fall ausnahmsweise auf den Namen in der Anrede und schreibe nur die Anrede ohne Namen, gefolgt von einem Komma."; + + return [ + "Du erhältst eine pseudonymisierte Email-Konversation (oder eine einzelne Email) und sollst eine Antwort verfassen.", + "", + "Personenbezogene Daten wurden bereits durch Platzhalter wie , , , , ersetzt.", + "STRENGE REGEL: Gib jeden Platzhalter zeichengetreu (inklusive spitzer Klammern, Großbuchstaben und Unterstrich + Nummer) zurück. Erfinde keine neuen Platzhalter.", + "Du darfst Platzhalter NIEMALS auflösen, raten, übersetzen oder mit erfundenen Namen ersetzen.", + "", + "WICHTIG — wer ist der Empfänger der Antwort?", + "Die zu beantwortende Email ist die OBERSTE/NEUESTE im Text (Email-Clients zeigen neueste oben).", + "In dieser obersten Email gilt:", + "- Direkt am Anfang steht meist eine Anrede wie „Lieber ,“ oder „Hallo ,“. Diese Person ist der EMPFÄNGER der EINGEGANGENEN Email — also der NUTZER selbst, für den du die Antwort verfasst. Verwende DIESEN Platzhalter NICHT als Adressaten deiner Antwort.", + "- Am Ende der obersten Email (vor möglichen älteren, eingerückten/zitierten Mails) steht meist eine Grußformel („LG “, „Liebe Grüße “, „Mit freundlichen Grüßen “, o. ä.). Diese Person ist der SENDER der eingegangenen Email — und damit der EMPFÄNGER deiner Antwort.", + "Beispiel: Eingang lautet „Lieber , … LG “. Dann ist der Nutzer (NICHT verwenden), und du adressierst die Antwort an .", + "", + "Aufgabe: Verfasse die Antwort auf Deutsch.", + "", + "Format der Antwort — exakt einhalten:", + `1. Erste Zeile: Begrüßung im Format „${options.greeting} ,“ — wähle den Platzhalter des SENDERS der obersten Email (siehe oben). Wähle NICHT den Platzhalter aus der Anrede der eingegangenen Email.`, + ` ${allowlistText}`, + " ABSOLUT VERBOTEN: Schreibe niemals ``, ``, ``, `` oder ähnliche Schema-Platzhalter mit Buchstaben statt Ziffern. Nur konkrete Werte aus der Allowlist sind erlaubt.", + "2. Eine Leerzeile.", + "3. Den eigentlichen Antworttext.", + "WICHTIG: Schreibe KEINE Signatur, KEINE Grußformel und KEINEN Absendernamen am Ende — die Grußformel und der Absender werden außerhalb der KI angefügt.", + extra, + "", + "--- Email-Konversation (pseudonymisiert) ---", + options.pseudonymizedText, + "--- Ende der Konversation ---", + ].join("\n"); +} + +export function composeReplyEmail(options: { + body: string; + signOff: string; + userFullName: string; + disclosureBlock?: string; +}): string { + const trimmedBody = options.body.trim(); + const signature = `${options.signOff}\n${options.userFullName.trim()}`; + const tail = options.disclosureBlock?.trim() + ? `\n\n${options.disclosureBlock.trim()}` + : ""; + return `${trimmedBody}\n\n${signature}${tail}`; +} + +export function extractAllPlaceholdersFromText( + pseudonymizedText: string, +): string[] { + const matches = pseudonymizedText.match(/<[A-Z]+_\d+>/g) ?? []; + return Array.from(new Set(matches)); +} + +export type DisclosureContent = { + text: string; + html: string; +}; + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">"); +} + +const TYPE_LABELS: Record = { + PERSON: "Personen", + ORT: "Orte", + LOCATION: "Orte", + GPE: "Orte", + EMAIL: "E-Mail-Adressen", + EMAIL_ADDRESS: "E-Mail-Adressen", + PHONE: "Telefonnummern", + PHONE_NUMBER: "Telefonnummern", + ADDRESS: "Adressen", + ORG: "Organisationen", + ORGANIZATION: "Organisationen", + DATE: "Datumsangaben", + TIME: "Zeitangaben", + IBAN: "IBANs", + IBAN_CODE: "IBANs", + URL: "URLs", + CREDIT_CARD: "Kreditkartennummern", + ID: "IDs", + MISC: "Sonstiges", +}; + +function labelForType(type: string): string { + const upper = type.toUpperCase(); + return TYPE_LABELS[upper] ?? upper; +} + +export function buildDisclosureContent( + pseudonymizedText: string, + mapping: PlaceholderMapping, +): DisclosureContent | null { + const entries = Object.entries(mapping) + .filter(([key]) => { + const stripped = key.replace(/[<>]/g, ""); + return ( + pseudonymizedText.includes(key) || + pseudonymizedText.includes(`<${stripped}>`) || + pseudonymizedText.includes(stripped) + ); + }) + .map(([key, entry]) => ({ + original: entry.original, + placeholder: key.replace(/[<>]/g, ""), + type: entry.type, + })) + .sort((a, b) => a.placeholder.localeCompare(b.placeholder)); + + if (entries.length === 0) return null; + + const groups = new Map(); + for (const entry of entries) { + const label = labelForType(entry.type); + const list = groups.get(label) ?? []; + list.push(entry); + groups.set(label, list); + } + const groupedEntries = Array.from(groups.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + + const heading = "Dieses Email wurde KI generiert."; + const subheading = + "Folgende Texte wurden vor der Übermittlung lokal maskiert:"; + + const text = [ + "---", + heading, + subheading, + ...groupedEntries.map( + ([label, list]) => + `${label}: ${list.map((e) => `${e.original} (${e.placeholder})`).join(", ")}`, + ), + ].join("\n"); + + const html = [ + "
", + `

${escapeHtml(heading)}
${escapeHtml(subheading)}

`, + "

", + groupedEntries + .map( + ([label, list]) => + `${escapeHtml(label)}: ${list + .map( + (e) => + `${escapeHtml(e.original)} (${escapeHtml(e.placeholder)})`, + ) + .join(", ")}`, + ) + .join("
"), + "

", + ].join("\n"); + + return { text, html }; +} + +export function plainTextToHtmlEmail(plainText: string): string { + const escaped = plainText + .replace(/&/g, "&") + .replace(//g, ">"); + const paragraphs = escaped.split(/\n\n+/); + return paragraphs + .map((paragraph) => { + if (/^\s*---\s*$/.test(paragraph)) { + return "
"; + } + if (/^---\n/.test(paragraph)) { + const rest = paragraph.replace(/^---\n/, ""); + return `
\n

${rest.replace(/\n/g, "
")}

`; + } + return `

${paragraph.replace(/\n/g, "
")}

`; + }) + .join("\n"); +} diff --git a/src/selection.ts b/src/selection.ts new file mode 100644 index 0000000..64e7de8 --- /dev/null +++ b/src/selection.ts @@ -0,0 +1,35 @@ +import { Clipboard, getSelectedText } from "@raycast/api"; + +// Raycast 2.0 Beta: getSelectedText() alone does not yield the frontmost app's +// selection — a Clipboard.clear() beforehand appears to be required to push +// focus back to the source app. We deliberately do NOT snapshot/restore the +// previous clipboard, because Clipboard.copy() afterwards dismisses the main +// window in 2.0. Side effect: the user's previous clipboard content is lost +// whenever this helper is invoked. +// +// Outlook (and other Electron/slow-responding apps) sometimes write to the +// clipboard after getSelectedText() has already read it — so we also fall +// back to a delayed Clipboard.readText() before declaring "no selection". +export async function getSelectedTextSafely(): Promise { + async function attempt(): Promise { + await Clipboard.clear().catch(() => undefined); + + try { + const selected = await getSelectedText(); + if (selected && selected.length > 0) return selected; + } catch { + // fall through to clipboard fallback + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + const clipboardText = await Clipboard.readText().catch(() => undefined); + return clipboardText && clipboardText.length > 0 ? clipboardText : null; + } + + let result = await attempt(); + if (result === null) { + await new Promise((resolve) => setTimeout(resolve, 200)); + result = await attempt(); + } + return result; +} diff --git a/src/sessions.ts b/src/sessions.ts new file mode 100644 index 0000000..aa590e8 --- /dev/null +++ b/src/sessions.ts @@ -0,0 +1,192 @@ +import { LocalStorage } from "@raycast/api"; +import { randomUUID } from "node:crypto"; +import { getPreferences } from "./preferences"; +import type { PlaceholderMapping, SessionMode, VelumSession } from "./types"; + +const SESSIONS_STORAGE_KEY = "velum.sessions.v1"; +const ACTIVE_SESSION_STORAGE_KEY = "velum.sessions.active.v1"; + +function nowIso(): string { + return new Date().toISOString(); +} + +function dayKey(date = new Date()): string { + return date.toISOString().slice(0, 10); +} + +function sessionSort(a: VelumSession, b: VelumSession): number { + return Date.parse(b.updatedAt) - Date.parse(a.updatedAt); +} + +async function saveSessions(sessions: VelumSession[]): Promise { + const maxSessions = getPreferences().maxSessions; + const pruned = [...sessions].sort(sessionSort).slice(0, maxSessions); + await LocalStorage.setItem(SESSIONS_STORAGE_KEY, JSON.stringify(pruned)); + + const activeId = await getActiveSessionId(); + if (activeId && !pruned.some((session) => session.id === activeId)) { + await LocalStorage.setItem(ACTIVE_SESSION_STORAGE_KEY, pruned[0]?.id ?? ""); + } +} + +export async function listSessions(): Promise { + const raw = await LocalStorage.getItem(SESSIONS_STORAGE_KEY); + if (!raw) { + return []; + } + + try { + return (JSON.parse(raw) as VelumSession[]).sort(sessionSort); + } catch { + await LocalStorage.removeItem(SESSIONS_STORAGE_KEY); + return []; + } +} + +export async function getActiveSessionId(): Promise { + const id = await LocalStorage.getItem(ACTIVE_SESSION_STORAGE_KEY); + return id || undefined; +} + +export async function setActiveSession(id: string): Promise { + await LocalStorage.setItem(ACTIVE_SESSION_STORAGE_KEY, id); +} + +export async function createSession(name?: string): Promise { + const timestamp = nowIso(); + const session: VelumSession = { + id: randomUUID(), + name: name || `Sitzung ${new Date().toLocaleString()}`, + createdAt: timestamp, + updatedAt: timestamp, + mapping: {}, + }; + + const sessions = await listSessions(); + await saveSessions([session, ...sessions]); + await setActiveSession(session.id); + return session; +} + +export async function getSession( + id: string, +): Promise { + const sessions = await listSessions(); + return sessions.find((session) => session.id === id); +} + +export async function getActiveSession(): Promise { + const sessions = await listSessions(); + const activeId = await getActiveSessionId(); + const active = sessions.find((session) => session.id === activeId); + if (active) { + return active; + } + return createSession(); +} + +export async function resolveSessionForMode( + mode: SessionMode, +): Promise { + if (mode === "new-each-request") { + return createSession(`Anfrage ${new Date().toLocaleString()}`); + } + + if (mode === "daily") { + const name = `Tag ${dayKey()}`; + const existing = (await listSessions()).find( + (session) => session.name === name, + ); + if (existing) { + await setActiveSession(existing.id); + return existing; + } + return createSession(name); + } + + return getActiveSession(); +} + +export async function resolveDefaultSession(): Promise { + return resolveSessionForMode(getPreferences().sessionMode); +} + +export async function updateSessionMapping( + id: string, + mapping: PlaceholderMapping, + entityTypes?: string[], +): Promise { + const sessions = await listSessions(); + const timestamp = nowIso(); + const index = sessions.findIndex((session) => session.id === id); + + if (index === -1) { + const created: VelumSession = { + id, + name: `Sitzung ${new Date().toLocaleString()}`, + createdAt: timestamp, + updatedAt: timestamp, + mapping, + entityTypes, + }; + await saveSessions([created, ...sessions]); + return created; + } + + const updated: VelumSession = { + ...sessions[index], + mapping, + entityTypes, + updatedAt: timestamp, + }; + const next = [...sessions]; + next[index] = updated; + await saveSessions(next); + return updated; +} + +export async function renameSession(id: string, name: string): Promise { + const sessions = await listSessions(); + await saveSessions( + sessions.map((session) => + session.id === id + ? { ...session, name: name.trim() || session.name, updatedAt: nowIso() } + : session, + ), + ); +} + +export async function clearSessionMapping(id: string): Promise { + const sessions = await listSessions(); + await saveSessions( + sessions.map((session) => + session.id === id + ? { ...session, mapping: {}, updatedAt: nowIso() } + : session, + ), + ); +} + +export async function deleteSession(id: string): Promise { + const sessions = await listSessions(); + const next = sessions.filter((session) => session.id !== id); + await saveSessions(next); + + const activeId = await getActiveSessionId(); + if (activeId === id) { + if (next[0]) { + await setActiveSession(next[0].id); + } else { + await LocalStorage.removeItem(ACTIVE_SESSION_STORAGE_KEY); + } + } +} + +export async function deleteAllSessions(): Promise { + await LocalStorage.removeItem(SESSIONS_STORAGE_KEY); + await LocalStorage.removeItem(ACTIVE_SESSION_STORAGE_KEY); +} + +export function countMappingEntries(mapping: PlaceholderMapping): number { + return Object.keys(mapping).length; +} diff --git a/src/summarize-email.tsx b/src/summarize-email.tsx new file mode 100644 index 0000000..b0cc752 --- /dev/null +++ b/src/summarize-email.tsx @@ -0,0 +1,538 @@ +import { + Action, + ActionPanel, + AI, + Clipboard, + Detail, + Form, + Icon, + showHUD, + showToast, + Toast, + useNavigation, +} from "@raycast/api"; +import { copyRichText, markdownToHtml, maybeCloseRaycast } from "./ai-views"; +import { getSelectedTextSafely } from "./selection"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { getPreferences } from "./preferences"; +import { + createSession, + getActiveSessionId, + getSession, + listSessions, + setActiveSession, + updateSessionMapping, +} from "./sessions"; +import { buildSummaryPrompt, type Creativity } from "./summarize"; +import type { EntityType, VelumSession } from "./types"; +import { + markdownCodeBlock, + mappingDetailTable, + NEW_SESSION_ID, + sessionSubtitle, +} from "./ui"; +import { + getEntityTypes, + localDepseudonymize, + normalizePseudonymizeResponse, + pseudonymize, +} from "./velum"; + +type FormValues = { + text: string; + sessionId: string; + entityTypes: string[]; + model: string; + creativity: Creativity; + instructions: string; +}; + +const CREATIVITY_OPTIONS: Creativity[] = ["none", "low", "medium", "high"]; + +const MODEL_OPTIONS: Array<{ value: string; title: string }> = [ + { value: "anthropic-claude-sonnet-4-6", title: "Claude 4.6 Sonnet" }, + { value: "anthropic-claude-opus-4-7", title: "Claude 4.7 Opus" }, + { value: "anthropic-claude-4-5-haiku", title: "Claude 4.5 Haiku" }, + { value: "openai-gpt-5.3-instant", title: "OpenAI GPT-5.3 Instant" }, + { value: "openai-gpt-4.1", title: "OpenAI GPT-4.1" }, + { value: "openai-gpt-4o-mini", title: "OpenAI GPT-4o mini" }, +]; + +export default function Command() { + const preferences = getPreferences(); + const [text, setText] = useState(""); + const [sessions, setSessions] = useState([]); + const [activeSessionId, setActiveSessionId] = useState(); + const [entityTypes, setEntityTypes] = useState([]); + const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { push } = useNavigation(); + + useEffect(() => { + async function load() { + const [loadedSessions, activeId] = await Promise.all([ + listSessions(), + getActiveSessionId(), + ]); + setSessions(loadedSessions); + setActiveSessionId(activeId); + + const selection = await getSelectedTextSafely(); + if (selection && selection.trim()) { + setText(selection); + } + + try { + const loadedEntityTypes = await getEntityTypes(); + setEntityTypes(loadedEntityTypes); + setSelectedEntityTypes(loadedEntityTypes); + } catch { + setEntityTypes([]); + } finally { + setIsLoading(false); + } + } + load(); + }, []); + + const defaultSessionId = useMemo( + () => activeSessionId || sessions[0]?.id || NEW_SESSION_ID, + [activeSessionId, sessions], + ); + + async function loadSelectedText() { + const selected = await getSelectedTextSafely(); + if (!selected) { + await showToast({ + style: Toast.Style.Failure, + title: "Kein markierter Text gefunden", + }); + return; + } + setText(selected); + } + + async function loadClipboardText() { + const clipboardText = await Clipboard.readText(); + if (!clipboardText) { + await showToast({ + style: Toast.Style.Failure, + title: "Zwischenablage enthält keinen Text", + }); + return; + } + setText(clipboardText); + } + + async function handleSubmit(values: FormValues) { + const input = values.text.trim(); + if (!input) { + await showToast({ + style: Toast.Style.Failure, + title: "Text zum Zusammenfassen eingeben", + }); + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Pseudonymisiere…", + }); + + try { + const session = + values.sessionId === NEW_SESSION_ID + ? await createSession() + : ((await getSession(values.sessionId)) ?? (await createSession())); + await setActiveSession(session.id); + + const rawResult = await pseudonymize( + input, + session.mapping, + values.entityTypes, + ); + const pseudoResult = normalizePseudonymizeResponse(rawResult); + const updatedSession = await updateSessionMapping( + session.id, + pseudoResult.mapping, + pseudoResult.selected_entity_types, + ); + + toast.style = Toast.Style.Success; + toast.title = "Pseudonymisiert"; + toast.message = `${pseudoResult.entity_count} Zuordnungen`; + + push( + , + ); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Pseudonymisierung fehlgeschlagen"; + toast.message = error instanceof Error ? error.message : String(error); + } + } + + return ( +
+ + + + + } + > + + + + {sessions.map((session) => ( + + ))} + + {entityTypes.length > 0 ? ( + + {entityTypes.map((entityType) => ( + + ))} + + ) : null} + + {MODEL_OPTIONS.map((option) => ( + + ))} + + + {CREATIVITY_OPTIONS.map((value) => ( + + ))} + + + + ); +} + +type StageProps = { + session: VelumSession; + pseudonymizedText: string; + model: string; + creativity: Creativity; + instructions: string; +}; + +function ConfirmPseudonymized({ + session, + pseudonymizedText, + model, + creativity, + instructions, +}: StageProps) { + const [editedText, setEditedText] = useState(pseudonymizedText); + const [editedInstructions, setEditedInstructions] = useState(instructions); + const { push } = useNavigation(); + + async function handleSubmit() { + if (!editedText.trim()) { + await showToast({ + style: Toast.Style.Failure, + title: "Pseudonymisierter Text ist leer", + }); + return; + } + + push( + , + ); + } + + return ( +
+ + + + } + > + werden nach der Zusammenfassung wiederhergestellt. Sitzung: ${session.name} · ${Object.keys(session.mapping).length} Zuordnungen.`} + /> + + + + + + ); +} + +function mappingDescriptionText(session: VelumSession): string { + const entries = Object.entries(session.mapping).sort(([a], [b]) => + a.localeCompare(b), + ); + if (entries.length === 0) { + return "Keine Einträge — nichts wurde pseudonymisiert."; + } + return entries + .map(([placeholder, entry]) => `${placeholder} → ${entry.original}`) + .join("\n"); +} + +type Phase = "summarizing" | "restoring" | "done" | "error"; + +function SummarizeEmailResult({ + session, + pseudonymizedText, + model, + creativity, + instructions, +}: StageProps) { + const [aiBuffer, setAiBuffer] = useState(""); + const [restored, setRestored] = useState(null); + const [replacementsMade, setReplacementsMade] = useState(0); + const [phase, setPhase] = useState("summarizing"); + const [error, setError] = useState(null); + const controllerRef = useRef(null); + + useEffect(() => { + const controller = new AbortController(); + controllerRef.current = controller; + let cancelled = false; + + async function run() { + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Fasse zusammen…", + }); + + try { + const prompt = buildSummaryPrompt(pseudonymizedText, instructions); + const stream = AI.ask(prompt, { + model: model as AI.Model, + creativity, + signal: controller.signal, + }); + stream.on("data", (chunk) => { + if (cancelled) return; + setAiBuffer((prev) => prev + chunk); + }); + const aiText = await stream; + if (cancelled) return; + + toast.title = "Stelle wieder her…"; + setPhase("restoring"); + + const restoreResult = localDepseudonymize(aiText, session.mapping); + if (cancelled) return; + + setRestored(restoreResult.original_text); + setReplacementsMade(restoreResult.replacements_made); + setPhase("done"); + + toast.style = Toast.Style.Success; + toast.title = "Fertig"; + toast.message = `${restoreResult.replacements_made} Ersetzungen`; + } catch (err) { + if (cancelled || controller.signal.aborted) return; + const message = err instanceof Error ? err.message : String(err); + setError(message); + setPhase("error"); + toast.style = Toast.Style.Failure; + toast.title = "Zusammenfassung fehlgeschlagen"; + toast.message = message; + } + } + + run(); + + return () => { + cancelled = true; + controller.abort(); + }; + }, [pseudonymizedText, model, creativity, instructions, session.mapping]); + + const phaseLabel = + phase === "summarizing" + ? "KI generiert Zusammenfassung …" + : phase === "restoring" + ? "Platzhalter werden ersetzt …" + : phase === "error" + ? "Fehler" + : "Fertig"; + + const summaryBody = restored ?? aiBuffer; + + const markdown = [ + `# ${session.name}`, + sessionSubtitle(session), + `*Status:* ${phaseLabel}`, + "", + summaryBody.trim() + ? summaryBody + : "_Noch keine Inhalte — warte auf das Modell …_", + "", + restored + ? `*${replacementsMade} Platzhalter ersetzt.*` + : "_Originalnamen werden eingesetzt, sobald das Modell fertig ist._", + "", + "## Pseudonymisierte Eingabe", + markdownCodeBlock(pseudonymizedText), + "", + "## Pseudonymisierte KI-Ausgabe", + aiBuffer.trim() + ? markdownCodeBlock(aiBuffer) + : "_Noch nichts vom Modell empfangen._", + "", + "## Zuordnung", + mappingDetailTable(session.mapping), + error ? `\n> Fehler: ${error}` : "", + ].join("\n"); + + const isLoading = phase === "summarizing" || phase === "restoring"; + + return ( + + {restored ? ( + <> + { + try { + await copyRichText(markdownToHtml(restored), restored); + await showHUD("Als Rich Text kopiert"); + } catch { + await Clipboard.copy(restored); + await showHUD("Kopiert (Plain Text Fallback)"); + } + await maybeCloseRaycast(); + }} + /> + { + await Clipboard.copy(restored); + await showHUD("Als Markdown kopiert"); + await maybeCloseRaycast(); + }} + /> + { + await Clipboard.paste({ + html: markdownToHtml(restored), + text: restored, + }); + await maybeCloseRaycast(); + }} + /> + + ) : null} + {aiBuffer ? ( + + ) : null} + + + } + /> + ); +} diff --git a/src/summarize.ts b/src/summarize.ts new file mode 100644 index 0000000..61fbc2c --- /dev/null +++ b/src/summarize.ts @@ -0,0 +1,58 @@ +import { AI } from "@raycast/api"; + +export type Creativity = "none" | "low" | "medium" | "high"; + +export function buildSummaryPrompt( + pseudonymizedText: string, + instructions?: string, +): string { + const extra = instructions?.trim() + ? `\n\nZusätzliche Anweisungen des Nutzers:\n${instructions.trim()}` + : ""; + + return [ + "Du erhältst eine pseudonymisierte Email-Konversation.", + "Personenbezogene Daten wurden bereits durch Platzhalter wie , , , , ersetzt.", + "", + "STRENGE REGEL: Gib jeden Platzhalter zeichengetreu (inklusive spitzer Klammern, Großbuchstaben und Unterstrich + Nummer) zurück.", + "Du darfst Platzhalter NIEMALS auflösen, raten, übersetzen oder mit erfundenen Namen ersetzen. Schreibe sie exakt so, wie sie in der Eingabe stehen.", + "", + "Aufgabe: Erstelle eine prägnante deutschsprachige Zusammenfassung der Konversation mit folgenden Abschnitten als Markdown:", + "- **Teilnehmer**: Liste der beteiligten Platzhalter, ggf. mit Rolle (Absender/Empfänger).", + "- **Anliegen**: Worum geht es im Kern?", + "- **Verlauf**: Chronologische Kurzfassung der wichtigsten Punkte.", + "- **Offene Punkte / Action Items**: Was ist noch zu tun, von wem, bis wann.", + "", + "Antworte ausschließlich in Markdown, ohne einleitende Floskeln.", + extra, + "", + "--- Email-Konversation (pseudonymisiert) ---", + pseudonymizedText, + "--- Ende der Konversation ---", + ].join("\n"); +} + +export type SummaryStreamOptions = { + prompt: string; + model: string; + creativity: Creativity; + signal?: AbortSignal; + onChunk: (chunk: string) => void; +}; + +export async function runSummaryStream( + options: SummaryStreamOptions, +): Promise { + let buffer = ""; + const stream = AI.ask(options.prompt, { + model: options.model as AI.Model, + creativity: options.creativity, + signal: options.signal, + }); + stream.on("data", (chunk) => { + buffer += chunk; + options.onChunk(chunk); + }); + await stream; + return buffer; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..94b28cb --- /dev/null +++ b/src/types.ts @@ -0,0 +1,55 @@ +export type EntityType = string; + +export type MappingEntry = { + original: string; + type: EntityType; +}; + +export type PlaceholderMapping = Record; + +export type PseudonymizeResponse = { + pseudonymized_text: string; + selected_entity_types: EntityType[]; + mapping: PlaceholderMapping; + spans: Array<{ + start: number; + end: number; + type: EntityType; + score: number; + placeholder: string; + original: string; + }>; + entity_count: number; +}; + +export type DepseudonymizeResponse = { + original_text: string; + replacements_made: number; +}; + +export type VelumSession = { + id: string; + name: string; + createdAt: string; + updatedAt: string; + mapping: PlaceholderMapping; + entityTypes?: EntityType[]; +}; + +export type SessionMode = "reuse-active" | "new-each-request" | "daily"; +export type QuickOutput = "copy" | "paste"; + +export type ExtensionPreferences = { + velumBaseUrl: string; + authentikTokenUrl: string; + clientId: string; + serviceAccountUsername: string; + serviceAccountPassword: string; + scope?: string; + sessionMode: SessionMode; + quickOutput: QuickOutput; + summaryModel: string; + userFullName?: string; + maxSessions: string; + closeAfterAction: boolean; +}; diff --git a/src/ui.ts b/src/ui.ts new file mode 100644 index 0000000..28e6ce9 --- /dev/null +++ b/src/ui.ts @@ -0,0 +1,57 @@ +import type { PlaceholderMapping, VelumSession } from "./types"; + +export const NEW_SESSION_ID = "__new_session__"; + +export function markdownCodeBlock(value: string): string { + return `\`\`\`text\n${value.replace(/```/g, "`\u200b``")}\n\`\`\``; +} + +function escapeMarkdownTableCell(value: string): string { + return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " "); +} + +export function mappingDetailTable(mapping: PlaceholderMapping): string { + const rows = Object.entries(mapping) + .sort(([a], [b]) => a.localeCompare(b)) + .map( + ([placeholder, entry]) => + `| \`${placeholder}\` | ${escapeMarkdownTableCell(entry.original)} | ${entry.type} |`, + ); + + if (rows.length === 0) { + return "Keine Einträge."; + } + + return [ + "| Platzhalter | Original | Typ |", + "| --- | --- | --- |", + ...rows, + ].join("\n"); +} + +export function mappingSummary(mapping: PlaceholderMapping): string { + const counts = Object.values(mapping).reduce>( + (acc, entry) => { + acc[entry.type] = (acc[entry.type] ?? 0) + 1; + return acc; + }, + {}, + ); + + const rows = Object.entries(counts) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([type, count]) => `| ${type} | ${count} |`); + + if (rows.length === 0) { + return "Keine Einträge."; + } + + return ["| Typ | Anzahl |", "| --- | ---: |", ...rows].join("\n"); +} + +export function sessionSubtitle(session: VelumSession): string { + const entries = Object.keys(session.mapping).length; + return `${entries} ${entries === 1 ? "Zuordnung" : "Zuordnungen"} · aktualisiert ${new Date( + session.updatedAt, + ).toLocaleString()}`; +} diff --git a/src/velum.ts b/src/velum.ts new file mode 100644 index 0000000..d8b8681 --- /dev/null +++ b/src/velum.ts @@ -0,0 +1,144 @@ +import { fetchWithAuth } from "./auth"; +import { getPreferences } from "./preferences"; +import type { + DepseudonymizeResponse, + EntityType, + PlaceholderMapping, + PseudonymizeResponse, +} from "./types"; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function normalizePseudonymizeResponse( + response: PseudonymizeResponse, +): PseudonymizeResponse { + const normalizedMapping: PlaceholderMapping = {}; + const strippedKeys = new Set(); + + for (const [key, entry] of Object.entries(response.mapping)) { + const stripped = key.replace(/[<>]/g, ""); + normalizedMapping[`<${stripped}>`] = entry; + strippedKeys.add(stripped); + } + + let text = response.pseudonymized_text; + const ordered = Array.from(strippedKeys).sort((a, b) => b.length - a.length); + for (const stripped of ordered) { + const bracketed = `<${stripped}>`; + const bareRegex = new RegExp( + `(?])`, + "g", + ); + text = text.replace(bareRegex, bracketed); + } + + return { + ...response, + pseudonymized_text: text, + mapping: normalizedMapping, + }; +} + +export function localDepseudonymize( + text: string, + mapping: PlaceholderMapping, +): DepseudonymizeResponse { + let result = text; + let replacementsMade = 0; + + const entries = Object.entries(mapping) + .map(([key, entry]) => ({ + stripped: key.replace(/[<>]/g, ""), + entry, + })) + .sort((a, b) => b.stripped.length - a.stripped.length); + + for (const { stripped, entry } of entries) { + const bracketed = `<${stripped}>`; + const bracketedRegex = new RegExp(escapeRegExp(bracketed), "g"); + const bracketedMatches = result.match(bracketedRegex); + if (bracketedMatches) { + replacementsMade += bracketedMatches.length; + result = result.replace(bracketedRegex, entry.original); + } + + const bareRegex = new RegExp(`\\b${escapeRegExp(stripped)}\\b`, "g"); + const bareMatches = result.match(bareRegex); + if (bareMatches) { + replacementsMade += bareMatches.length; + result = result.replace(bareRegex, entry.original); + } + } + + return { original_text: result, replacements_made: replacementsMade }; +} + +function apiUrl(path: string): string { + return `${getPreferences().velumBaseUrl}${path}`; +} + +async function parseJsonResponse(response: Response): Promise { + const contentType = response.headers.get("content-type") ?? ""; + if (!response.ok) { + const details = await response.text().catch(() => response.statusText); + throw new Error( + `Velum API request failed: ${response.status} ${response.statusText}: ${details}`, + ); + } + if (!contentType.includes("application/json")) { + const details = await response.text().catch(() => ""); + throw new Error( + `Velum API returned non-JSON response: ${details.slice(0, 200)}`, + ); + } + return (await response.json()) as T; +} + +export async function getEntityTypes(): Promise { + const response = await fetchWithAuth(apiUrl("/api/entity-types")); + const payload = await parseJsonResponse<{ entity_types: EntityType[] }>( + response, + ); + return payload.entity_types; +} + +export async function pseudonymize( + text: string, + mapping: PlaceholderMapping, + entityTypes?: EntityType[], +): Promise { + const response = await fetchWithAuth(apiUrl("/api/pseudonymize"), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text, + mapping, + entity_types: + entityTypes && entityTypes.length > 0 ? entityTypes : undefined, + }), + }); + + return parseJsonResponse(response); +} + +export async function depseudonymize( + text: string, + mapping: PlaceholderMapping, +): Promise { + const response = await fetchWithAuth(apiUrl("/api/depseudonymize"), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text, + mapping, + }), + }); + + return parseJsonResponse(response); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fc758c5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2023" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"] +}