Skip to content

Commit 63cea1d

Browse files
authored
feat: add CLI contract workflows
- Add assistant prompt contracts with built-in and file-backed validation plus one repair attempt - Add opt-in JSON error envelopes via `--error-format json` and `KAGI_ERROR_FORMAT=json` - Resolve `kagi search --lens` exact enabled lens names to the current numeric position - Add `kagi extract --filter` JSONL mode for stdin URL pipelines - Update `quinn-proto` in `Cargo.lock` to satisfy the RustSec audit gate
1 parent e2b4732 commit 63cea1d

11 files changed

Lines changed: 1197 additions & 62 deletions

File tree

.facts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
# domain
3+
- a Contract is a prompt-mode output shape that the assistant command asks Kagi Assistant to return as JSON and validates before printing
4+
- an ErrorEnvelope is an opt-in JSON stderr object for command failures with stable code, category, retryable, message, and suggested_commands fields
5+
6+
# cli
7+
8+
## assistant
9+
- label: kagi assistant --contract decision validates the final assistant reply as a JSON object containing decision, rationale, and next_actions before printing it
10+
command: cargo test --test integration-cli assistant_contract_decision_prints_validated_json -- --exact
11+
tags: [implemented]
12+
- label: kagi assistant --contract-file accepts a small JSON contract file with required top-level string keys and rejects assistant replies that do not satisfy it
13+
command: cargo test --test integration-cli assistant_contract_file_rejects_missing_required_key -- --exact
14+
tags: [implemented]
15+
16+
## errors
17+
- label: runtime command failures can opt into JSON stderr with --error-format json or KAGI_ERROR_FORMAT=json
18+
command: cargo test --test integration-cli json_error_format_prints_structured_stderr -- --exact
19+
tags: [implemented]
20+
21+
## search
22+
- label: kagi search --lens accepts an enabled lens exact name, resolves it through the authenticated lens settings page, and sends the current numeric lens position to search
23+
command: cargo test --test integration-cli search_lens_name_resolves_to_current_position -- --exact
24+
tags: [implemented]
25+
26+
## extract
27+
- label: kagi extract --filter reads HTTPS URLs from stdin and writes one compact JSONL record per input in order, including per-item errors without corrupting stdout
28+
command: cargo test --test integration-cli extract_filter_prints_ordered_jsonl_records -- --exact
29+
tags: [implemented]

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/commands/assistant.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Prompt Kagi Assistant, continue an existing thread, use a saved assistant profil
1414
```bash
1515
kagi assistant [OPTIONS] <QUERY>
1616
kagi assistant --stream [--stream-output text|json] <QUERY>
17+
kagi assistant --contract decision <QUERY>
18+
kagi assistant --contract-file ./contract.json <QUERY>
1719
kagi assistant thread list
1820
kagi assistant thread get <THREAD_ID>
1921
kagi assistant thread delete <THREAD_ID>
@@ -76,6 +78,39 @@ kagi assistant --format pretty "What changed in Rust 1.86?"
7678
kagi assistant --format markdown "Write a release summary"
7779
```
7880

81+
#### `--contract <NAME>`
82+
83+
Ask Assistant to return only JSON that satisfies a built-in contract, then validate the final reply before printing it. Contract mode supports `--format json` and `--format compact`.
84+
85+
Supported built-ins:
86+
87+
- `decision` - requires `decision`, `rationale`, and `next_actions`
88+
- `checklist` - requires `items`
89+
- `plan` - requires `steps`
90+
91+
```bash
92+
kagi assistant --contract decision "Should we ship this release?"
93+
```
94+
95+
If the first reply is invalid, the CLI sends one repair prompt in the same thread. If the repaired reply still fails validation, the command exits with a contract error.
96+
97+
#### `--contract-file <PATH>`
98+
99+
Load a small JSON contract file. The supported subset is a top-level object with `required` and optional `properties.<key>.type` fields:
100+
101+
```json
102+
{
103+
"type": "object",
104+
"required": ["summary", "verdict"],
105+
"properties": {
106+
"summary": { "type": "string" },
107+
"verdict": { "type": "string" }
108+
}
109+
}
110+
```
111+
112+
Required entries default to `string` when `properties` does not specify a type. Supported types are `string`, `number`, `integer`, `boolean`, `object`, and `array`.
113+
79114
#### `--no-color`
80115

