From 27d09b6791411df0c5143885ca42ba6909180168 Mon Sep 17 00:00:00 2001 From: Microck Date: Thu, 19 Mar 2026 18:07:41 +0000 Subject: [PATCH] feat(assistant): add ask-page command --- README.md | 15 ++- docs/commands/ask-page.mdx | 140 ++++++++++++++++++++++++++++ docs/demo-assets/ask-page.gif | Bin 0 -> 15487 bytes docs/demos.md | 6 +- docs/docs.json | 1 + docs/guides/quickstart.mdx | 7 ++ docs/index.mdx | 3 + docs/llms.txt | 1 + docs/project/demos.mdx | 10 +- docs/reference/auth-matrix.mdx | 6 ++ docs/reference/coverage.mdx | 7 +- docs/reference/error-reference.mdx | 40 ++++++++ docs/reference/output-contract.mdx | 30 ++++++ images/demos/ask-page.gif | Bin 0 -> 15487 bytes project/demos.mdx | 10 +- scripts/demo-ask-page.sh | 23 +++++ src/api.rs | 141 +++++++++++++++++++++++++++-- src/auth.rs | 17 ++-- src/cli.rs | 13 +++ src/main.rs | 17 +++- src/types.rs | 20 ++++ 21 files changed, 482 insertions(+), 25 deletions(-) create mode 100644 docs/commands/ask-page.mdx create mode 100644 docs/demo-assets/ask-page.gif create mode 100644 images/demos/ask-page.gif create mode 100755 scripts/demo-ask-page.sh diff --git a/README.md b/README.md index 712e700..fb2ce85 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ --- -`kagi` is a terminal CLI for Kagi that gives you command-line access to search, lenses, assistant, summarization, feeds, and paid API commands. it is built for people who want one command surface for interactive use, shell workflows, and structured JSON output. +`kagi` is a terminal CLI for Kagi that gives you command-line access to search, lenses, ask-page, assistant, summarization, feeds, and paid API commands. it is built for people who want one command surface for interactive use, shell workflows, and structured JSON output. the main setup path is your existing Kagi session-link URL. paste it into `kagi auth set --session-token` and the CLI extracts the token for you. if you also use Kagi's paid API, add `KAGI_API_TOKEN` and the public API commands are available too. @@ -101,7 +101,7 @@ export KAGI_API_TOKEN='...' | credential | what it unlocks | | --- | --- | -| `KAGI_SESSION_TOKEN` | base search, `search --lens`, `assistant`, `summarize --subscriber` | +| `KAGI_SESSION_TOKEN` | base search, `search --lens`, `ask-page`, `assistant`, `summarize --subscriber` | | `KAGI_API_TOKEN` | public `summarize`, `fastgpt`, `enrich web`, `enrich news` | | none | `news`, `smallweb`, `auth status`, `--help` | @@ -138,6 +138,7 @@ for the full command-to-token matrix, use the [`auth-matrix`](https://kagi.micr. | `kagi auth` | inspect, validate, and save credentials | | `kagi summarize` | use the paid public summarizer API or the subscriber summarizer with `--subscriber` | | `kagi news` | read Kagi News from public JSON endpoints | +| `kagi ask-page` | ask Kagi Assistant about a specific web page | | `kagi assistant` | prompt Kagi Assistant with a subscriber session token | | `kagi fastgpt` | query FastGPT through the paid API | | `kagi enrich` | query Kagi's web and news enrichment indexes | @@ -200,6 +201,12 @@ continue research with assistant: kagi assistant "plan a focused research session in the terminal" ``` +ask assistant about a page directly: + +```bash +kagi ask-page https://rust-lang.org/ "What is this page about?" +``` + use the subscriber summarizer: ```bash @@ -227,10 +234,12 @@ kagi enrich news "browser privacy" ## what it looks like -if you want a quick feel for the cli before installing it, this is the kind of output you get from the subscriber summarizer, assistant, and public news feed: +if you want a quick feel for the cli before installing it, this is the kind of output you get from ask-page, the subscriber summarizer, assistant, and the public news feed: ![summarize demo](images/demos/summarize.gif) +![ask-page demo](images/demos/ask-page.gif) + ![assistant demo](images/demos/assistant.gif) ![news demo](images/demos/news.gif) diff --git a/docs/commands/ask-page.mdx b/docs/commands/ask-page.mdx new file mode 100644 index 0000000..d960d6d --- /dev/null +++ b/docs/commands/ask-page.mdx @@ -0,0 +1,140 @@ +--- +title: "ask-page" +description: "Complete reference for *kagi* ask-page command - ask Kagi Assistant about a specific web page." +--- + +# `kagi ask-page` + +Ask Kagi Assistant about a specific web page and return the new thread plus the answer as JSON. + +![Ask Page demo](/images/demos/ask-page.gif) + +## Synopsis + +```bash +kagi ask-page +``` + +## Description + +The `kagi ask-page` command starts a new Assistant thread focused on one page URL. It mirrors the live Kagi web flow used by “Continue in Assistant” from search results, but packages it as a direct CLI command. + +The response includes: + +- stream metadata +- the normalized source URL and question +- the created Assistant thread +- the Assistant reply payload + +Use this command when you want page-specific follow-up without manually pasting the URL into a browser Assistant session. + +## Authentication + +**Required:** `KAGI_SESSION_TOKEN` + +This command uses the subscriber Assistant web-product flow. + +## Arguments + +### `` (Required) + +Absolute page URL to discuss. + +Constraints: + +- must be an absolute `http` or `https` URL +- empty values are rejected + +Examples: + +```bash +kagi ask-page https://rust-lang.org/ "What is this page about?" +kagi ask-page https://kagi.com "Summarize the main product pitch" +``` + +### `` (Required) + +Question to ask about the page. + +Examples: + +```bash +kagi ask-page https://rust-lang.org/ "What learning resources does this page link to?" +kagi ask-page https://example.com "Give me the core claim in one sentence" +``` + +## Output Format + +```json +{ + "meta": { + "version": "202603171911.stage.707e740", + "trace": "trace-123" + }, + "source": { + "url": "https://rust-lang.org/", + "question": "What is this page about?" + }, + "thread": { + "id": "thread-1", + "title": "Rust Programming Language Website", + "created_at": "2026-03-19T17:58:59Z" + }, + "message": { + "id": "msg-1", + "thread_id": "thread-1", + "state": "done", + "prompt": "https://rust-lang.org/\nWhat is this page about?", + "markdown": "This page is about the Rust programming language." + } +} +``` + +## Examples + +### Basic Usage + +```bash +kagi ask-page https://rust-lang.org/ "What is this page about?" +``` + +### Extract Only the Answer + +```bash +kagi ask-page https://rust-lang.org/ "What is this page about?" \ + | jq -r '.message.markdown' +``` + +### Continue the Thread in Assistant + +```bash +THREAD_ID=$( + kagi ask-page https://rust-lang.org/ "What is this page about?" \ + | jq -r '.thread.id' +) + +kagi assistant --thread-id "$THREAD_ID" "What learning resources does it mention?" +``` + +### Save a Page Analysis + +```bash +kagi ask-page https://example.com "Summarize the main argument" > page-analysis.json +``` + +## Error Cases + +Typical configuration errors: + +```text +Config error: ask-page URL cannot be empty +Config error: invalid ask-page URL: relative URL without a base +Config error: ask-page URL must use http or https, got `file` +Config error: ask-page question cannot be empty +``` + +## Notes + +- `ask-page` always starts a new Assistant thread in this version. +- For follow-up questions, continue the returned thread with `kagi assistant --thread-id ...`. +- The Assistant response keeps the combined prompt in `message.prompt`, while `source.url` and `source.question` preserve the original CLI inputs separately. diff --git a/docs/demo-assets/ask-page.gif b/docs/demo-assets/ask-page.gif new file mode 100644 index 0000000000000000000000000000000000000000..c74e157dd597f6362ba9256b99ba587dba23cb07 GIT binary patch literal 15487 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 ` | ask-page | ✅ | | `--thread-id` | assistant | ✅ | ## Not Available @@ -120,7 +123,7 @@ These features were evaluated and left out of the public CLI surface: | Type | Support | Commands | |------|---------|----------| -| Session Token | ✅ | search, summarize --subscriber, assistant | +| Session Token | ✅ | search, ask-page, summarize --subscriber, assistant | | API Token | ✅ | search, summarize, fastgpt, enrich | ### Authentication Patterns @@ -145,6 +148,7 @@ All commands output JSON: | news | Stable | | smallweb | Stable | | summarize | Stable | +| ask-page | Stable | | assistant | Stable | | fastgpt | Stable | | enrich | Stable | @@ -191,6 +195,7 @@ All commands output JSON: |---------|-----|-----| | Search | ✅ | ✅ | | Lens Search | ✅ | ✅ | +| Ask Questions about Page | ✅ | ✅ | | Assistant | ✅ (basic) | ✅ (full) | | Summarizer | ✅ | ✅ | | Translate | ❌ | ✅ | diff --git a/docs/reference/error-reference.mdx b/docs/reference/error-reference.mdx index f12d42a..50c3630 100644 --- a/docs/reference/error-reference.mdx +++ b/docs/reference/error-reference.mdx @@ -215,6 +215,46 @@ kagi summarize --url https://example.com kagi summarize --text "Content to summarize..." ``` +### "invalid ask-page URL" + +**Message:** +```text +Config error: invalid ask-page URL: relative URL without a base +``` + +**Meaning:** `kagi ask-page` requires an absolute `http` or `https` URL. + +**Solution:** +```bash +kagi ask-page https://example.com "What is this page about?" +``` + +### "ask-page URL must use http or https" + +**Message:** +```text +Config error: ask-page URL must use http or https, got `file` +``` + +**Meaning:** Local file URLs are not supported by `ask-page`. + +**Solution:** +Use a normal web URL, or ask the question through a different command surface. + +### "ask-page question cannot be empty" + +**Message:** +```text +Config error: ask-page question cannot be empty +``` + +**Meaning:** The URL alone is not enough. The command requires a page question too. + +**Solution:** +```bash +kagi ask-page https://example.com "Give me the main argument in one sentence" +``` + ## Network Errors ### "Network error: request to Kagi timed out" diff --git a/docs/reference/output-contract.mdx b/docs/reference/output-contract.mdx index a2cef36..2e6cd4e 100644 --- a/docs/reference/output-contract.mdx +++ b/docs/reference/output-contract.mdx @@ -137,6 +137,33 @@ Subscriber mode: } ``` +### `kagi ask-page` + +```json +{ + "meta": { + "version": "202603171911.stage.707e740", + "trace": "trace-123" + }, + "source": { + "url": "https://rust-lang.org/", + "question": "What is this page about?" + }, + "thread": { + "id": "thread-1", + "title": "Rust Programming Language Website", + "created_at": "2026-03-19T17:58:59Z" + }, + "message": { + "id": "msg-1", + "thread_id": "thread-1", + "state": "done", + "prompt": "https://rust-lang.org/\nWhat is this page about?", + "markdown": "This page is about the Rust programming language." + } +} +``` + ### `kagi fastgpt` ```json @@ -221,6 +248,9 @@ kagi fastgpt "What is Rust?" | jq -r '.data.output' # Assistant markdown reply kagi assistant "Hello" | jq -r '.message.markdown' +# Ask-page markdown reply +kagi ask-page https://rust-lang.org/ "What is this page about?" | jq -r '.message.markdown' + # Subscriber summary text kagi summarize --subscriber --url https://example.com | jq -r '.data.output' diff --git a/images/demos/ask-page.gif b/images/demos/ask-page.gif new file mode 100644 index 0000000000000000000000000000000000000000..c74e157dd597f6362ba9256b99ba587dba23cb07 GIT binary patch literal 15487 zcmc(`S5#A7ps2gDl0XQ(BT_=|y@jTsS0Qvn(17#~A_Af&^xg#ogwU(>4r1sa9RU#= z3Ic)x3Ieu2hku{5_u1#(`*O#9xa(n+Ip%y@;~VSy##{zQ`e;>WR_G7}5B#r#k4$Z* zXKkgUh1HXnmW4vjhxz`Y^zd?*zUAWu@co3rv(JYl|DFBsx@ZHeDHAix^z<|%6EiI> z9Rnj16%t89bAC%lO-)13z<9n)`h59+USMPfz%uG7dT*wWj1x_B^(ywPYxLb42$Y-) z@p~WEcwp;n2rUjYl;ljGs0g0)RV%@=Ll~bpG_5v-PJNAdHiZf^l`fpyX^)uk?|XmP z9ck9|`rxep{@lTDGM*3>9TOWDpOBc8oRXTBo{^cAo%1j^FaJ@24fwdYxUjUWyrNQ| zq^7E_zTrt`O-W;GTYE=XGqJO`um7oI_v3+~;gL}d5;*pJYI>%cG`q0)Vsv8ZOR-?q1N&mxIG^*Z05wI5{;wKKuRWui~$NKqS{04f|j;g4r9W zX)Ya(py4$xvX|x`P3BPWelyWhKAw&u>m+d*wN^}Ki(1t=PPSG~<;mWBUS!l(HB*Sb zwfknWt$MBm6OH6HZm(G=H_R4ydfs09q8eLn{MfjoZn@sR&HL^1j{28PxZxyjlg@@$ zZJsacoTfUTyzUCzc>dU=tMP4L*wOCWsjjB?&+t%c9@FmT55oz}64$4@TQ)<^p znwrU4LNXJl! zZD#1Qd2MDI3fFFCnJRzY%*N`Aeaf-6_4@SC-o5ryu4CxuPkFdFv8{a9T(7N1o>jG5 z1-_l1w+aKt#I}nE5c_YwSPS z-D@IINPKA?Ve|RYG9g_5rFB|)?+dwYUSHyC`;x8C*N#>9`mddDLifIQt;b31cW>tU z?DyD=4zI~=Vb5T89*s{NP@BZ9u6Wz8V-kORK6Sz zGZ;vI8)32Y{Wi+s(eQ1I>&};NUdt4-S2q8P~^$+qN&Q);}=*1sUJ($c78vW?LD6SSaH1b^~XzG zywu66Yo6c9E6?gDCu_c4Ur$~Kj!T`s33=&v`ZjF)$?3bu)32xR@s!eM>oM&9XCD$o z8qYqasO+C@WEiBSa$M&HlmTSeV+;iL9~7DRmm&xN34l;_Aojl~Qe0fDrL7$h7^tMI zVt@7O;NalhyOH7&5*-~KadGjF9~Wn3Wx2Y#)z;S5)z$IvqOM#q!)R&Wbaf2~2sFKN zCF1T~JNv8u9YyC5`kyrVzoY2?1k?YgCpa00N)y2SUs)D;Kqe9Y7t5FoyroO#YeJ0w zlVvLff=&O$vP)TXaZFS5he|YqI!;CCKzf)s1-%s*NASbTB(Vv@?u(`a_w?^*ws3->5(Pd*5LgPmO0zOg; zVu(9b8Z0)Vb?m?bgY!{TB0C&W>wElw+JHQ_J&FiX?)3jqAWP~Rgn9~#AI4(+Mu9ev z?aX*jR*FZHnxR6kXk9CMBAn`wb)I$wGT)#_2q$dk9p|@XBJKa-SH`!Xy#$2Pe zCQ%8te;&I=9a0esa#My?GTlpFkau!+h7dWt>MPevZ~zO(1#gKHR$GUPM6`V%0!K;= z+I}RPYe$VLdx)1Rp3k`4pux&$DOFLq5<4e{U!xgDp$45TI~g!2ChS8K!v+q3-~+Ir zY+#LY3J&w0Cg->QU0woO^ zI-@Z)PL+ZCPj*)nJRUb*k=4RZmQi{af}(=eAd_Ho1GWD@6x$|b;fqW1rDlOyitWLi zI-~Wns3%&|4Id3kRuK#Y2vQQyQTZ`#4eagi+@!pekYL(6Ic+Nd0jUfz`Py&2BebCc zFGW`iLg+|USgvvdQ*jyVHy5QCB<8Cvx;oO&A>@GwTqJRr@_<+f8hF`L~!^1X3t z2#f)mfo0O4wTmuOx*p(bASY}HIO6iwhFOO5jecp}pO_jP%4{mi}z!`mx{5K3sq;ytNSX0)wz;>bPffxagh zK-A+66#(Cdy}zAsR9M-S|g@-r*8xq6nqoSVQnsyRnCF2nJ`_J9g znk=^NZq##VXs}-P%BX~}aa^Z9tOn`K=7M8y!9YWM6SBf zq@}o2sBD1r;5+qJ5&0WsUj=pkAF@=DlHr!HI@dxa2MPi0eu55)jrA*aa(QE!3&R9YlnXX3PdLW z;G$Uhk5At@zLwdb1q1Qxo38P%eNzH>ngQRD*@uhAW#5^$XN;3fP$~))G)#e+&XV$3 zgMf#&M|aQIA^Am#?4h;J-p<1Px}fA4>$e3%LsH5YZnAFciX@l?)9DA#W&Wstry$i`p_(PC6B5ue~}r^Yg0byvYT8Q5g0(1AjThB&~K)Psgn}VlM0Amyz7t!w|XV4 zoZ^fpYRJNeQdohcN1(!fsla;_>-T*@%@Z-#Q0rvCMLM|6Xg|q6-Mi#-uB|G7AqFtr zrqO>DKJwH}Az8%Jcz@re>nTRs;>@=mgTg2k-TwiPQquZqHd&i=)|etn#DuU+m}V7x z6m$*0a^gRq;VL+#q|PdN-D}l?Ei5!4{$j1R#o@K=X`5sw>KG0_y_DWW0h=Jd@tagy z^US{KR^)pw4dO$mR;P~*31)D-rU(ATX-{rB@>86P`pz?*I$uQB7w^AJJ1>8HZ-+Ax zF>167l%VW7*oDV{s}4r+9Ap(!4W5559#8E^lGrH);4my?xtAV9Vy)OOU*CS%&_wjL_0!Yrbx(z`A6_CRkNeTI148Kg*)=N5209lh|}kd6i6Us=V^u0 zW=oyv+U+)!xP45p>(oM^DZ%6k{&`alO$KA&0$pR-kYDXsROT`cH5aPBT7jWBTkIdX zZW_n5@hH{2!8r_xA6Hb4jS+jZL8%*lMRqkPeb|{E{-tMV;P5B_@N3Mhom9g0kV(~` zGmZvJlmMggTwQ5g zl?}(uY_*gI{Juiy!X_X?%JJ4$Frj`-^Ulw=@L#r41J2UkBk{KcOe@snFREhdkj-MO z_~&K>ZS%n9n}w?uOK(d)j$8N)^Vz1x1}v1if8{xJU*f|E&Ee3g1u zmY_+c^!X!f@Stuh=weZ6Bopi9!9L0i$HCj;3JcotZ;{IQCn&tVI+g^GC)z`j=-qk# zxS!=JHXh_`6b9V=L~ZYaxn2EiQ{Jx0X{yU#I{JeoJ~V4ujfO$p)fe_4Q|_JI=FA1} z=aSOg2C1-1N&rFN$3;22!Rc>bDZ_m|8AH8)y^LjgcISc6j?U7r7_M((dS=V)DrAdJ z*8PVF49Fa(k~OGXRq^0O3bReuU?+RstpU->fvESEB5PMVVKiYdcNY}~M0Kpk0O^($ z+6T$vS@7!nNXWBygQ^J+BIVBB!HxU}j=Ts(cmp<4Te z40t1<+}5&p|8zkW?0{eKP#A+c9V(cy22R~AA6YJUkt6EwxJ{jl;v^-(5`%vu5+%7B zC4Ckp%MmS~02epJDTGC%^P|*eyl@xtmA6z^|M$UN1O{f&ecB7JuJ>MKhC>5&UZD=|12($BR&`% zA7URL8WtaxA0N>jAGsQT|12KQk?`AqKgvEKE-ZoH&_AI&A!RjzD9d{LQ$iLxF~>eJ zH!LwfKe3=Yv1m21_$-mgkyM6Gs<2P03QMZVzc1IG6qk{d+jy1~GLck|NN%@J_7P2% zb4~8;PIgI1eln3v;z+UINfs1M84F7>Fie@~PMOY68CXr3=SaO&ld@=^y225?6qdTy zo%$y)_03u8x*`2Lj~nzsss>fYdBpgyVDZ0I<6JWSr)`|; z#kpsk+s3(boO{N(Rh+BFxpth}#<^;o3&*)zoJ+>JYMeXAxp|!1#<^sitH!x-{QsdE zDAfN$Hu>+W@gLdbzpF;Xv-RfxQjPyN*`%=Oad8RpKe9pYwMLk6HoF2iQxzu83GapF9i#4`vO7a6vc{r7D0zpDxP|I8*W0Q%Ne z`{Yr{Y>8&{{_k3=nuWV+wDzP76ltOQC`22D#}TY*?BB9VH7nHH-M-d$pZl7$q*<%y z_$Nhx`1w_P;L{Hwfkdr^?y!yzl7JRrxhouMH`N0GcBCj~9fi9#EgSABye8MHjsUPF zTY>q-t95*IRe=uuZ!aR=W})T*<7iRuk0;9sPE*D3zHeXtGn)jI!x;b!sS_Cm0{lye zOZ{j7GGT5g>c~$x;-%(yWGCRX(?3fg81R>b?*>7ir;|IC@c;lW)G}}!_Ur#X{We)L zZ|*XXL5a+7CPBV^Ch<~J-iNG{5pcvI&%Hg1TfC7%h5!#0g&>0t$drR4#R|CEM?Iih zly$yeb=zJ$ioUoRpTuEI{6LLF*bUti4qT0jT}manASJ(xqEZCxPqjgXf@e_RX!%4(mF#haak3& zfwWCR0Z@=D5EBHlc<(%lV?&k#wD@pL76&ODC=^7`lc*3d`yu?@fjsw8-;ZZpG4|e* zl_iO}wL=6dE+Q$H;|x(l9PJU?36q!>s{xIa6G)}Qs%kb=kFw^8kF50tTtH1*pcFDH zx6TgGb75D>Egsru?;!LE439yYgk~TZs-5qi>C0+Z)hRCddZWN2Zm+y|;+N`Ipg`I1 zZD?RI5$NNWT;=V02cqf#1~rK&h}l00LxlOx@ZDQEOeh)XJ_I1tiJ&oJFgjf$jI-|r z6h4sZ*ktILV9M*2&`DbzLcgH>R{5P(eiHyLv}5uGq8A()1%b~0j=EoV$>1R8l6E%` zPRr>RSj~_7T!Fxzj4M!4>Y$H?Ee{M1;+dKo+7W1^>=sP>X3dT0(ZMTGBuFA35{j`kS zONGX%EjNf0@nMvboNw<58{{2PUHUDiK3*pLIQ-ov%e-e-+U1vD*Vov18mNre2hmjL zHMMyYkd%c$!s@ZVpBG#thi5)hXxi_FvycJ^3|J%y(pdiZ+iO3AS37r7mya&d z71-ZE;wL#*{6nAqHo-o}tu!KHXtkPrxRNO8o=DaGzHkUoJY-YOA-FoVqk$`T~hiYSgi(pKazwe(-;zEB)qv` zCe7;19CNN56QnRy`!G-3p{B!T8Ipsjck!DXl%0rwl}ib9I%AUR>iA#M1rm570z*9( zU2e#C>a(wl$Ipb3tPz3ZRHkl#rZLG{n8>rwF_X=9iw>+B0_td=c%CF)t7bdILjW2` z=bw)-*(xchB1VH|brw(?#V|Xj!zu*_k0mKhy#2nXE!0=Cnqy5aO0DPRN_+xb!z(Fh z@O~WLTL=ae&$^W9t#)5PTFl0jgNqcE-;nsxfpnR9shJjmE}|Eom&#_8g+ENGzOr=z zG2q)0tA;1>UBGK`2$CLxk0+JcwWb_Th(IUIO;_dlLRtj*xEw`WYiMA+m{rgYpQHhA z*%P^0h}#IN)sIvT1w{{}*VSZhe_qaNUSyV?qPW5|f0mS$_=ktFS&dld0f-fBp$x{+*~*yO?1c-)2im zv+NgYW%a(M4W9}ZH=@T{QS;l>Xo``rbb*U_)ckZL8?qRTuqw^{B_zo&ZShMNLh)`1 z%_*GV2W01}*~k{_-ewEmFP*pknX?88&4pdgua|qfw!E?kGyu_0Htjd@nGk%s*V`ra z@>3-~yiBC0;;L84$`d%26q(OFhu|RcL9NYTWZHb&j+&b$n3i?BzlHWTC#Q~XW(%%E za)7o4R<)DkvvGWfWZV$@z{e^8q|d)WCCu|$)OOV1|`}4-uc&)b-SLG3(?C64GTh)BYT_zL&$iFHG$%R;MJ`w@SP?= zz}Nhm-a4MqEEwLcf%Tw3J&`wx2sW{~+W1DnVSnA{5`#DE&ELO^dKJA*{}4FyDdsP1 zZkqa2+dI|Ndw9G5X1GQ64lU~`tn^}!vucQIc#s&nHtB^-w7;2`e--ss;r5D2POD;7 zrUHO2_nUsMmkw_n#+UJZwRtzdxb3hU0IuGJP43-U;$T5lz4~tchz+9SSG&fD!Mn3} zxjBugeiqd!fH%$9J>rDDg+S&?gb$r*JQ7F@KkRRwomliZW6>1YB)=Om33TA5^Me)uJ z`gn~EfJzf7Z2Z`Q3mEoaff2n5yiPeq{xL)MUIl%TFwK*~5y)9aQ}KO@30*N{!uV8ZvNE{uB#+tAcam75Gi5>%~C*f|ZqppPxXsls|LTQ9}57&4m(Fz+4I0za&Ho3bwQL^h{)K* zs|fK>HdRWcdPtH3`FTF_as6BZnDO4Z9x7KUG*zKydRR1wGjqMIzR+TvTkc!PeuYgy z1?Pf4YqO`v=+hx24_egt(JGG~s_zp*?`(v`GNBo9e32o1)L6*88wZDi=5Cz2=qlSY z-5}u%^RsQ|XBBuies_fg<)XG*^jHX+3*_rKo3x?(){!C$Dk`Gd%~BL%-5oF&1UVQ7 zbc0dDL)YzWAOhBa-^l}I27H#l9i1(GHqY>)>^KP$Z^zMXT`bz_yJ9bkqkpzENOH1b zi3W6~KIesJw?#|bvQZZaDwPjD6OMA$1{8(>7ToRE?6*!}=CoOYA}c@!TE%=qVN;Oj znl^+yB$o&KEFM{YV$zEs#S5Z0wg3akMieH1iRzzbp^Ev>R1 zWtYWtdBc=ZY4*u5DDY1_YI(wcx5~-&huj4#uIpKHw4lamyNsx!)IvEQsFS*)AQ#6d z^pekg(?tO-0vc?BWkZQ}ZGcx^!u=X>>6?Vm1{clkn2(FXdzFH}N+V%sYMT)MI4dw` z+|EqiF2pnB-G=gtYsOuJJJ+>SaxGIpCSid@$>;WhNG#;99l!5QHDa7{acR_;r<(8W ztY?cj@2>dQ-C*p7qrzCkeQgNqVWj=m1rP%X%Fg*)onv+;OfxTF;Hn0L=Cs?TlEri% zZVe}@@Ef+PINdvnjE@J=pj{%U7V<6hG&Hw<5ht~TQV&ZjrKL+v1;~rk%XC_HPwxlY;1z*caGs{XZIdj5M1!Gy3(rtKCCN7IyfEic#(Xz07 z60FQD7;ELez^_?RUMPd&U$|ND?HhL2k`vkqQjm%&!@2A>ifX8eMlM`Z7b>J4mMrm- z)E|8`n+a6)h&`^gjtKV{{J{0`l|tEP`5tX-_M;NZL;SB-CFX&T{xPy4Ur-{}Um#LM z5D^7LWG|6sjY#*4$WTW+t;MB*6NMLW#aZB_h}^0jxNFCyQa>(;V%1zwWg0{gY@m&2~s%&W^`|j@~SuV9@6MR0f=4{-(rC#Iv_lfDAJA9 zZz|XL!>Z*YU>PGYdfBSzbsIVoJHbVua@=;oLQO0Ln6XbLGcafuZwh>icaa9T3D4zN z!|a{c*s7w-ZEo57!hL7~(W}hKkw<2cN)lc!k1m$iNjBH#S*dvlRZy(kQIhn0qUwz- z>mC^BD3p~cPiFNO2uOLU{d&O@GpcuQRK?;EE>s7|S@aR~M#q+2My%&<)`2ygqV;O3 z2#h^L*^`CD`r+}QG*}HdK_|qvH$|hrR-0mTQ;~g?A_bITJW@1UJqys>4VstINq1gjBX%Z;dNAGT}A< z!8771I}gf68ADc_y=xE|Re-3mJpq3SR^t{f-&hXAdfIdT7 zibb@FPfEs`RZLg(JFA8SzUH=;$%csj{Lt;B0y*J$n`)7eiJ->_BRwCc?z_QWGz^!9 zK1eKqSz`s-blqwnba-p(dz*IH9YCP*jO!T{_vVr+^a!91%+P)*<)YjRC;|l%GD|uG zTR!qT*R-vor08up@-J#G)XOhNaoyjmA$MKu`>5_@>;+u=u06Qf-+|DC2KR{TXxcar zkfuBk?4+N1Q z1x|PF4u4BFP101r-<~VI92Ocx$iCEK+;%a3dPqZ0$(Y#XxAqg+QwUBUmg!>Q=V3SGOBDO)WXGssf8r>&t+ND&|&~MTK0r zCc4j;AOj0aew8H^h!V4ga5NV%HrtA4S8D2`ziS)vhKzULRGEX0_tl}#T>K`f`BpzV zX198TezAVhV1A}Ra%Zczvj-1+MN2>OOuNe-6DBxj=8Hiz_0t3{pX+g1a@<#VxHX0H zxis-EqO_jl=FrB6BGA_ggJEp_BW;<&Bwl*W#cvDGQ;$^XpT@UM%fVoRE zEBa;EeSdIp!eBPu=>FcTV2;IFnY;%`ESHz)+&itg(;~(Ag1IX6eDNzpNp7t?7?x zG@fY(I2O%X>Ojm)K-Ux{@}7XqkEM{$p>u9=W1072<^(*XP~!dSLJMDmzqM49i@pN{ z*X4D2WfM~KW)~3qy)@}}T*)6rj_cE$(%`z3 zG}HWBIVTyZV~H5g=f2Om6))*BPxFeUWt3!p(Di?LdzbIXHLxpX;FR`_html2)lbs4 zZ=G!TUzfiZv%|?OzBS1}EeAtNsoh*HAru*qJw1R9CE&_7@*%UEQd%_`83*F{_G7m$ z0O=wq_a(j9I8RRNd^x9Fh%Zm(!HA^riZt8e{Xm{%?pBplZxC?drs=punE|07J#=ji zh)Pq+eW8;Im449$lFhB6?QLK4!yw#QkdL7&!2(j}iIegT8#_t%fXf)f#A&agyl1x{ zv?;_(_p2K3UQA2Me<#E1Z^Bo+Aj1!7tmdzMa-9RXdo30G>8k${pUSwKTXM@1vBtR#JYQc(XD7_6T;^E|xFp~T{j9LK&o71l20^)7 zb19$2mW}T|po-fs2FbI!MlSMo?}U4bxj+3K{5^&hFm>X!)v&D?^10O2q_jNmEdL-I zvNNFR+ZIssd*4I)V7dqv5+Sr9Cl+TfmEM2ZD#+%-tMcMqtH+z*Kr9d_L`|~a zx0NiJs0&AB{^G_*7HjU?RZbK?e%<`}yx^zV&C?H4M>u}373;H51x*6jZDHAyQlcfQ z^D|HLhoAPF$MR2n&42!SVdohoKs<2%`r)KVGKzEYq?Y_UE#b$voV%?p@i%^?N1o-J@NfoK{(*~BrvFjq631D9yPL7kgIRNB#uPzWLGlZdD2I(n4Z2s``g!v= zc}G|8$_BN3lM8PBE`O`-NFk*CSn+np52et~6XiQyrz&CHXXx;rpQ;hPztryb{Z^0c z|D$p5>0izJ1K6mTk-eSEEYyO{vhyae%MV1DV2)$+i^G!8R4$r_&82!NLc&`a^Hqgf z{uIB+He8Isu<<}KbDA&XS?jI$dEy^$j_r~jGA1~N1%XB*Ge!KNSqVfr77IP2AQlGw zvg#BOVFngYmZeooK)-}-l8?A+RsEvEKf7XGgsPRF7Lkh&X^Z3CG(BH z{74+aN1HCSf(ffpjgwgb;v!GxDTlL4=8YPm7N8-eKrI$oK-2PJqa)ah?SA|JtWfQtr3blz?J3C8hC%%b-JyKcq@0+!V(LHKt-WmVRoloe~?Z%jP(Ni>QE& z&iwwnCqjyv8hRT)XP?101b;z8%k{v25bqKF#X0S8;?h&Z(bU`FbjF}h7_0UweYha` zlL#|rk~W{4>aR5kbJk!9}4xiW6PW$feE6}~nlzux&wbDf> zMYo)wVtJJ}On6;bBtn)>Zp}rGCS->hMOPgKnlX9_YO`@6Nfm_I^Jq-TK*ZQkabX}3 zqos@&Hr0=p>u|c(7yyohx#ap~<}&PIVF;4AAPq(q1SvB_hMT*2=BLCO$UGzxg0-j; z4;D>E_`wAchoq}44LCfwXvrT1ed(@lO@n1^-mR6$cN2$14JvU$m%vjf(7!Uj+fXa$ zlLeG@;w7$|N&UbB-Qs{0Y;l(w-%;I|tUb_g0e*2AVv;C;R`aZjUeBMIO2kn#JO}Ef zzbw4~SF}k~t;ak-?sJyrHxf6h%ani?WXS6%`XVk=KBA%^$=ms`7fI?X5?#m4FkndS z)HOfrJ?th=vKkiJrDCKS$3a=bOgJFhFyCW&R~$C1O_T)mULad1`jkxx(2U~sm8Xue z4WtQ#s+8?qO!*s!o+riBnA&PvSuTG$30Z|XX-NzPV-A@sBff4i;v=++*RL2e_T>vB|jJ-z5#Elon^k@OMp(>axgkhQyNKN4v>e@5fF@HXFc-Z3z5VY zwF_)Btxz)z=L!{5$EImtIi0N55G;j^I-bqKS5Y9}#iUoZ2vu&*1r`7H zf}6qofb*(cvYsF>TyKM&xqH-x?+>L^%vST1uqSwx~IgCBDs z-y+H5NBnpJT{WuQ`(bHN`)d9H&2}7$g$_u&Fve_Mb>GNGC}Z}>V0tyvr)pB5Mw*3? z{LL>@bNvyG%Fn@jjJ^eYEU(oWzYp?<4N@m`i_ErD8TO#~GoXhT#UN^W_Zv|e08ky^ z+h#N_swLHM$mW;kLGT(NGSdJrzY)n4R~R=J;-5&Yv?y0Ad*$LB$g-l^K^dgDpeW`c z)H|UnCHw>1QKxdFNKXBbuRwlGmi{AqNfrM{Mv8}OoAo%R9rgN6QPj>*H92I$7l{ zC=nUZKRX5NrfSPnaK}$;Be_eli+Q!`wV!{vAT>(*1X@LGr|c?}|-+RKLWUg0D~nu_B4Pqd4H zL+m1!6puY0&3@D0bzvX5#j=;O?Nw5@Hfmj2xGA(_RUglU%5`tFo%PG{!WaC#WH*+f zG?{dpVTIS87MXB?bgvY6n!8n}E897J`6xp~ZgDG;Nh6hx`OVBY(;zb)g2#sz?R`VI_)oK#luhb z2pJI$G^?T4GcMdLW=u*dTsoLdf@{F7jx`EPV)JA0+>xX|O3uUFrn2O^Z1MYR$lSwPoc; z_nZB8|IEX&po%LoqP?y5D#o8l&|DFGT?0ntEu~gm1PI{}5QejXxPt3%Iu+?vw#GId zR^$!`Jsee-GN#EJp4svu*AS=!(mYviU6p>l58Y~DmZ4-U2zwY_76aydo>>uZhcMDGJ zp0vR+Jv1OOBhWp-pM(qjWqRB4?~&Gp|0K=uT}(~57e1oDaVr-JG>K4p#3qNS`quuq z)BLA>KCw8;OAnLt<8L9a(eg=0{JoGXyw%<38*SQ}gua;F5%4_UbLsH%>(Li~!rSmP zoR5e7mit_2t62K?UL$tsrzFDuMQMgoRT6W{`y|=FdA|_N#qa~p2sVK<2u-Af*GgVj zlf94g>la0S<) zS}q|M|9bRhKQm2Wa&RvZpGH+MwVVXj@cg}}JIT#MPlM6nAequ)j5q)gi$XZ9!+$LU z=f5-r65$RgO1db@Vw{gJ?m|A1a&{EzNHBbafB6(o1!lf>9;1@R&?v{Q7Fj`J)&pyZ zM$RjTs`MAR|N;?II3iBx{l$IBk8glLB?J z;#EOBKnw$aa6we1NTv_mvrAgOL`0P{L$PzXDMql2NZGkgYmbxRazUt(k%& z=XF#F?peF74;++8Ly7TIyW?Fh)-7AX_*t)`m_C%ApIr#Xyll=W@lwRVC^T#cWZ@^s zee{AwA$xH$ilcnW0@Bx`m^euMS%4_Al-Z^1(gafM_CwJ>acY5_WTP^n{Moi50nL?i zb#%6-tqyoR)7&v8hXmD&hcB%V-ALs^2vUg{X7Q7Njh=up7ozv?fl?aqP0~ zCJcqq*RL?RFK8H7VVEXplv`m`ENEO+VcaNa(ph0LAZR*PVLB^#Wu@ZEJ3+Ip3bTDd z^OFkm0oQMjNM&##)h`0nFsLHRjajrZT8@*7;T<)HkSYzil4=5xVei(!SecdsN&iz} zHzxQsRU5LCC4Q{OHlp9csN*n8a+rl<66ebmLF@B9;0d&nJ@&0jY*GZ+_Th@-K-nc$SYnJL7#0#PZ%4^-jq3+pJ8%A841 z2BA?oQVFerr3UFY2Cv2Q35vteIZ;A4=U-M#LC~b(naWr`3%|7?1Z)_#BP>Lsfg;AR zYI?G0CTn2;M#F9O5f9&~f;wUX2Oj(L#N48LEBhhJ4;(Gi0zfoC(VXn7O?o$31?5-+ zi?)4fwy|+A4uZ`R_jTFN=w&qXjvVeP*{DG^(Lo ztBI*Y8phmE#zFevEo7Lmr~i&>F?cLXA~no(rBqr8$Eu%X(G@+1I#%r}Vj!}n)WIBZ zz(Rb&e9$r7=h6xmO^Whcw;H8^M(TpcZnV~RkfNmBrB81Z-Tz?qS~Jo!`Ne9*#GDK8 z&$~raq~ky$EhKV|EGqV8B*mvJRYaHO;?_e?4H7J*WY~fPYw)7MuN0PfmHT;Fg@N~3 zi(+Ry8QgDf0~YtK@jk*PJosFOQe#}LsbK4eGp9l5{3MVc7wi6zMvyN%ShQ*}`s9)A z;tusVqr+IothQ~=K=Xp29C>cU`bCQ`-_jR~e=u2Qq&!bRta5jR!tWvKQ+yu;a&o z#>usF&DnRW(p|h)ezQvXYW7==@tXSH6Yf7OSw5QwZi1G_QbSCbAG zmR(F~EUrwu>?THQI}8q41UME`^}l(_DUtmui^nOIchD*KZS~h~9~->w*gqDa=U!3i z+Y;Y;1p;q2hHdOuraeKTW7?WI*2#`%sM5^axvG9PZL`Ew7=m= zgBMj?zcyyaLMo=UQodBmBNN|^A;H#{$Ez;X{IdhOemzzR0|_ssaeVkp-@!&Bhl|^E z=aH1d-;v(tIQ4(5YCaiUr|~NdPmhzIGxs$can@b_pijJ4zrF>V6=wDlmEO42q@(7a zJvqpk0d&J)YYm*8a#f#7Y3&6`(knMUe9M2^Ii_9v+(azE4t6!lCSTVJ?yQ|jElACE z@_yNSX|VR;DL)zJFmr7bIW^dUn&-r?i3--_G@*jhe<m{(nX0k6%Ytt~ zG3c=9?RK*RMf1~kbFw0qvIA@4&aCnc%d2E5qHS^Uwxvpk z6*KvVTH>hO3-@R?0-f`9Z_SKRO2Z7rTO&!;u zx34|xa6AZcoa=C!3vqhialI+z`a#Ey7U>F>QMskkb?dT9h(T9~xyo(3 zuG=?MLOr@d161zZ>ALekB`m%xEL|l$uPeMnC8D}3qDkd$SJ&NVDv{$|k#j2dUUuDk zuX2Bz+;#sz<-uv!1F{O9vKvp2Ca`xCc+pWJ-BB{=XqE2h%jg(`?ih1)tX+5P4RoAG zcU%BE{!VxN19U=scS1TkF|RwZ1f5jfoz#R*?&?l{hE5spPMJfezU)qYk51d}PCG!S zpLVB{(HWFI8T6`|>^+&hs#zjESu(2GDm~ekRdWn_a?Dj9+VwoVp_=Q_lN+F#cc&-s zfogtyPky@Uqr9F+C8`D0Jq1mwg?P8xm9qDi@~V}I^p?q}m8`H&^el>+86o-s#cT8KB;Er?2aQdUt$Zce;8{USCg% zdT(`KZ Result { + let source_url = normalize_ask_page_url(&request.url)?; + let question = normalize_ask_page_question(&request.question)?; + let assistant = execute_assistant_prompt( + &AssistantPromptRequest { + query: build_ask_page_prompt(&source_url, &question), + thread_id: None, + }, + token, + ) + .await?; + + Ok(AskPageResponse { + meta: assistant.meta, + source: AskPageSource { + url: source_url, + question, + }, + thread: assistant.thread, + message: assistant.message, + }) +} + pub async fn execute_fastgpt( request: &FastGptRequest, token: &str, @@ -732,6 +759,39 @@ fn format_client_error_suffix(body: &str) -> String { format!("; {trimmed}") } +fn normalize_ask_page_url(raw: &str) -> Result { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err(KagiError::Config( + "ask-page URL cannot be empty".to_string(), + )); + } + + let url = Url::parse(trimmed) + .map_err(|error| KagiError::Config(format!("invalid ask-page URL: {error}")))?; + match url.scheme() { + "http" | "https" => Ok(url.to_string()), + scheme => Err(KagiError::Config(format!( + "ask-page URL must use http or https, got `{scheme}`" + ))), + } +} + +fn normalize_ask_page_question(raw: &str) -> Result { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err(KagiError::Config( + "ask-page question cannot be empty".to_string(), + )); + } + + Ok(trimmed.to_string()) +} + +fn build_ask_page_prompt(url: &str, question: &str) -> String { + format!("{url}\n{question}") +} + #[derive(Debug, Deserialize)] struct ApiErrorBody { #[allow(dead_code)] @@ -901,14 +961,17 @@ pub struct KagiEnvelope { #[cfg(test)] mod tests { use super::{ - ApiErrorBody, KagiEnvelope, normalize_subscriber_summary_input, + ApiErrorBody, KagiEnvelope, build_ask_page_prompt, normalize_ask_page_question, + normalize_ask_page_url, normalize_subscriber_summary_input, normalize_subscriber_summary_length, normalize_subscriber_summary_type, parse_assistant_prompt_stream, parse_subscriber_summarize_stream, resolve_news_category, }; - use crate::types::SubscriberSummarizeRequest; + use crate::types::{AskPageRequest, SubscriberSummarizeRequest}; use crate::types::{ FastGptAnswer, NewsBatchCategory, NewsCategoryMetadata, Reference, Summarization, }; + use reqwest::Url; + use std::env; #[test] fn parses_summarize_envelope() { @@ -1121,4 +1184,68 @@ mod tests { assert_eq!(parsed.thread.id, "thread-1"); assert_eq!(parsed.message.markdown.as_deref(), Some("Hi")); } + + #[test] + fn normalizes_ask_page_url() { + let normalized = normalize_ask_page_url("https://rust-lang.org").expect("url parses"); + assert_eq!(normalized, "https://rust-lang.org/"); + } + + #[test] + fn rejects_invalid_ask_page_url() { + let error = normalize_ask_page_url("rust-lang.org").expect_err("url should fail"); + assert!(error.to_string().contains("invalid ask-page URL")); + } + + #[test] + fn rejects_non_http_ask_page_url() { + let error = + normalize_ask_page_url("file:///tmp/page.html").expect_err("scheme should fail"); + assert!(error.to_string().contains("http or https")); + } + + #[test] + fn rejects_empty_ask_page_question() { + let error = normalize_ask_page_question(" ").expect_err("question should fail"); + assert!(error.to_string().contains("question cannot be empty")); + } + + #[test] + fn builds_ask_page_prompt() { + let prompt = build_ask_page_prompt("https://rust-lang.org/", "What is this page about?"); + assert_eq!(prompt, "https://rust-lang.org/\nWhat is this page about?"); + } + + #[tokio::test] + #[ignore = "requires live KAGI_SESSION_TOKEN"] + async fn live_ask_page_rust_homepage() { + let raw_token = env::var("KAGI_SESSION_TOKEN").expect("KAGI_SESSION_TOKEN must be set"); + let token = if raw_token.starts_with("http://") || raw_token.starts_with("https://") { + let url = Url::parse(&raw_token).expect("session link URL should parse"); + url.query_pairs() + .find_map(|(key, value)| (key == "token").then(|| value.into_owned())) + .expect("session link URL should include token query param") + } else { + raw_token + }; + + let response = super::execute_ask_page( + &AskPageRequest { + url: "https://rust-lang.org/".to_string(), + question: "What is this page about?".to_string(), + }, + &token, + ) + .await + .expect("live ask-page should succeed"); + + assert_eq!(response.source.url, "https://rust-lang.org/"); + assert!(!response.thread.id.is_empty()); + let answer = response + .message + .markdown + .unwrap_or_default() + .to_ascii_lowercase(); + assert!(answer.contains("rust")); + } } diff --git a/src/auth.rs b/src/auth.rs index 6809cd6..6339992 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -183,11 +183,14 @@ pub fn load_credential_inventory() -> Result { source: CredentialSource::Env, value, }); - let env_session = read_env_credential(SESSION_TOKEN_ENV).map(|value| Credential { - kind: CredentialKind::SessionToken, - source: CredentialSource::Env, - value, - }); + let env_session = read_env_credential(SESSION_TOKEN_ENV) + .map(|value| normalize_session_token_input(&value)) + .transpose()? + .map(|value| Credential { + kind: CredentialKind::SessionToken, + source: CredentialSource::Env, + value, + }); let config_api = config .auth @@ -205,8 +208,8 @@ pub fn load_credential_inventory() -> Result { .auth .as_ref() .and_then(|auth| auth.session_token.as_ref()) - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) + .map(|value| normalize_session_token_input(value)) + .transpose()? .map(|value| Credential { kind: CredentialKind::SessionToken, source: CredentialSource::Config, diff --git a/src/cli.rs b/src/cli.rs index 5b86e4f..5578816 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -81,6 +81,8 @@ pub enum Commands { News(NewsArgs), /// Prompt Kagi Assistant with subscriber session-token auth Assistant(AssistantArgs), + /// Ask Kagi Assistant about a specific web page + AskPage(AskPageArgs), /// Answer a query with Kagi's FastGPT API Fastgpt(FastGptArgs), /// Query Kagi's enrichment indexes @@ -274,6 +276,17 @@ pub struct AssistantArgs { pub thread_id: Option, } +#[derive(Debug, Args)] +pub struct AskPageArgs { + /// Absolute page URL to discuss with Assistant + #[arg(value_name = "URL")] + pub url: String, + + /// Question to ask about the page + #[arg(value_name = "QUESTION")] + pub question: String, +} + #[derive(Debug, Args)] pub struct EnrichCommand { #[command(subcommand)] diff --git a/src/main.rs b/src/main.rs index ce19788..8c5f7ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,8 @@ use clap::{CommandFactory, Parser}; use clap_complete::{generate, shells}; use crate::api::{ - execute_assistant_prompt, execute_enrich_news, execute_enrich_web, execute_fastgpt, - execute_news, execute_news_categories, execute_news_chaos, execute_smallweb, + execute_ask_page, execute_assistant_prompt, execute_enrich_news, execute_enrich_web, + execute_fastgpt, execute_news, execute_news_categories, execute_news_chaos, execute_smallweb, execute_subscriber_summarize, execute_summarize, }; use crate::auth::{ @@ -21,8 +21,8 @@ use crate::auth::{ use crate::cli::{AuthSetArgs, AuthSubcommand, Cli, Commands, CompletionShell, EnrichSubcommand}; use crate::error::KagiError; use crate::types::{ - AssistantPromptRequest, FastGptRequest, SearchResponse, SubscriberSummarizeRequest, - SummarizeRequest, + AskPageRequest, AssistantPromptRequest, FastGptRequest, SearchResponse, + SubscriberSummarizeRequest, SummarizeRequest, }; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -139,6 +139,15 @@ async fn run() -> Result<(), KagiError> { let response = execute_assistant_prompt(&request, &token).await?; print_json(&response) } + Commands::AskPage(args) => { + let token = resolve_session_token()?; + let request = AskPageRequest { + url: args.url, + question: args.question, + }; + let response = execute_ask_page(&request, &token).await?; + print_json(&response) + } Commands::Fastgpt(args) => { let request = FastGptRequest { query: args.query, diff --git a/src/types.rs b/src/types.rs index c9b39e0..1a70398 100644 --- a/src/types.rs +++ b/src/types.rs @@ -240,6 +240,12 @@ pub struct AssistantPromptRequest { pub thread_id: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AskPageRequest { + pub url: String, + pub question: String, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AssistantThread { pub id: String, @@ -280,6 +286,20 @@ pub struct AssistantPromptResponse { pub message: AssistantMessage, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AskPageSource { + pub url: String, + pub question: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AskPageResponse { + pub meta: AssistantMeta, + pub source: AskPageSource, + pub thread: AssistantThread, + pub message: AssistantMessage, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct FastGptRequest { pub query: String,