diff --git a/README.md b/README.md index 66c752e..e47ca10 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` | @@ -139,6 +139,7 @@ for the full command-to-token matrix, use the [`auth-matrix`](https://kagi.micr. | `kagi summarize` | use the paid public summarizer API or the subscriber summarizer with `--subscriber` | | `kagi news` | read Kagi News from public JSON endpoints | | `kagi assistant` | prompt Kagi Assistant, continue threads, and manage thread list/export/delete with a subscriber session token | +| `kagi ask-page` | ask Kagi Assistant about a specific web page | | `kagi fastgpt` | query FastGPT through the paid API | | `kagi enrich` | query Kagi's web and news enrichment indexes | | `kagi smallweb` | fetch the Kagi Small Web feed | @@ -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?" +``` + list or export Assistant threads: ```bash @@ -234,10 +241,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 0000000..c74e157 Binary files /dev/null and b/docs/demo-assets/ask-page.gif differ diff --git a/docs/demos.md b/docs/demos.md index 4af039d..912be79 100644 --- a/docs/demos.md +++ b/docs/demos.md @@ -9,6 +9,7 @@ The demo scripts build the local debug binary, expose it as `kagi` through `/tmp - `docs/demo-assets/search.gif` - `docs/demo-assets/summarize.gif` - `docs/demo-assets/news.gif` +- `docs/demo-assets/ask-page.gif` - `docs/demo-assets/assistant.gif` ## Regenerate @@ -20,10 +21,12 @@ The current demo commands are: - `kagi search --format pretty "obsidian cli daily notes workflow"` - `kagi summarize --subscriber --url https://mullvad.net/en/browser | jq -M ...` - `kagi news --category tech --limit 1 | jq -M ...` +- `kagi ask-page https://rust-lang.org/ "What is this page about in one sentence?" | jq -M ...` +- `kagi assistant "plan a private obsidian workflow for cafe work. give me 3 setup tips and a short checklist." | jq -M ...` - `RESPONSE=$(kagi assistant --model gpt-5-mini "..."); THREAD_ID=...; kagi assistant --thread-id "$THREAD_ID" "..."; kagi assistant thread export "$THREAD_ID"` ```bash -chmod +x scripts/demo-search.sh scripts/demo-summarize.sh scripts/demo-news.sh scripts/demo-assistant.sh +chmod +x scripts/demo-search.sh scripts/demo-summarize.sh scripts/demo-news.sh scripts/demo-ask-page.sh scripts/demo-assistant.sh mkdir -p docs/demo-assets /tmp/kagi-demos @@ -33,11 +36,13 @@ agg --version KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-search.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/search.cast KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-summarize.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/summarize.cast asciinema rec -c ./scripts/demo-news.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/news.cast +KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-ask-page.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/ask-page.cast KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-assistant.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/assistant.cast agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/search.cast docs/demo-assets/search.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/summarize.cast docs/demo-assets/summarize.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/news.cast docs/demo-assets/news.gif +agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/ask-page.cast docs/demo-assets/ask-page.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/assistant.cast docs/demo-assets/assistant.gif ``` diff --git a/docs/docs.json b/docs/docs.json index 8285979..be8d5a4 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -84,6 +84,7 @@ { "group": "AI & Enrichment", "pages": [ + "commands/ask-page", "commands/assistant", "commands/fastgpt", "commands/enrich" diff --git a/docs/guides/quickstart.mdx b/docs/guides/quickstart.mdx index 8303e7e..0a03b60 100644 --- a/docs/guides/quickstart.mdx +++ b/docs/guides/quickstart.mdx @@ -174,6 +174,9 @@ kagi search --lens 2 "developer documentation" # Test Assistant kagi assistant "What are the key features of Rust?" +# Ask Assistant about a page +kagi ask-page https://rust-lang.org/ "What is this page about?" + # List Assistant threads kagi assistant thread list @@ -185,6 +188,10 @@ kagi summarize --subscriber --url https://www.rust-lang.org --summary-type keypo ![Assistant command demo](/images/demos/assistant.gif) +**Ask Page Demo:** + +![Ask Page command demo](/images/demos/ask-page.gif) + **Subscriber Summarize Demo:** ![Summarize command demo](/images/demos/summarize.gif) diff --git a/docs/index.mdx b/docs/index.mdx index d675b9e..70bf699 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -50,6 +50,7 @@ Unlock the full potential of your Kagi subscription: - **Lens-Aware Search**: Use your custom Kagi lenses directly from the command line. - **Assistant Integration**: Prompt Kagi Assistant programmatically, continue conversations, and manage threads across sessions. +- **Ask Page**: Start a page-focused Assistant thread directly from one URL and one question. - **Subscriber Features**: Access subscriber-only capabilities like the web-based Summarizer with full control over output length and style. @@ -79,6 +80,7 @@ Two credential types serve different purposes: - **FastGPT**: Quick answers powered by Kagi's FastGPT API - **Enrichment**: Query specialized web and news indexes - **Assistant**: Full conversation support with thread continuation and thread management +- **Ask Page**: Page-focused Assistant questions with structured output ### Public Feeds @@ -107,6 +109,7 @@ flowchart TB web2["assistant"] web3["summarize --subscriber"] web4["search (session)"] + web5["ask-page"] end subgraph PublicFeeds["Public Feeds (No Auth)"] diff --git a/docs/llms.txt b/docs/llms.txt index 5f99fe3..27c253e 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -19,6 +19,7 @@ - [summarize](https://kagi.micr.dev/commands/summarize): Complete reference for *kagi* summarize command - summarize URLs and text using subscriber or public API modes. - [news](https://kagi.micr.dev/commands/news): Complete reference for *kagi* news command - fetch Kagi News from public JSON endpoints with category filtering. - [smallweb](https://kagi.micr.dev/commands/smallweb): Complete reference for *kagi* smallweb command - fetch the Kagi Small Web feed of independent websites. +- [ask-page](https://kagi.micr.dev/commands/ask-page): Complete reference for *kagi* ask-page command - ask Kagi Assistant about a specific web page. - [assistant](https://kagi.micr.dev/commands/assistant): Complete reference for *kagi* assistant command - interact with Kagi AI Assistant programmatically. - [fastgpt](https://kagi.micr.dev/commands/fastgpt): Complete reference for *kagi* fastgpt command - get quick answers using Kagi's FastGPT API. - [enrich](https://kagi.micr.dev/commands/enrich): Complete reference for *kagi* enrich command - query Kagi's enrichment indexes for web and news. diff --git a/docs/project/demos.mdx b/docs/project/demos.mdx index 2e60804..e191be9 100644 --- a/docs/project/demos.mdx +++ b/docs/project/demos.mdx @@ -10,6 +10,7 @@ The docs site ships with recorded terminal GIFs from the repo: - search - subscriber summarize - news +- ask-page - assistant ## Preview @@ -30,6 +31,10 @@ The docs site ships with recorded terminal GIFs from the repo: ![Assistant demo](/images/demos/assistant.gif) +### Ask Page + +![Ask Page demo](/images/demos/ask-page.gif) + ## Regenerate Subscriber demos require `KAGI_SESSION_TOKEN`. @@ -41,10 +46,12 @@ The current demo commands are: - `kagi search --format pretty "obsidian cli daily notes workflow"` - `kagi summarize --subscriber --url https://mullvad.net/en/browser | jq -M ...` - `kagi news --category tech --limit 1 | jq -M ...` +- `kagi ask-page https://rust-lang.org/ "What is this page about in one sentence?" | jq -M ...` +- `kagi assistant "plan a private obsidian workflow for cafe work. give me 3 setup tips and a short checklist." | jq -M ...` - `RESPONSE=$(kagi assistant --model gpt-5-mini "..."); THREAD_ID=...; kagi assistant --thread-id "$THREAD_ID" "..."; kagi assistant thread export "$THREAD_ID"` ```bash -chmod +x scripts/demo-search.sh scripts/demo-summarize.sh scripts/demo-news.sh scripts/demo-assistant.sh +chmod +x scripts/demo-search.sh scripts/demo-summarize.sh scripts/demo-news.sh scripts/demo-ask-page.sh scripts/demo-assistant.sh mkdir -p docs/demo-assets /tmp/kagi-demos @@ -52,6 +59,7 @@ agg --version KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-search.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/search.cast KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-summarize.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/summarize.cast asciinema rec -c ./scripts/demo-news.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/news.cast +KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-ask-page.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/ask-page.cast KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-assistant.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/assistant.cast ``` @@ -63,5 +71,6 @@ Use the official asciinema `agg` binary when exporting GIFs. On this machine, th agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/search.cast docs/demo-assets/search.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/summarize.cast docs/demo-assets/summarize.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/news.cast docs/demo-assets/news.gif +agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/ask-page.cast docs/demo-assets/ask-page.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 4 /tmp/kagi-demos/assistant.cast docs/demo-assets/assistant.gif ``` diff --git a/docs/reference/auth-matrix.mdx b/docs/reference/auth-matrix.mdx index 0752d02..9e0ba0e 100644 --- a/docs/reference/auth-matrix.mdx +++ b/docs/reference/auth-matrix.mdx @@ -19,6 +19,7 @@ This reference provides a complete mapping of which commands require which authe | `summarize` | `KAGI_API_TOKEN` | None | Paid public API | | `summarize --subscriber` | `KAGI_SESSION_TOKEN` | None | Subscriber web product | | `news` | None | None | Public endpoint | +| `ask-page` | `KAGI_SESSION_TOKEN` | None | Subscriber feature | | `assistant` | `KAGI_SESSION_TOKEN` | None | Subscriber feature | | `fastgpt` | `KAGI_API_TOKEN` | None | Paid public API | | `enrich web` | `KAGI_API_TOKEN` | None | Paid public API | @@ -102,6 +103,7 @@ flowchart TD | Command | Token | Purpose | |---------|-------|---------| +| `ask-page` | `KAGI_SESSION_TOKEN` | Ask Assistant about one page URL | | `assistant` | `KAGI_SESSION_TOKEN` | Conversational AI with threads | | `fastgpt` | `KAGI_API_TOKEN` | Quick factual answers | @@ -130,7 +132,9 @@ Both `enrich web` and `enrich news` require `KAGI_API_TOKEN`: Requires `KAGI_SESSION_TOKEN`: - ✅ Lens-aware search (`--lens`) +- ✅ Ask Page (`ask-page`) - ✅ Kagi Assistant prompt and thread commands (`assistant`) +- ✅ Ask Page (`ask-page`) - ✅ Subscriber Summarizer (`summarize --subscriber`) - ✅ Base search (fallback) @@ -206,6 +210,7 @@ kagi auth set --session-token 'https://kagi.com/search?token=...' **Working commands:** - ✅ `kagi search "query"` (uses session path) - ✅ `kagi search --lens 2 "query"` +- ✅ `kagi ask-page https://example.com "question"` - ✅ `kagi assistant "prompt"` - ✅ `kagi summarize --subscriber --url ...` - ✅ `kagi news` @@ -233,6 +238,7 @@ kagi auth set --api-token 'your_api_token' **Non-working:** - ❌ `kagi search --lens 2` - requires session token +- ❌ `kagi ask-page https://example.com "question"` - requires session token - ❌ `kagi assistant` - requires session token - ❌ `kagi summarize --subscriber` - requires session token @@ -250,6 +256,7 @@ kagi auth set --session-token '...' --api-token '...' - `search`: Uses API (preferred), falls back to session if needed - `summarize` without `--subscriber`: Uses API - `summarize --subscriber`: Uses session +- `ask-page`: Uses session - `assistant`: Uses session - `fastgpt`: Uses API diff --git a/docs/reference/coverage.mdx b/docs/reference/coverage.mdx index 27f1fb6..2fd453f 100644 --- a/docs/reference/coverage.mdx +++ b/docs/reference/coverage.mdx @@ -31,6 +31,7 @@ These features use the subscriber web product (Session Token): | Lens Search | `kagi search --lens` | ✅ Implemented | | Web Summarizer | `kagi summarize --subscriber` | ✅ Implemented | | Assistant prompt + thread management | `kagi assistant` | ✅ Implemented | +| Ask Questions About a Page | `kagi ask-page` | ✅ Implemented | ### Public Product Endpoints @@ -56,6 +57,7 @@ These require no authentication: | `summarize --subscriber` | Web summarizer | Session | ✅ | | `news` | News feed | None | ✅ | | `assistant` | AI assistant with thread management | Session | ✅ | +| `ask-page` | Page-focused Assistant question | Session | ✅ | | `fastgpt` | Fast answers | API | ✅ | | `enrich web` | Web enrichment | API | ✅ | | `enrich news` | News enrichment | API | ✅ | @@ -80,6 +82,7 @@ These require no authentication: | `--cache` | summarize (API), fastgpt | ✅ | | `--web-search` | fastgpt | ✅ | | `--target-language` | summarize | ✅ | +| ` ` | ask-page | ✅ | | `--thread-id` | assistant | ✅ | | `--model` | assistant | ✅ | | `--lens` | assistant | ✅ | @@ -125,7 +128,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 @@ -150,6 +153,7 @@ All commands output JSON: | news | Stable | | smallweb | Stable | | summarize | Stable | +| ask-page | Stable | | assistant | Stable | | fastgpt | Stable | | enrich | Stable | @@ -197,6 +201,7 @@ All commands output JSON: | Search | ✅ | ✅ | | Lens Search | ✅ | ✅ | | Assistant | ✅ (prompt + threads) | ✅ (full) | +| Ask Questions about Page | ✅ | ✅ | | Summarizer | ✅ | ✅ | | Translate | ❌ | ✅ | | Settings | ❌ | ✅ | 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 f1d795e..d44e003 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' + # Assistant thread ids kagi assistant thread list | jq -r '.threads[].id' diff --git a/images/demos/ask-page.gif b/images/demos/ask-page.gif new file mode 100644 index 0000000..c74e157 Binary files /dev/null and b/images/demos/ask-page.gif differ diff --git a/project/demos.mdx b/project/demos.mdx index 7f489c5..6be043a 100644 --- a/project/demos.mdx +++ b/project/demos.mdx @@ -10,6 +10,7 @@ The docs site ships with recorded terminal GIFs from the repo: - search - subscriber summarize - news +- ask-page - assistant ## Preview @@ -30,6 +31,10 @@ The docs site ships with recorded terminal GIFs from the repo: ![Assistant demo](/images/demos/assistant.gif) +### Ask Page + +![Ask Page demo](/images/demos/ask-page.gif) + ## Regenerate Subscriber demos require `KAGI_SESSION_TOKEN`. @@ -41,10 +46,12 @@ The current demo commands are: - `kagi search --format pretty "obsidian cli daily notes workflow"` - `kagi summarize --subscriber --url https://mullvad.net/en/browser | jq -M ...` - `kagi news --category tech --limit 1 | jq -M ...` +- `kagi ask-page https://rust-lang.org/ "What is this page about in one sentence?" | jq -M ...` +- `kagi assistant "plan a private obsidian workflow for cafe work. give me 3 setup tips and a short checklist." | jq -M ...` - `RESPONSE=$(kagi assistant --model gpt-5-mini "..."); THREAD_ID=...; kagi assistant --thread-id "$THREAD_ID" "..."; kagi assistant thread export "$THREAD_ID"` ```bash -chmod +x scripts/demo-search.sh scripts/demo-summarize.sh scripts/demo-news.sh scripts/demo-assistant.sh +chmod +x scripts/demo-search.sh scripts/demo-summarize.sh scripts/demo-news.sh scripts/demo-ask-page.sh scripts/demo-assistant.sh mkdir -p docs/demo-assets /tmp/kagi-demos @@ -52,6 +59,7 @@ agg --version KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-search.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/search.cast KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-summarize.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/summarize.cast asciinema rec -c ./scripts/demo-news.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/news.cast +KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-ask-page.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/ask-page.cast KAGI_SESSION_TOKEN='...' asciinema rec -c ./scripts/demo-assistant.sh -q -i 0.2 --cols 92 --rows 22 /tmp/kagi-demos/assistant.cast ``` @@ -63,5 +71,6 @@ Use the official asciinema `agg` binary when exporting GIFs. On this machine, th agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 2 /tmp/kagi-demos/search.cast docs/demo-assets/search.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 2 /tmp/kagi-demos/summarize.cast docs/demo-assets/summarize.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 2 /tmp/kagi-demos/news.cast docs/demo-assets/news.gif +agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 2 /tmp/kagi-demos/ask-page.cast docs/demo-assets/ask-page.gif agg --theme asciinema --font-size 14 --idle-time-limit 2 --last-frame-duration 2 /tmp/kagi-demos/assistant.cast docs/demo-assets/assistant.gif ``` diff --git a/scripts/demo-ask-page.sh b/scripts/demo-ask-page.sh new file mode 100755 index 0000000..3f8bbf5 --- /dev/null +++ b/scripts/demo-ask-page.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +: "${KAGI_SESSION_TOKEN:?set KAGI_SESSION_TOKEN before running this demo}" + +cargo build --quiet +mkdir -p /tmp/kagi-demo-bin +ln -sf "$PWD/target/debug/kagi" /tmp/kagi-demo-bin/kagi +export PATH="/tmp/kagi-demo-bin:$PATH" + +printf '\033c' +sleep 1.2 +printf '$ kagi ask-page https://rust-lang.org/ "What is this page about in one sentence?" | jq -M ...\n' +sleep 0.4 +kagi ask-page https://rust-lang.org/ "What is this page about in one sentence?" \ + | jq -M '{ + source: .source.url, + thread_id: .thread.id, + reply: .message.markdown + }' +sleep 2 diff --git a/src/api.rs b/src/api.rs index bfa2e3f..9d28464 100644 --- a/src/api.rs +++ b/src/api.rs @@ -14,14 +14,15 @@ use crate::parser::parse_assistant_thread_list; #[cfg(test)] use crate::types::ApiMeta; use crate::types::{ - AssistantMessage, AssistantMeta, AssistantPromptRequest, AssistantPromptResponse, - AssistantThread, AssistantThreadDeleteResponse, AssistantThreadExportResponse, - AssistantThreadListResponse, AssistantThreadOpenResponse, AssistantThreadPagination, - EnrichResponse, FastGptRequest, FastGptResponse, NewsBatchCategories, NewsBatchCategory, - NewsCategoriesResponse, NewsCategoryMetadata, NewsCategoryMetadataList, NewsChaos, - NewsChaosResponse, NewsLatestBatch, NewsResolvedCategory, NewsStoriesPayload, - NewsStoriesResponse, SmallWebFeed, SubscriberSummarization, SubscriberSummarizeMeta, - SubscriberSummarizeRequest, SubscriberSummarizeResponse, SummarizeRequest, SummarizeResponse, + AskPageRequest, AskPageResponse, AskPageSource, AssistantMessage, AssistantMeta, + AssistantPromptRequest, AssistantPromptResponse, AssistantThread, + AssistantThreadDeleteResponse, AssistantThreadExportResponse, AssistantThreadListResponse, + AssistantThreadOpenResponse, AssistantThreadPagination, EnrichResponse, FastGptRequest, + FastGptResponse, NewsBatchCategories, NewsBatchCategory, NewsCategoriesResponse, + NewsCategoryMetadata, NewsCategoryMetadataList, NewsChaos, NewsChaosResponse, NewsLatestBatch, + NewsResolvedCategory, NewsStoriesPayload, NewsStoriesResponse, SmallWebFeed, + SubscriberSummarization, SubscriberSummarizeMeta, SubscriberSummarizeRequest, + SubscriberSummarizeResponse, SummarizeRequest, SummarizeResponse, }; const USER_AGENT: &str = "kagi-cli/0.1.0 (+https://github.com/)"; @@ -435,6 +436,36 @@ pub async fn execute_assistant_thread_export( } } +pub async fn execute_ask_page( + request: &AskPageRequest, + token: &str, +) -> 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, + model: None, + lens_id: None, + internet_access: None, + personalizations: 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, @@ -1159,6 +1190,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)] @@ -1365,7 +1429,8 @@ pub struct KagiEnvelope { #[cfg(test)] mod tests { use super::{ - ApiErrorBody, KagiEnvelope, normalize_assistant_query, normalize_assistant_thread_id, + ApiErrorBody, KagiEnvelope, build_ask_page_prompt, normalize_ask_page_question, + normalize_ask_page_url, normalize_assistant_query, normalize_assistant_thread_id, normalize_subscriber_summary_input, normalize_subscriber_summary_length, normalize_subscriber_summary_type, parse_assistant_prompt_stream, parse_assistant_thread_delete_stream, parse_assistant_thread_list_stream, @@ -1377,7 +1442,7 @@ mod tests { execute_assistant_thread_get, execute_assistant_thread_list, }; use crate::auth::{SESSION_TOKEN_ENV, load_credential_inventory}; - use crate::types::SubscriberSummarizeRequest; + use crate::types::{AskPageRequest, SubscriberSummarizeRequest}; use crate::types::{ AssistantPromptRequest, FastGptAnswer, NewsBatchCategory, NewsCategoryMetadata, Reference, Summarization, @@ -1760,4 +1825,63 @@ mod tests { .expect("assistant thread delete should succeed"); assert_eq!(deleted.deleted_thread_ids, vec![thread_id]); } + + #[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 Some(token) = live_session_token() else { + eprintln!("skipping live ask-page test because {SESSION_TOKEN_ENV} is not set"); + return; + }; + + 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/cli.rs b/src/cli.rs index dfeea7e..057e2c5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -87,6 +87,8 @@ pub enum Commands { News(NewsArgs), /// Prompt Kagi Assistant and manage Assistant threads 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 @@ -350,6 +352,17 @@ pub struct AssistantThreadExportArgs { pub format: AssistantThreadExportFormat, } +#[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 a592c14..4702181 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,10 +10,11 @@ use clap::{CommandFactory, Parser}; use clap_complete::{generate, shells}; use crate::api::{ - execute_assistant_prompt, execute_assistant_thread_delete, execute_assistant_thread_export, - execute_assistant_thread_get, execute_assistant_thread_list, execute_enrich_news, - execute_enrich_web, execute_fastgpt, execute_news, execute_news_categories, execute_news_chaos, - execute_smallweb, execute_subscriber_summarize, execute_summarize, + execute_ask_page, execute_assistant_prompt, execute_assistant_thread_delete, + execute_assistant_thread_export, execute_assistant_thread_get, execute_assistant_thread_list, + 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::{ Credential, CredentialKind, SearchCredentials, format_status, load_credential_inventory, @@ -25,8 +26,8 @@ use crate::cli::{ }; 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}; @@ -193,6 +194,15 @@ async fn run() -> Result<(), KagiError> { 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 947e568..a16d2af 100644 --- a/src/types.rs +++ b/src/types.rs @@ -250,6 +250,12 @@ pub struct AssistantPromptRequest { pub personalizations: 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, @@ -298,6 +304,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 AssistantThreadSummary { pub id: String,