81116
Disable ANSI colors in `--format pretty`.
@@ -289,6 +324,16 @@ Prompt mode returns:
289324
}
290325
```
291326

327+
Contract mode returns only the validated contract JSON, not the normal Assistant envelope:
328+
329+
```json
330+
{
331+
"decision": "ship",
332+
"rationale": "tests pass",
333+
"next_actions": ["open PR"]
334+
}
335+
```
336+
292337
`thread list` returns:
293338

294339
```json

docs/commands/extract.mdx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Extract the readable content of a web page as markdown using Kagi's Extract API.
1111

1212
```bash
1313
kagi extract <URL> [--format markdown|json|compact]
14+
kagi extract --filter
1415
```
1516

1617
## Description
@@ -19,6 +20,8 @@ The `kagi extract` command sends one HTTPS URL to Kagi's v1 Extract API and prin
1920

2021
The command uses JSON mode internally because that is the stable envelope returned by the API, then prints the first page's `markdown` field. Use `--format json` or `--format compact` to print the full response envelope, including retained link metadata when Kagi returns it. If Kagi returns no page markdown, the CLI reports the Extract API error details and trace id when available.
2122

23+
Use `--filter` for pipelines that already have one URL per line. Filter mode ignores the single-URL markdown default and prints one compact JSON record per input line in the same order. Successful records include the Extract API response under `response`. Failed records include an `error` envelope on stdout and the command exits nonzero after all inputs are processed.
24+
2225
## Authentication
2326

2427
**Required:** `KAGI_API_KEY`
@@ -37,6 +40,16 @@ kagi extract "https://example.com/article"
3740

3841
Only `https://` URLs with a valid host are accepted.
3942

43+
Omit `<URL>` when using `--filter`.
44+
45+
### `--filter`
46+
47+
Read one URL per stdin line and print JSONL records.
48+
49+
```bash
50+
kagi search "rust release notes" --template '{{url}}' | kagi extract --filter
51+
```
52+
4053
### `--format <FORMAT>`
4154

4255
Output format. Default: `markdown`.
@@ -81,6 +94,13 @@ With `--format json`, the command prints the full response envelope:
8194
}
8295
```
8396

97+
With `--filter`, stdout is JSONL:
98+
99+
```jsonl
100+
{"url":"https://example.com/article","ok":true,"response":{"meta":{"trace":"trace-1","node":"test","ms":12},"data":[{"url":"https://example.com/article","markdown":"# Article title\n\nExtracted page content...","links":[]}]}}
101+
{"url":"http://example.com/bad","ok":false,"error":{"code":"configuration_error","category":"configuration","retryable":false,"message":"configuration error: extract URL must use the https scheme","suggested_commands":[],"docs_url":"https://kagi.micr.dev/reference/error-reference"}}
102+
```
103+
84104
## Examples
85105

86106
### Save an Article
@@ -101,12 +121,18 @@ kagi extract "https://example.com/article" | sed -n '1,80p'
101121
kagi extract "https://example.com/article" --format json | jq '.data[0].links'
102122
```
103123

124+
### Extract Search Results
125+
126+
```bash
127+
kagi search "rust async cancellation" --template '{{url}}' | kagi extract --filter > pages.jsonl
128+
```
129+
104130
## Exit Codes
105131

106132
| Code | Meaning |
107133
|------|---------|
108-
| 0 | Success - markdown extracted |
109-
| 1 | Error - see stderr |
134+
| 0 | Success - markdown extracted, or every `--filter` input produced an `ok: true` record |
135+
| 1 | Error - see stderr; `--filter` may still have written per-input records to stdout |
110136

111137
Common errors:
112138

docs/commands/search.mdx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,17 @@ kagi search --snap reddit "rust async runtime"
9393
kagi search --snap @map "coffee near london bridge"
9494
```
9595

96-
### `--lens <INDEX>`
96+
### `--lens <INDEX_OR_NAME>`
9797

98-
Scope search to one of your enabled Kagi lenses.
98+
Scope search to one of your enabled Kagi lenses. Numeric values keep the existing index behavior. Non-numeric values are matched against enabled lens names exactly at runtime, then resolved to the current account-specific numeric position before search.
9999

100100
```bash
101101
kagi search --lens 2 "developer documentation"
102+
kagi search --lens "Rust Docs" "borrow checker examples"
102103
```
103104

105+
If a name is missing, disabled, or duplicated, the command fails before search and points you to `kagi lens list` or the numeric index path. Numeric-looking names are treated as numeric indexes.
106+
104107
### `--region <REGION>`
105108

106109
Restrict results to a Kagi region code such as `us`, `gb`, `jp`, or `no_region`.
@@ -361,7 +364,10 @@ kagi search --format markdown "rust ownership"
361364
- `this search option requires KAGI_SESSION_TOKEN` - you used a web-product-only search option without a session token
362365
- `search --time cannot be combined with --from-date or --to-date` - choose a preset window or a custom date range
363366
- `search --from-date must use YYYY-MM-DD format` - dates must be zero-padded ISO dates
364-
- `lens 'foo' must be a numeric index` - lens values are numeric Kagi lens indices
367+
- `lens named 'foo' was not found` - named lens search matches enabled lens names exactly. Run `kagi lens list`
368+
- `lens name 'foo' is ambiguous` - multiple lenses have that exact name. Use the numeric index
369+
- `lens named 'foo' is disabled` - enable it with `kagi lens enable <ID_OR_NAME>` or choose an enabled lens
370+
- `lens 'foo' must be a numeric index` - internal numeric validation still applies after named lenses are resolved
365371

366372
## Related Commands
367373

docs/reference/coverage.mdx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ These endpoints come from Kagi's current OpenAPI contract at `https://kagi.com/a
1616
| Endpoint | Command | Status |
1717
|----------|---------|--------|
1818
| Search API (`POST /api/v1/search`) | `kagi search` | ✅ Implemented for query, limit, and `filters.region/after/before` |
19-
| Extract API | `kagi extract` | ✅ Implemented |
19+
| Extract API | `kagi extract` | ✅ Implemented, including stdin URL filter mode |
2020

2121
Current V1 Search optional fields not exposed by this CLI: `workflow`, API response `format`, `lens_id`, inline `lens`, `timeout`, `page`, search-result `extract`, `safe_search`, and explicit personalization rule objects. Subscriber web-product features cover some adjacent workflows, but they are not represented as V1 OpenAPI request fields.
2222

@@ -39,12 +39,12 @@ These features use the subscriber web product with `KAGI_SESSION_TOKEN`:
3939
|---------|---------|--------|
4040
| Base search | `kagi search` | ✅ Implemented |
4141
| Snap-prefixed search | `kagi search --snap` | ✅ Implemented |
42-
| Lens search | `kagi search --lens` | ✅ Implemented |
42+
| Lens search | `kagi search --lens` | ✅ Implemented for numeric indexes and exact enabled lens names |
4343
| Filtered search | `kagi search --region/--from-date/--to-date` on V1 API or session path; `--time/--order/...` on session path | ✅ Implemented |
4444
| Quick Answer | `kagi quick` | ✅ Implemented |
4545
| Web Summarizer | `kagi summarize --subscriber` | ✅ Implemented |
4646
| Search result summarize pipeline | `kagi search --follow` | ✅ Implemented |
47-
| Assistant prompt + thread management | `kagi assistant` | ✅ Implemented |
47+
| Assistant prompt + thread management | `kagi assistant` | ✅ Implemented, including prompt contracts |
4848
| Assistant terminal REPL | `kagi assistant repl` | ✅ Implemented |
4949
| Custom assistant management | `kagi assistant custom` | ✅ Implemented |
5050
| Ask questions about a page | `kagi ask-page` | ✅ Implemented |
@@ -71,7 +71,7 @@ These require no authentication:
7171
|---------|-------------|------|--------|
7272
| `search` | Base Kagi search | API or Session ||
7373
| `search --snap` | Snap-prefixed search | API or Session ||
74-
| `search --lens` | Lens-aware search | Session ||
74+
| `search --lens` | Lens-aware search by numeric index or exact enabled name | Session ||
7575
| `search` with filters | Region, time, date, order, verbatim, personalization filters | Session ||
7676
| `search --template` | Lightweight result templates | API or Session ||
7777
| `search --follow` | Search plus subscriber summaries for top results | Session ||
@@ -83,7 +83,7 @@ These require no authentication:
8383
| `summarize` | Public API summarizer | API ||
8484
| `summarize --subscriber` | Web summarizer | Session ||
8585
| `summarize --filter` | Summarize stdin items as URLs or text | API or Session ||
86-
| `extract` | Extract page content as markdown | API ||
86+
| `extract` | Extract page content as markdown or stdin URL JSONL records | API ||
8787
| `watch` | Search diff monitoring | API or Session ||
8888
| `notify` | Webhook notifications for search/news | API, Session, or None ||
8989
| `history` | Local history and stats | None ||
@@ -130,19 +130,22 @@ These require no authentication:
130130
| `--personalized` / `--no-personalized` | search, batch, assistant ||
131131
| `--template` | search, batch ||
132132
| `--local-cache` / `--cache-ttl` | search, summarize, quick, fastgpt ||
133+
| `--error-format` | global | Implemented |
133134

134135
### Assistant and Settings Options
135136

136137
| Option | Commands | Status |
137138
|--------|----------|--------|
138139
| `--assistant` | assistant ||
140+
| `--contract` / `--contract-file` | assistant | Implemented |
139141
| `--thread-id` | assistant ||
140142
| `--model` | assistant, assistant custom ||
141143
| `--lens` | assistant, assistant custom ||
142144
| `--web-access` / `--no-web-access` | assistant, assistant custom ||
143145
| `thread list/get/delete/export` | assistant ||
144146
| `custom list/get/create/update/delete` | assistant ||
145147
| `repl` | assistant ||
148+
| `--filter` | extract | Implemented |
146149
| `list/get/create/update/delete` | lens ||
147150
| `enable` / `disable` | lens, redirect ||
148151
| `custom list/get/create/update/delete` | bang ||

docs/reference/error-reference.mdx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This guide catalogs all error messages from the *kagi* CLI, organized by categor
99

1010
## Error Format
1111

12-
Errors follow this pattern:
12+
Plain text errors follow this pattern:
1313

1414
```
1515
Error: {category}: {message}
@@ -31,6 +31,27 @@ Transport errors include the request URL when the HTTP client exposes it:
3131
Error: Network error: request to https://kagi.com/api/v1/search timed out after the configured timeout
3232
```
3333

34+
Automation can request compact JSON stderr instead:
35+
36+
```bash
37+
kagi --error-format json search "rust"
38+
KAGI_ERROR_FORMAT=json kagi search "rust"
39+
```
40+
41+
The JSON error envelope uses stable top-level fields:
42+
43+
```json
44+
{
45+
"code": "missing_credentials",
46+
"category": "configuration",
47+
"retryable": false,
48+
"message": "configuration error: missing credentials: search was not sent...",
49+
"required_auth": "KAGI_API_KEY or KAGI_SESSION_TOKEN",
50+
"suggested_commands": ["kagi auth status", "kagi auth set --api-key <key>", "kagi auth set --session-token <token>"],
51+
"docs_url": "https://kagi.micr.dev/reference/error-reference"
52+
}
53+
```
54+
3455
## Authentication Errors
3556

3657
### "missing credentials"
@@ -269,22 +290,25 @@ Error: Config: --length requires --subscriber
269290
kagi summarize --subscriber --url https://example.com --length digest
270291
```
271292

272-
### "lens 'abc' must be a numeric index"
293+
### "lens named 'abc' was not found"
273294

274295
**Message:**
275296
```
276-
configuration error: lens 'abc' must be a numeric index (e.g., '0', '1', '2').
297+
configuration error: lens named 'abc' was not found. Lens names are matched exactly; run `kagi lens list` to inspect available lenses
277298
```
278299

279-
**Meaning:** Lens-aware commands only accept the numeric `l=` value from Kagi's web UI.
300+
**Meaning:** `kagi search --lens` was given a non-numeric value, but no enabled lens has that exact name.
280301

281302
**Solution:**
282303
```bash
283-
# Search
284-
kagi search --lens 2 "developer documentation"
304+
kagi lens list
305+
kagi search --lens "Exact Lens Name" "developer documentation"
306+
```
285307

286-
# Quick Answer
287-
kagi quick --lens 2 "best rust tutorials"
308+
Use a numeric index when names are duplicated or numeric-looking:
309+
310+
```bash
311+
kagi search --lens 2 "developer documentation"
288312
```
289313

290314
### "--engine is only supported for the paid public summarizer API"

0 commit comments

Comments
 (0)