mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
feat(cli): smart defaults and flatter command surface for ktx (#177)
Bare invocations now do the obvious thing instead of erroring out, and mode-as-subcommand patterns collapse into flags on the parent. No new top-level commands. - `ktx ingest` (bare) ingests every configured connection. The `text` subcommand is gone; capture inline notes with `ktx ingest --text "..."` and files with `ktx ingest --file path` (use `-` for stdin). `--text`/`--file` reject a positional connection id; pass `--connection-id` to tag captured notes. - `ktx connection` (bare) lists; `ktx connection test` (bare) tests every configured connection. - `ktx wiki` and `ktx sl` flatten `list`/`search`: bare lists, with a `[query...]` positional searches (multi-word joined with spaces). `sl validate` and `sl query` stay as distinct verbs and now read `--connection-id` from the parent. - `ktx mcp` (bare) prints daemon status. Adds a shared `resolveConnectionSelection` helper consumed by ingest and connection test. Updates README, docs-site cli-reference and guides, next-steps strings, agent SKILL templates, and all affected tests. Per-package type-check, unit tests (605), smoke tests, and dead-code checks all pass.
This commit is contained in:
parent
14626c294b
commit
2c9a58bb56
33 changed files with 438 additions and 380 deletions
20
README.md
20
README.md
|
|
@ -95,17 +95,21 @@ Agent integration ready: yes (codex:project)
|
|||
|---------|---------|
|
||||
| `ktx setup` | Create, resume, or update a KTX project |
|
||||
| `ktx status` | Check project readiness |
|
||||
| `ktx connection list` | List configured connections |
|
||||
| `ktx connection` | List configured connections |
|
||||
| `ktx connection test` | Test every configured connection |
|
||||
| `ktx connection test <id>` | Test one connection |
|
||||
| `ktx ingest` | Build context for every configured connection |
|
||||
| `ktx ingest <id>` | Build context for one connection |
|
||||
| `ktx ingest --all` | Build context for every configured connection |
|
||||
| `ktx ingest text <file> --connection-id <connectionId>` | Capture free-form notes into memory |
|
||||
| `ktx sl list` | List semantic-layer sources |
|
||||
| `ktx sl search "revenue"` | Search semantic-layer sources |
|
||||
| `ktx ingest --text "..."` | Capture free-form notes into memory |
|
||||
| `ktx ingest --file notes.md --connection-id <id>` | Capture a text file into memory |
|
||||
| `ktx sl` | List semantic-layer sources |
|
||||
| `ktx sl "revenue"` | Search semantic-layer sources |
|
||||
| `ktx sl validate <source> --connection-id <id>` | Validate a semantic source |
|
||||
| `ktx sl query --measure <measure> --format sql` | Compile semantic-layer SQL |
|
||||
| `ktx sql --connection <id> "select 1"` | Execute read-only SQL |
|
||||
| `ktx wiki search "revenue definition"` | Search local wiki context |
|
||||
| `ktx wiki` | List local wiki pages |
|
||||
| `ktx wiki "revenue definition"` | Search local wiki context |
|
||||
| `ktx mcp` | Show MCP daemon status |
|
||||
| `ktx mcp start` | Start the local MCP server for agent clients |
|
||||
|
||||
Project resolution defaults to `KTX_PROJECT_DIR`, then the nearest `ktx.yaml`,
|
||||
|
|
@ -140,8 +144,8 @@ A typical agent workflow combines wiki and semantic-layer search before
|
|||
querying:
|
||||
|
||||
```bash
|
||||
ktx sl search "revenue" --json
|
||||
ktx wiki search "refund policy" --json
|
||||
ktx sl "revenue" --json
|
||||
ktx wiki "refund policy" --json
|
||||
ktx sl query --connection-id warehouse --measure orders.revenue --format sql
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -10,15 +10,21 @@ systems. Use `ktx setup` to add, remove, or reconfigure them.
|
|||
## Command signature
|
||||
|
||||
```bash
|
||||
ktx connection <subcommand> [options]
|
||||
ktx connection # list all configured connections
|
||||
ktx connection list # explicit list
|
||||
ktx connection test [connectionId] # test one (or all, when omitted)
|
||||
```
|
||||
|
||||
Bare `ktx connection` lists configured connections. `ktx connection test`
|
||||
with no positional and no flag tests every configured connection.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Subcommand | Description |
|
||||
|-----------|-------------|
|
||||
| (none) | List configured connections (alias for `list`) |
|
||||
| `list` | List configured connections |
|
||||
| `test [connectionId]` | Test one configured connection, or every connection with `--all` |
|
||||
| `test [connectionId]` | Test one configured connection; omit the id (or pass `--all`) to test every connection |
|
||||
|
||||
## Options
|
||||
|
||||
|
|
@ -29,7 +35,7 @@ ktx connection <subcommand> [options]
|
|||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--all` | Test every configured connection and print a summary list | `false` |
|
||||
| `--all` | Test every configured connection and print a summary list | implicit when no `connectionId` is supplied |
|
||||
|
||||
Project directory resolution defaults to `KTX_PROJECT_DIR`, then the nearest
|
||||
`ktx.yaml`, then the current working directory.
|
||||
|
|
@ -38,12 +44,15 @@ Project directory resolution defaults to `KTX_PROJECT_DIR`, then the nearest
|
|||
|
||||
```bash
|
||||
# List all configured connections
|
||||
ktx connection list
|
||||
|
||||
# Test a connection
|
||||
ktx connection test my-warehouse
|
||||
ktx connection
|
||||
|
||||
# Test every configured connection
|
||||
ktx connection test
|
||||
|
||||
# Test one connection
|
||||
ktx connection test my-warehouse
|
||||
|
||||
# Test every connection explicitly
|
||||
ktx connection test --all
|
||||
|
||||
# Test a connection from outside the project
|
||||
|
|
@ -58,7 +67,8 @@ Metabase mapping prompts for BI-to-warehouse mappings.
|
|||
|
||||
## Output
|
||||
|
||||
`ktx connection list` prints a table of configured ids and drivers.
|
||||
`ktx connection` (or `ktx connection list`) prints a table of configured ids
|
||||
and drivers.
|
||||
|
||||
```text
|
||||
ID DRIVER
|
||||
|
|
@ -76,8 +86,8 @@ Driver: postgres
|
|||
Status: ok
|
||||
```
|
||||
|
||||
`ktx connection test --all` prints one row per configured connection and exits
|
||||
non-zero if any probe fails.
|
||||
`ktx connection test` (bare) and `ktx connection test --all` print one row per
|
||||
configured connection and exit non-zero if any probe fails.
|
||||
|
||||
```text
|
||||
╭ connection test --all
|
||||
|
|
|
|||
|
|
@ -1,35 +1,44 @@
|
|||
---
|
||||
title: "ktx ingest"
|
||||
description: "Build or refresh KTX context from configured connections."
|
||||
description: "Build or refresh KTX context, or capture text into KTX memory."
|
||||
---
|
||||
|
||||
`ktx ingest` builds or refreshes KTX context from configured connections.
|
||||
Database connections build schema context. Context-source connections ingest
|
||||
metadata from tools such as dbt, Looker, Metabase, MetricFlow, LookML, and
|
||||
Notion. The current public command is connection-centric: pass one
|
||||
`connectionId`, or pass `--all`.
|
||||
`ktx ingest` builds or refreshes KTX context from configured connections, and
|
||||
can also capture free-form text into KTX memory. Database connections build
|
||||
schema context. Context-source connections ingest metadata from tools such as
|
||||
dbt, Looker, Metabase, MetricFlow, LookML, and Notion. Pass `--text` or
|
||||
`--file` to capture inline text or text files into memory instead.
|
||||
|
||||
## Command signature
|
||||
|
||||
```bash
|
||||
ktx ingest [options] [connectionId]
|
||||
ktx ingest text [options] [files...]
|
||||
```
|
||||
|
||||
Use a connection id to build one configured connection. Use `--all` to build
|
||||
every configured connection. Database connections run before context-source
|
||||
connections when you use `--all`.
|
||||
- Bare `ktx ingest` (no positional, no `--all`) ingests every configured
|
||||
connection.
|
||||
- `ktx ingest <connectionId>` ingests one configured connection.
|
||||
- `ktx ingest --text "..."` (or `--file <path>`) captures notes into KTX
|
||||
memory instead of ingesting a connection.
|
||||
|
||||
## `ktx ingest` Options
|
||||
Database connections run before context-source connections when more than one
|
||||
connection is selected.
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--all` | Ingest all configured connections | `false` |
|
||||
| `--all` | Ingest all configured connections (same as bare invocation) | `false` |
|
||||
| `--fast` | Use deterministic database schema ingest | Stored connection default, or `fast` |
|
||||
| `--deep` | Use AI-enriched database ingest | Stored connection default, or `fast` |
|
||||
| `--query-history` | Include database query-history usage patterns | Stored connection default |
|
||||
| `--no-query-history` | Skip database query-history usage patterns for this run | Stored connection default |
|
||||
| `--query-history-window-days <days>` | BigQuery/Snowflake query-history lookback window for this run | Stored connection default |
|
||||
| `--text <content>` | Capture inline text into KTX memory; repeatable | `[]` |
|
||||
| `--file <path>` | Capture a text file into KTX memory; use `-` for stdin; repeatable | `[]` |
|
||||
| `--connection-id <connectionId>` | KTX connection id to tag captured text/file notes | - |
|
||||
| `--user-id <id>` | Memory user id for text/file capture attribution | `local-cli` |
|
||||
| `--fail-fast` | Stop after the first failed text/file item | `false` |
|
||||
| `--plain` | Print plain text output | `true` |
|
||||
| `--json` | Print JSON output | `false` |
|
||||
| `--yes` | Install required managed runtime features without prompting | `false` |
|
||||
|
|
@ -42,8 +51,8 @@ Postgres reads the current `pg_stat_statements` aggregate data instead of a
|
|||
time-windowed history table. Query-history ingest runs after schema ingest and
|
||||
requires deep ingest readiness.
|
||||
|
||||
When `--all` selects both databases and context sources, database ingest runs
|
||||
first, then source ingest and memory updates run for source connections.
|
||||
When more than one connection is selected, database ingest runs first, then
|
||||
source ingest and memory updates run for source connections.
|
||||
|
||||
Some ingest paths use the managed KTX Python runtime. Query-history ingest uses
|
||||
it for SQL analysis, and Looker source ingest uses it for Looker identifier
|
||||
|
|
@ -51,23 +60,15 @@ parsing. In an interactive terminal, `ktx ingest` prompts before installing the
|
|||
required runtime features. Use `--yes` to install them without prompting, or
|
||||
use `--no-input` to fail fast with install guidance.
|
||||
|
||||
## `ktx ingest text` Options
|
||||
|
||||
Use `ktx ingest text` to capture free-form text artifacts into KTX memory.
|
||||
Provide files, pass `--text` one or more times, or use `-` as a file argument to
|
||||
read one item from stdin.
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--text <content>` | Text content to ingest; repeat for a batch | `[]` |
|
||||
| `--connection-id <connectionId>` | Optional KTX connection id for semantic-layer capture | - |
|
||||
| `--user-id <id>` | Memory user id for capture attribution | `local-cli` |
|
||||
| `--json` | Print JSON output | `false` |
|
||||
| `--fail-fast` | Stop after the first failed text item | `false` |
|
||||
`--text` and `--file` cannot be combined with a positional `connectionId` or
|
||||
`--all`; pass `--connection-id <id>` instead to tag captured notes.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Build every configured connection (bare = --all)
|
||||
ktx ingest
|
||||
|
||||
# Build one database or source connection
|
||||
ktx ingest warehouse
|
||||
|
||||
|
|
@ -85,15 +86,17 @@ ktx ingest warehouse --query-history-window-days 30
|
|||
# Build a source connection
|
||||
ktx ingest notion
|
||||
|
||||
# Build all configured connections
|
||||
ktx ingest --all
|
||||
ktx ingest --all --deep
|
||||
# Capture inline text into memory
|
||||
ktx ingest --text "Refunds are excluded from net revenue."
|
||||
|
||||
# Capture local Markdown notes into memory
|
||||
ktx ingest text docs/revenue-notes.md --connection-id warehouse
|
||||
# Capture multiple text snippets in one call
|
||||
ktx ingest --text "Revenue is gross receipts." --text "Orders are completed purchases."
|
||||
|
||||
# Capture a local Markdown file into memory and tag it to a connection
|
||||
ktx ingest --file docs/revenue-notes.md --connection-id warehouse
|
||||
|
||||
# Capture one stdin item
|
||||
printf "Refunds are excluded from net revenue." | ktx ingest text -
|
||||
printf "Refunds are excluded from net revenue." | ktx ingest --file -
|
||||
```
|
||||
|
||||
## Output
|
||||
|
|
@ -154,6 +157,5 @@ KTX_INGEST_TRACE_LEVEL=trace ktx ingest metabase
|
|||
| Deep readiness is missing | `--deep` or query history needs model, embedding, and scan-enrichment configuration | Run `ktx setup` or rerun with `--fast` |
|
||||
| Query history is unsupported | The selected database driver does not support query history | Run schema ingest without query-history flags |
|
||||
| Python runtime is missing | The selected ingest target needs runtime-backed SQL analysis or source parsing | Accept the interactive prompt, rerun with `--yes`, or run the suggested `ktx admin runtime install` command |
|
||||
| No ingest target was selected | No connection id was provided and `--all` was omitted | Run `ktx ingest <connectionId>` or `ktx ingest --all` |
|
||||
| Source options were ignored | Depth and query-history flags were supplied for a non-database source | Omit database-only flags when ingesting source connections |
|
||||
| Text ingest stops early | `--fail-fast` was used and one item failed | Fix the failed item or rerun without `--fail-fast` to collect all failures |
|
||||
|
|
|
|||
|
|
@ -10,34 +10,33 @@ the vocabulary agents use to generate correct SQL.
|
|||
## Command signature
|
||||
|
||||
```bash
|
||||
ktx sl <subcommand> [options]
|
||||
ktx sl [options] [query...] # list (bare) or search (with query)
|
||||
ktx sl validate <sourceName> [options]
|
||||
ktx sl query [options]
|
||||
```
|
||||
|
||||
- Bare `ktx sl` lists semantic-layer sources.
|
||||
- `ktx sl <query...>` searches semantic-layer sources (multi-word queries are
|
||||
joined with a space).
|
||||
- `ktx sl validate` and `ktx sl query` remain as explicit subcommands.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Subcommand | Description |
|
||||
|-----------|-------------|
|
||||
| `list` | List semantic-layer sources |
|
||||
| `search <query>` | Search semantic-layer sources |
|
||||
| (none, no query) | List semantic-layer sources |
|
||||
| (none, with query) | Search semantic-layer sources |
|
||||
| `validate <sourceName>` | Validate a semantic-layer source against the database schema |
|
||||
| `query` | Compile or execute a Semantic Query |
|
||||
|
||||
## Options
|
||||
|
||||
### `sl list`
|
||||
### `sl` (list or search)
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--connection-id <id>` | Filter by KTX connection id | - |
|
||||
| `--output <mode>` | Output mode: `pretty` (default in TTY), `plain` (TSV), or `json` | `pretty` |
|
||||
| `--json` | Shortcut for `--output=json` (overrides `--output`) | `false` |
|
||||
|
||||
### `sl search`
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--connection-id <id>` | Filter by KTX connection id | - |
|
||||
| `--limit <number>` | Maximum search results | - |
|
||||
| `--limit <number>` | Maximum search results (search mode only) | - |
|
||||
| `--output <mode>` | Output mode: `pretty` (default in TTY), `plain` (TSV), or `json` | `pretty` |
|
||||
| `--json` | Shortcut for `--output=json` (overrides `--output`) | `false` |
|
||||
|
||||
|
|
@ -73,16 +72,16 @@ ktx sl <subcommand> [options]
|
|||
|
||||
```bash
|
||||
# List all semantic sources
|
||||
ktx sl list
|
||||
ktx sl
|
||||
|
||||
# List sources for a specific connection
|
||||
ktx sl list --connection-id my-warehouse
|
||||
ktx sl --connection-id my-warehouse
|
||||
|
||||
# List sources as JSON
|
||||
ktx sl list --json
|
||||
ktx sl --json
|
||||
|
||||
# Search sources as JSON
|
||||
ktx sl search "revenue" --json
|
||||
ktx sl "revenue" --json
|
||||
|
||||
# Validate a source against the live schema
|
||||
ktx sl validate orders --connection-id my-warehouse
|
||||
|
|
@ -137,13 +136,13 @@ ktx sl query \
|
|||
|
||||
## Output
|
||||
|
||||
Semantic-layer list and search commands return human-readable output by
|
||||
default. Use `--json` on `list` or `search` when an agent needs structured
|
||||
output. Use `--format sql` on `query` to inspect generated SQL before
|
||||
execution, or leave `--format json` for the compiled query and optional rows.
|
||||
Pretty `sl search` output shows `#1`, `#2`, and later rank badges for the
|
||||
displayed results. Plain and JSON output keep the raw `score` value, which is a
|
||||
ranking score rather than a percentage.
|
||||
Bare `ktx sl` (list) and `ktx sl <query>` (search) return human-readable
|
||||
output by default. Use `--json` when an agent needs structured output. Use
|
||||
`--format sql` on `query` to inspect generated SQL before execution, or leave
|
||||
`--format json` for the compiled query and optional rows. Pretty search output
|
||||
shows `#1`, `#2`, and later rank badges for the displayed results. Plain and
|
||||
JSON output keep the raw `score` value, which is a ranking score rather than a
|
||||
percentage.
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -161,8 +160,8 @@ ranking score rather than a percentage.
|
|||
|
||||
| Error | Cause | Recovery |
|
||||
|-------|-------|----------|
|
||||
| Source not found | Source name or connection id is wrong | Run `ktx sl list --json` and retry with an exact source name and connection id |
|
||||
| Source not found | Source name or connection id is wrong | Run `ktx sl --json` and retry with an exact source name and connection id |
|
||||
| Validation fails | YAML references missing columns, invalid joins, or invalid SQL expressions | Fix the source YAML and rerun `ktx sl validate` |
|
||||
| Query compile fails | Measure, dimension, filter, or segment name is invalid | Search sources with `ktx sl search`, inspect the source YAML in your project files, then retry using declared fields |
|
||||
| Query compile fails | Measure, dimension, filter, or segment name is invalid | Search sources with `ktx sl <query>`, inspect the source YAML in your project files, then retry using declared fields |
|
||||
| Execution returns too many rows | `--max-rows` is missing or too high | Add `--max-rows` with a bounded value before executing |
|
||||
| Runtime install is blocked | Query execution needs the managed Python runtime and prompts are disabled | Run `ktx admin runtime install --feature core --yes`, or rerun `ktx sl query --yes` |
|
||||
|
|
|
|||
|
|
@ -10,42 +10,28 @@ them for context when answering questions about your data.
|
|||
## Command signature
|
||||
|
||||
```bash
|
||||
ktx wiki <subcommand> [options]
|
||||
ktx wiki [options] [query...]
|
||||
```
|
||||
|
||||
## Subcommands
|
||||
- Bare `ktx wiki` lists local wiki pages.
|
||||
- `ktx wiki <query...>` searches local wiki pages (multi-word queries are
|
||||
joined with a space).
|
||||
|
||||
| Subcommand | Description |
|
||||
|-----------|-------------|
|
||||
| `list` | List local wiki pages |
|
||||
| `search <query>` | Search local wiki pages |
|
||||
|
||||
The current public CLI lists and searches wiki pages. Edit the Markdown files
|
||||
under `wiki/` directly, or ingest source content with `ktx ingest`, when you
|
||||
need to add or update wiki knowledge.
|
||||
Edit the Markdown files under `wiki/` directly, or ingest source content with
|
||||
`ktx ingest`, when you need to add or update wiki knowledge.
|
||||
|
||||
## Options
|
||||
|
||||
### `wiki list`
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--user-id <id>` | Local user id | `local` |
|
||||
| `--limit <number>` | Maximum search results (search mode only) | - |
|
||||
| `--output <mode>` | Output mode: `pretty` (default in TTY), `plain` (TSV), or `json` | `pretty` |
|
||||
| `--json` | Shortcut for `--output=json` (overrides `--output`) | `false` |
|
||||
|
||||
### `wiki search`
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--user-id <id>` | Local user id | `local` |
|
||||
| `--limit <number>` | Maximum search results | - |
|
||||
| `--output <mode>` | Output mode: `pretty` (default in TTY), `plain` (TSV), or `json` | `pretty` |
|
||||
| `--json` | Shortcut for `--output=json` (overrides `--output`) | `false` |
|
||||
|
||||
`wiki search` uses hybrid search when `storage.search` is `sqlite-fts5`. KTX
|
||||
combines lexical SQLite FTS5 matches, token matches, and semantic matches from
|
||||
wiki page embeddings stored in `.ktx/db.sqlite`. If embeddings are not
|
||||
`ktx wiki <query>` uses hybrid search when `storage.search` is `sqlite-fts5`.
|
||||
KTX combines lexical SQLite FTS5 matches, token matches, and semantic matches
|
||||
from wiki page embeddings stored in `.ktx/db.sqlite`. If embeddings are not
|
||||
configured or the embedding backend is unavailable, KTX skips the semantic lane
|
||||
and keeps lexical and token results.
|
||||
|
||||
|
|
@ -53,22 +39,22 @@ and keeps lexical and token results.
|
|||
|
||||
```bash
|
||||
# List all wiki pages
|
||||
ktx wiki list
|
||||
ktx wiki
|
||||
|
||||
# List all wiki pages as JSON
|
||||
ktx wiki list --json
|
||||
ktx wiki --json
|
||||
|
||||
# Search wiki pages
|
||||
ktx wiki search "monthly recurring revenue"
|
||||
ktx wiki "monthly recurring revenue"
|
||||
|
||||
# Search wiki pages as JSON
|
||||
ktx wiki search "monthly recurring revenue" --json --limit 10
|
||||
ktx wiki "monthly recurring revenue" --json --limit 10
|
||||
|
||||
# Print search results as TSV
|
||||
ktx wiki search "monthly recurring revenue" --output plain
|
||||
ktx wiki "monthly recurring revenue" --output plain
|
||||
|
||||
# Inspect which search lanes were used
|
||||
ktx --debug wiki search "monthly recurring revenue" --json
|
||||
ktx --debug wiki "monthly recurring revenue" --json
|
||||
```
|
||||
|
||||
## Output
|
||||
|
|
|
|||
|
|
@ -91,11 +91,11 @@ ktx status
|
|||
ktx ingest warehouse
|
||||
|
||||
# Build every configured connection
|
||||
ktx ingest --all
|
||||
ktx ingest
|
||||
|
||||
# Search semantic-layer sources and wiki pages
|
||||
ktx sl search "revenue"
|
||||
ktx wiki search "revenue recognition"
|
||||
ktx sl "revenue"
|
||||
ktx wiki "revenue recognition"
|
||||
|
||||
# Execute read-only SQL
|
||||
ktx sql --connection warehouse "select count(*) from public.orders"
|
||||
|
|
|
|||
|
|
@ -106,28 +106,30 @@ edits.
|
|||
|
||||
## Text ingest
|
||||
|
||||
Use `ktx ingest text` for notes, Markdown, runbooks, Slack exports, or other
|
||||
searchable memory.
|
||||
Use `ktx ingest --text` / `ktx ingest --file` for notes, Markdown, runbooks,
|
||||
Slack exports, or other searchable memory.
|
||||
|
||||
```bash
|
||||
# Capture a Markdown file
|
||||
ktx ingest text docs/revenue-notes.md --connection-id warehouse
|
||||
ktx ingest --file docs/revenue-notes.md --connection-id warehouse
|
||||
|
||||
# Capture one stdin item
|
||||
printf "Refunds are excluded from net revenue." | ktx ingest text -
|
||||
printf "Refunds are excluded from net revenue." | ktx ingest --file -
|
||||
|
||||
# Capture direct text
|
||||
ktx ingest text --text "ARR excludes one-time implementation fees."
|
||||
ktx ingest --text "ARR excludes one-time implementation fees."
|
||||
```
|
||||
|
||||
Useful flags:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--text <content>` | Capture inline text into memory; repeatable |
|
||||
| `--file <path>` | Capture a text file (or `-` for stdin) into memory; repeatable |
|
||||
| `--connection-id <connectionId>` | Attach the captured memory to a KTX connection |
|
||||
| `--user-id <id>` | Attribute capture to a user scope, default `local-cli` |
|
||||
| `--json` | Print structured output |
|
||||
| `--fail-fast` | Stop after the first failed text item |
|
||||
| `--fail-fast` | Stop after the first failed text/file item |
|
||||
|
||||
Use text ingest for small, high-signal documents. Prefer configured source
|
||||
ingest for Notion, dbt, Metabase, and similar systems.
|
||||
|
|
@ -165,8 +167,8 @@ Then inspect what changed:
|
|||
|
||||
```bash
|
||||
git status --short
|
||||
ktx sl list --json
|
||||
ktx wiki search "revenue" --json --limit 10
|
||||
ktx sl --json
|
||||
ktx wiki "revenue" --json --limit 10
|
||||
```
|
||||
|
||||
## Common errors
|
||||
|
|
@ -176,6 +178,6 @@ ktx wiki search "revenue" --json --limit 10
|
|||
| Connection not configured | The connection id is missing from `ktx.yaml` | Add it with `ktx setup` |
|
||||
| Deep readiness is missing | LLM or embeddings are not setup-ready | Run `ktx setup`, or rerun with `--fast` |
|
||||
| Query history is unsupported | The selected database driver does not expose query history | Run schema ingest without query-history flags |
|
||||
| No target selected | You omitted both a connection id and `--all` | Run `ktx ingest <connectionId>` or `ktx ingest --all` |
|
||||
| No connections configured | The project has no entries under `connections` | Run `ktx setup` and add a database or source connection |
|
||||
| Source flags have no effect | Depth and query-history flags were supplied for a source connector | Use those flags only for database connections |
|
||||
| Text ingest stops early | `--fail-fast` stopped on the first failed item | Fix the item or rerun without `--fail-fast` |
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@ context-build, and agent-integration readiness.
|
|||
### Semantic layer discovery
|
||||
|
||||
```bash
|
||||
ktx sl list --json
|
||||
ktx sl list --connection-id warehouse --json
|
||||
ktx sl search "revenue" --json --limit 10
|
||||
ktx sl --json
|
||||
ktx sl --connection-id warehouse --json
|
||||
ktx sl "revenue" --json --limit 10
|
||||
```
|
||||
|
||||
Use these commands to find source names, connection ids, measures, dimensions,
|
||||
|
|
@ -99,8 +99,8 @@ For complex calls, agents can write a JSON query object and pass it with
|
|||
### Wiki context
|
||||
|
||||
```bash
|
||||
ktx wiki list --json
|
||||
ktx wiki search "revenue recognition" --json --limit 10
|
||||
ktx wiki --json
|
||||
ktx wiki "revenue recognition" --json --limit 10
|
||||
```
|
||||
|
||||
Search wiki context for business definitions, metric caveats, process rules, and
|
||||
|
|
@ -112,8 +112,8 @@ Agents can refresh context when the user asks them to:
|
|||
|
||||
```bash
|
||||
ktx ingest warehouse --fast
|
||||
ktx ingest --all
|
||||
ktx ingest text docs/revenue-notes.md --connection-id warehouse
|
||||
ktx ingest
|
||||
ktx ingest --file docs/revenue-notes.md --connection-id warehouse
|
||||
```
|
||||
|
||||
Use `--deep` only when LLM and embedding setup is ready.
|
||||
|
|
@ -123,7 +123,7 @@ Use `--deep` only when LLM and embedding setup is ready.
|
|||
Agents should:
|
||||
|
||||
- Run `ktx status --json` before using KTX context.
|
||||
- Use `ktx sl search` and `ktx wiki search` before writing SQL from memory.
|
||||
- Use `ktx sl <query>` and `ktx wiki <query>` before writing SQL from memory.
|
||||
- Inspect the relevant YAML or Markdown files after search returns candidates.
|
||||
- Compile SQL with `ktx sl query --format sql` before executing.
|
||||
- Use `--max-rows` whenever executing a live query.
|
||||
|
|
@ -156,5 +156,5 @@ For per-client notes, see [Agent Clients](/docs/integrations/agent-clients).
|
|||
| Agent says KTX is unavailable | Agent did not load the generated instruction file | Rerun `ktx setup --agents --target <target>` and restart the agent session |
|
||||
| Agent command cannot find the project | Agent is running outside the KTX directory | Add `--project-dir <path>` or open the agent in the project root |
|
||||
| Generated rules point at a missing CLI path | CLI was moved, rebuilt, or reinstalled | Rerun `ktx setup --agents` |
|
||||
| Agent cannot find a metric | Context is missing or stale | Run `ktx sl search`, inspect source YAML, then refresh with `ktx ingest` if needed |
|
||||
| Agent cannot find a metric | Context is missing or stale | Run `ktx sl <query>`, inspect source YAML, then refresh with `ktx ingest` if needed |
|
||||
| Agent query returns too many rows | The command executed without a result cap | Require `--max-rows` for executed queries |
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ Use this order for most context changes:
|
|||
1. Discover existing context.
|
||||
|
||||
```bash
|
||||
ktx sl list --json
|
||||
ktx sl search "revenue" --json
|
||||
ktx wiki search "revenue recognition" --json --limit 10
|
||||
ktx sl --json
|
||||
ktx sl "revenue" --json
|
||||
ktx wiki "revenue recognition" --json --limit 10
|
||||
```
|
||||
|
||||
2. Edit the smallest relevant files under `semantic-layer/<connection-id>/` or
|
||||
|
|
@ -306,7 +306,7 @@ Useful frontmatter:
|
|||
1. Search first.
|
||||
|
||||
```bash
|
||||
ktx wiki search "active customer definition" --json --limit 10
|
||||
ktx wiki "active customer definition" --json --limit 10
|
||||
```
|
||||
|
||||
2. If no page covers the rule, create or edit a Markdown file under
|
||||
|
|
@ -323,8 +323,8 @@ Before accepting agent-written context:
|
|||
```bash
|
||||
git diff -- semantic-layer wiki
|
||||
ktx sl validate orders --connection-id warehouse
|
||||
ktx sl search "revenue" --json
|
||||
ktx wiki search "revenue recognition" --json --limit 10
|
||||
ktx sl "revenue" --json
|
||||
ktx wiki "revenue recognition" --json --limit 10
|
||||
```
|
||||
|
||||
Check definitions, hidden columns, join relationships, and generated SQL.
|
||||
|
|
|
|||
|
|
@ -130,10 +130,10 @@ description: Use local KTX semantic context and wiki knowledge for this project.
|
|||
|
||||
Available commands:
|
||||
- `ktx status --json --project-dir /path/to/project`
|
||||
- `ktx sl list --json --project-dir /path/to/project`
|
||||
- `ktx sl search '<text>' --json --project-dir /path/to/project --connection-id '<id>'`
|
||||
- `ktx sl --json --project-dir /path/to/project`
|
||||
- `ktx sl '<text>' --json --project-dir /path/to/project --connection-id '<id>'`
|
||||
- `ktx sl query --project-dir /path/to/project --connection-id '<id>' --query-file '<path>' --format json --execute --max-rows 100`
|
||||
- `ktx wiki search '<query>' --json --project-dir /path/to/project --limit 10`
|
||||
- `ktx wiki '<query>' --json --project-dir /path/to/project --limit 10`
|
||||
```
|
||||
|
||||
### Workflow tips
|
||||
|
|
@ -281,9 +281,9 @@ Admin CLI skills call the same KTX CLI commands:
|
|||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `ktx status --json` | Return project setup and context readiness |
|
||||
| `ktx wiki search <query> --json` | Search wiki pages |
|
||||
| `ktx sl list --json` | List semantic-layer sources |
|
||||
| `ktx sl search <query> --json` | Search semantic-layer sources |
|
||||
| `ktx wiki <query> --json` | Search wiki pages |
|
||||
| `ktx sl --json` | List semantic-layer sources |
|
||||
| `ktx sl <query> --json` | Search semantic-layer sources |
|
||||
| `ktx sl validate <source> --connection-id <id>` | Validate semantic source definitions |
|
||||
| `ktx sl query --format json` | Execute a Semantic Query when semantic compute is configured |
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { type Command } from '@commander-js/extra-typings';
|
|||
import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KtxConnectionArgs } from '../connection.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
import { resolveConnectionSelection } from './connection-selection.js';
|
||||
|
||||
profileMark('module:commands/connection-commands');
|
||||
|
||||
|
|
@ -18,7 +19,10 @@ export function registerConnectionCommands(program: Command, context: KtxCliComm
|
|||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the nearest ktx.yaml or current working directory.\n',
|
||||
);
|
||||
)
|
||||
.action(async (_options: unknown, command) => {
|
||||
await runConnectionArgs(context, { command: 'list', projectDir: resolveCommandProjectDir(command) });
|
||||
});
|
||||
connection.hook('preAction', (_thisCommand, actionCommand) => {
|
||||
context.writeDebug?.(commandName, actionCommand);
|
||||
});
|
||||
|
|
@ -32,25 +36,22 @@ export function registerConnectionCommands(program: Command, context: KtxCliComm
|
|||
|
||||
connection
|
||||
.command('test')
|
||||
.description('Test a configured connection')
|
||||
.argument('[connectionId]', 'KTX connection id (omit when --all is set)')
|
||||
.description('Test one or all configured connections (default: all)')
|
||||
.argument('[connectionId]', 'KTX connection id to test (omit to test all)')
|
||||
.option('--all', 'Test every configured connection and print a summary list')
|
||||
.action(async (connectionId: string | undefined, options: { all?: boolean }, command) => {
|
||||
const all = options.all === true;
|
||||
if (all && connectionId !== undefined) {
|
||||
if (options.all === true && connectionId !== undefined) {
|
||||
command.error('error: --all cannot be combined with a connection id argument');
|
||||
}
|
||||
if (!all && connectionId === undefined) {
|
||||
command.error('error: missing required argument <connectionId> (or pass --all)');
|
||||
}
|
||||
if (all) {
|
||||
const selection = resolveConnectionSelection({ connectionId, all: options.all === true });
|
||||
if (selection.kind === 'all') {
|
||||
await runConnectionArgs(context, { command: 'test-all', projectDir: resolveCommandProjectDir(command) });
|
||||
return;
|
||||
}
|
||||
await runConnectionArgs(context, {
|
||||
command: 'test',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
connectionId: connectionId as string,
|
||||
connectionId: selection.connectionId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
18
packages/cli/src/commands/connection-selection.ts
Normal file
18
packages/cli/src/commands/connection-selection.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export type ConnectionSelection =
|
||||
| { kind: 'all' }
|
||||
| { kind: 'single'; connectionId: string };
|
||||
|
||||
export interface ResolveConnectionSelectionInput {
|
||||
connectionId?: string | undefined;
|
||||
all: boolean;
|
||||
}
|
||||
|
||||
export function resolveConnectionSelection(input: ResolveConnectionSelectionInput): ConnectionSelection {
|
||||
if (input.all && input.connectionId !== undefined) {
|
||||
throw new Error('--all cannot be combined with a connection id argument');
|
||||
}
|
||||
if (input.connectionId !== undefined) {
|
||||
return { kind: 'single', connectionId: input.connectionId };
|
||||
}
|
||||
return { kind: 'all' };
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';
|
|||
import type { KtxPublicIngestArgs } from '../public-ingest.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
import type { KtxTextIngestArgs } from '../text-ingest.js';
|
||||
import { resolveConnectionSelection } from './connection-selection.js';
|
||||
|
||||
profileMark('module:commands/ingest-commands');
|
||||
|
||||
|
|
@ -24,15 +25,20 @@ export function registerIngestCommands(
|
|||
): void {
|
||||
const ingest = program
|
||||
.command('ingest')
|
||||
.description('Build or inspect KTX context')
|
||||
.description('Build or inspect KTX context, or capture text into memory')
|
||||
.usage('[options] [connectionId]')
|
||||
.argument('[connectionId]', 'Configured connection id to ingest')
|
||||
.argument('[connectionId]', 'Configured connection id to ingest (omit to ingest all)')
|
||||
.option('--all', 'Ingest all configured connections', false)
|
||||
.addOption(new Option('--fast', 'Use deterministic database schema ingest').conflicts('deep'))
|
||||
.addOption(new Option('--deep', 'Use AI-enriched database ingest').conflicts('fast'))
|
||||
.addOption(new Option('--query-history', 'Include database query-history usage patterns').conflicts('noQueryHistory'))
|
||||
.addOption(new Option('--no-query-history', 'Skip database query-history usage patterns'))
|
||||
.option('--query-history-window-days <days>', 'Query-history lookback window for this run', parsePositiveIntegerOption)
|
||||
.option('--text <content>', 'Capture inline text into KTX memory; repeatable', collectOption, [])
|
||||
.option('--file <path>', 'Capture a text file into KTX memory; use - for stdin; repeatable', collectOption, [])
|
||||
.option('--connection-id <connectionId>', 'KTX connection id to tag captured text/file notes')
|
||||
.option('--user-id <id>', 'Memory user id for text/file capture attribution', 'local-cli')
|
||||
.option('--fail-fast', 'Stop after the first failed text/file item', false)
|
||||
.addOption(new Option('--plain', 'Print plain text output').conflicts(['json']))
|
||||
.addOption(new Option('--json', 'Print JSON output').conflicts(['plain']))
|
||||
.option('--yes', 'Install required managed runtime features without prompting')
|
||||
|
|
@ -40,14 +46,45 @@ export function registerIngestCommands(
|
|||
.showHelpAfterError();
|
||||
|
||||
ingest.action(async (connectionId: string | undefined, options, command) => {
|
||||
const projectDir = resolveCommandProjectDir(command);
|
||||
const hasTextCapture = options.text.length > 0 || options.file.length > 0;
|
||||
|
||||
if (hasTextCapture) {
|
||||
if (connectionId !== undefined) {
|
||||
command.error(
|
||||
'error: --text/--file does not accept a positional connection id; use --connection-id <id> to tag captured notes',
|
||||
);
|
||||
}
|
||||
if (options.all === true) {
|
||||
command.error('error: --all cannot be combined with --text or --file');
|
||||
}
|
||||
context.setExitCode(
|
||||
await commandOptions.runTextIngest(
|
||||
{
|
||||
projectDir,
|
||||
texts: options.text,
|
||||
files: options.file,
|
||||
...(options.connectionId ? { connectionId: options.connectionId } : {}),
|
||||
userId: options.userId,
|
||||
json: options.json === true,
|
||||
failFast: options.failFast === true,
|
||||
},
|
||||
context.io,
|
||||
context.deps,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = resolveConnectionSelection({ connectionId, all: options.all === true });
|
||||
const { runKtxPublicIngest } = await import('../public-ingest.js');
|
||||
const queryHistory =
|
||||
options.queryHistory === true ? 'enabled' : options.queryHistory === false ? 'disabled' : 'default';
|
||||
const args: KtxPublicIngestArgs = {
|
||||
command: 'run',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
...(connectionId ? { targetConnectionId: connectionId } : {}),
|
||||
all: options.all === true,
|
||||
projectDir,
|
||||
...(selection.kind === 'single' ? { targetConnectionId: selection.connectionId } : {}),
|
||||
all: selection.kind === 'all',
|
||||
json: options.json === true,
|
||||
inputMode: options.input === false ? 'disabled' : 'auto',
|
||||
...(options.fast === true ? { depth: 'fast' as const } : {}),
|
||||
|
|
@ -63,32 +100,4 @@ export function registerIngestCommands(
|
|||
ingest.hook('preAction', (_thisCommand, actionCommand) => {
|
||||
context.writeDebug?.('ingest', actionCommand);
|
||||
});
|
||||
|
||||
ingest
|
||||
.command('text')
|
||||
.description('Ingest free-form text artifacts into KTX memory')
|
||||
.argument('[files...]', 'Files to ingest; use - to read one item from stdin')
|
||||
.option('--text <content>', 'Text content to ingest; repeat for a batch', collectOption, [])
|
||||
.option('--connection-id <connectionId>', 'Optional KTX connection id for semantic-layer capture')
|
||||
.option('--user-id <id>', 'Memory user id for capture attribution', 'local-cli')
|
||||
.option('--json', 'Print JSON output')
|
||||
.option('--fail-fast', 'Stop after the first failed text item', false)
|
||||
.action(async (files: string[], options, command) => {
|
||||
const parentOptions = command.parent?.opts() as { json?: boolean } | undefined;
|
||||
context.setExitCode(
|
||||
await commandOptions.runTextIngest(
|
||||
{
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
texts: options.text,
|
||||
files,
|
||||
...(options.connectionId ? { connectionId: options.connectionId } : {}),
|
||||
userId: options.userId,
|
||||
json: options.json === true || parentOptions?.json === true,
|
||||
failFast: options.failFast === true,
|
||||
},
|
||||
context.io,
|
||||
context.deps,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,59 +21,29 @@ function isDebugEnabled(command: CommandWithGlobalOptions): boolean {
|
|||
}
|
||||
|
||||
export function registerWikiCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const wiki = program
|
||||
program
|
||||
.command('wiki')
|
||||
.description('List or search local wiki pages')
|
||||
.usage('[options] [query...]')
|
||||
.argument('[query...]', 'Search query; omit to list all pages')
|
||||
.option('--user-id <id>', 'Local user id', 'local')
|
||||
.option('--limit <number>', 'Maximum search results (search mode only)', parsePositiveIntegerOption)
|
||||
.addOption(
|
||||
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
||||
'pretty',
|
||||
'plain',
|
||||
'json',
|
||||
]),
|
||||
)
|
||||
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
);
|
||||
|
||||
wiki
|
||||
.command('list')
|
||||
.description('List local wiki pages')
|
||||
.option('--user-id <id>', 'Local user id', 'local')
|
||||
.addOption(
|
||||
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
||||
'pretty',
|
||||
'plain',
|
||||
'json',
|
||||
]),
|
||||
)
|
||||
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
|
||||
.action(
|
||||
async (
|
||||
options: { userId: string; output?: 'pretty' | 'plain' | 'json'; json?: boolean },
|
||||
command,
|
||||
) => {
|
||||
await runKnowledgeArgs(context, {
|
||||
command: 'list',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
userId: options.userId,
|
||||
output: options.output,
|
||||
json: options.json,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
wiki
|
||||
.command('search')
|
||||
.description('Search local wiki pages')
|
||||
.argument('<query>', 'Search query')
|
||||
.option('--user-id <id>', 'Local user id', 'local')
|
||||
.option('--limit <number>', 'Maximum search results', parsePositiveIntegerOption)
|
||||
.addOption(
|
||||
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
||||
'pretty',
|
||||
'plain',
|
||||
'json',
|
||||
]),
|
||||
)
|
||||
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
|
||||
.action(
|
||||
async (
|
||||
query: string,
|
||||
query: string[],
|
||||
options: {
|
||||
userId: string;
|
||||
limit?: number;
|
||||
|
|
@ -82,10 +52,20 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
|
|||
},
|
||||
command,
|
||||
) => {
|
||||
if (query.length === 0) {
|
||||
await runKnowledgeArgs(context, {
|
||||
command: 'list',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
userId: options.userId,
|
||||
output: options.output,
|
||||
json: options.json,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await runKnowledgeArgs(context, {
|
||||
command: 'search',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
query,
|
||||
query: query.join(' '),
|
||||
userId: options.userId,
|
||||
output: options.output,
|
||||
json: options.json,
|
||||
|
|
|
|||
|
|
@ -36,8 +36,24 @@ function formatMcpStartResultMessage(input: { status: 'started' | 'already-runni
|
|||
].join('\n');
|
||||
}
|
||||
|
||||
async function printMcpStatus(context: KtxCliCommandContext, projectDir: string): Promise<void> {
|
||||
const status = await (context.deps.mcp?.readStatus ?? readKtxMcpDaemonStatus)({ projectDir });
|
||||
context.io.stdout.write(`${status.detail}\n`);
|
||||
if (status.kind === 'running') {
|
||||
context.io.stdout.write(`URL: ${status.url}\n`);
|
||||
context.io.stdout.write(`PID: ${status.state.pid}\n`);
|
||||
context.io.stdout.write(`Token auth: ${status.state.tokenAuth ? 'enabled' : 'disabled'}\n`);
|
||||
context.io.stdout.write(`Project: ${status.state.projectDir}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerMcpCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const mcp = program.command('mcp').description('Run the KTX MCP HTTP server');
|
||||
const mcp = program
|
||||
.command('mcp')
|
||||
.description('Manage the KTX MCP HTTP server (bare command: show status)')
|
||||
.action(async (_options, command) => {
|
||||
await printMcpStatus(context, resolveCommandProjectDir(command));
|
||||
});
|
||||
|
||||
mcp
|
||||
.command('stdio')
|
||||
|
|
@ -110,16 +126,7 @@ export function registerMcpCommands(program: Command, context: KtxCliCommandCont
|
|||
.command('status')
|
||||
.description('Show KTX MCP daemon status')
|
||||
.action(async (_options, command) => {
|
||||
const status = await (context.deps.mcp?.readStatus ?? readKtxMcpDaemonStatus)({
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
});
|
||||
context.io.stdout.write(`${status.detail}\n`);
|
||||
if (status.kind === 'running') {
|
||||
context.io.stdout.write(`URL: ${status.url}\n`);
|
||||
context.io.stdout.write(`PID: ${status.state.pid}\n`);
|
||||
context.io.stdout.write(`Token auth: ${status.state.tokenAuth ? 'enabled' : 'disabled'}\n`);
|
||||
context.io.stdout.write(`Project: ${status.state.projectDir}\n`);
|
||||
}
|
||||
await printMcpStatus(context, resolveCommandProjectDir(command));
|
||||
});
|
||||
|
||||
mcp
|
||||
|
|
|
|||
|
|
@ -42,59 +42,49 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
|
|||
const sl = program
|
||||
.command(commandName)
|
||||
.description('List, search, validate, or query local semantic-layer sources')
|
||||
.usage('[options] [query...]')
|
||||
.argument('[query...]', 'Search query; omit to list all sources')
|
||||
.option('--connection-id <id>', 'KTX connection id')
|
||||
.option('--limit <number>', 'Maximum search results (search mode only)', parsePositiveIntegerOption)
|
||||
.addOption(
|
||||
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
||||
'pretty',
|
||||
'plain',
|
||||
'json',
|
||||
]),
|
||||
)
|
||||
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
);
|
||||
|
||||
sl.command('list')
|
||||
.description('List semantic-layer sources')
|
||||
.option('--connection-id <id>', 'KTX connection id')
|
||||
.addOption(
|
||||
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
||||
'pretty',
|
||||
'plain',
|
||||
'json',
|
||||
]),
|
||||
)
|
||||
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
|
||||
.action(
|
||||
async (options: { connectionId?: string; output?: 'pretty' | 'plain' | 'json'; json?: boolean }, command) => {
|
||||
await runSlArgs(context, {
|
||||
command: 'list',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
connectionId: options.connectionId,
|
||||
output: options.output,
|
||||
json: options.json,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
sl.command('search')
|
||||
.description('Search semantic-layer sources')
|
||||
.argument('<query>', 'Search query')
|
||||
.option('--connection-id <id>', 'KTX connection id')
|
||||
.option('--limit <number>', 'Maximum search results', parsePositiveIntegerOption)
|
||||
.addOption(
|
||||
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
||||
'pretty',
|
||||
'plain',
|
||||
'json',
|
||||
]),
|
||||
)
|
||||
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
|
||||
.action(
|
||||
async (
|
||||
query: string,
|
||||
options: { connectionId?: string; limit?: number; output?: 'pretty' | 'plain' | 'json'; json?: boolean },
|
||||
query: string[],
|
||||
options: {
|
||||
connectionId?: string;
|
||||
limit?: number;
|
||||
output?: 'pretty' | 'plain' | 'json';
|
||||
json?: boolean;
|
||||
},
|
||||
command,
|
||||
) => {
|
||||
if (query.length === 0) {
|
||||
await runSlArgs(context, {
|
||||
command: 'list',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
connectionId: options.connectionId,
|
||||
output: options.output,
|
||||
json: options.json,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await runSlArgs(context, {
|
||||
command: 'search',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
connectionId: options.connectionId,
|
||||
query,
|
||||
query: query.join(' '),
|
||||
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
||||
output: options.output,
|
||||
json: options.json,
|
||||
|
|
@ -103,21 +93,24 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
|
|||
);
|
||||
|
||||
sl.command('validate')
|
||||
.description('Validate a semantic-layer source')
|
||||
.description('Validate a semantic-layer source (set --connection-id on `ktx sl`)')
|
||||
.argument('<sourceName>', 'Semantic-layer source name')
|
||||
.requiredOption('--connection-id <id>', 'KTX connection id')
|
||||
.action(async (sourceName: string, options: { connectionId: string }, command) => {
|
||||
.action(async (sourceName: string, _options, command) => {
|
||||
const parentOpts = command.parent?.opts() as { connectionId?: string } | undefined;
|
||||
const connectionId = parentOpts?.connectionId;
|
||||
if (connectionId === undefined) {
|
||||
command.error("error: required option '--connection-id <id>' not specified");
|
||||
}
|
||||
await runSlArgs(context, {
|
||||
command: 'validate',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
connectionId: options.connectionId,
|
||||
connectionId: connectionId as string,
|
||||
sourceName,
|
||||
});
|
||||
});
|
||||
|
||||
sl.command('query')
|
||||
.description('Compile or execute a semantic-layer query')
|
||||
.option('--connection-id <id>', 'KTX connection id')
|
||||
.description('Compile or execute a semantic-layer query (set --connection-id on `ktx sl`)')
|
||||
.option('--query-file <path>', 'JSON semantic-layer query file')
|
||||
.option('--measure <measure>', 'Measure to query; repeatable', collectOption, [])
|
||||
.option('--dimension <dimension>', 'Dimension to include; repeatable', collectOption, [])
|
||||
|
|
@ -135,10 +128,11 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
|
|||
if (options.measure.length === 0 && !options.queryFile) {
|
||||
throw new Error('sl query requires at least one --measure');
|
||||
}
|
||||
const parentOpts = command.parent?.opts() as { connectionId?: string } | undefined;
|
||||
const args = slQueryCommandSchema.parse({
|
||||
command: 'query',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
connectionId: options.connectionId,
|
||||
connectionId: parentOpts?.connectionId,
|
||||
...(options.queryFile
|
||||
? { queryFile: options.queryFile }
|
||||
: {
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ describe('formatDoctorReport', () => {
|
|||
expect(output).not.toContain('v22.16.0');
|
||||
expect(output).toContain('Everything ready.');
|
||||
expect(output).toContain('ktx status --json');
|
||||
expect(output).toContain('ktx sl list');
|
||||
expect(output).toContain('ktx wiki list');
|
||||
expect(output).toContain('ktx sl');
|
||||
expect(output).toContain('ktx wiki');
|
||||
expect(output).not.toContain('ktx scan');
|
||||
expect(output).not.toContain('ktx sl ask');
|
||||
});
|
||||
|
|
@ -561,8 +561,8 @@ describe('runKtxDoctor', () => {
|
|||
expect(out).toContain('info: pg_stat_statements.max is 1000');
|
||||
expect(out).not.toContain('Update the Postgres parameter group or config');
|
||||
expect(out).toContain('ktx status --json');
|
||||
expect(out).toContain('ktx sl list');
|
||||
expect(out).toContain('ktx wiki list');
|
||||
expect(out).toContain('ktx sl');
|
||||
expect(out).toContain('ktx wiki');
|
||||
expect(out).not.toContain('ktx scan');
|
||||
expect(out).not.toContain('ktx sl ask');
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
|
|
|
|||
|
|
@ -72,13 +72,13 @@ describe('standalone local warehouse example', () => {
|
|||
it('runs local CLI commands against the copied example project', async () => {
|
||||
const projectDir = await copyExampleProject(tempDir);
|
||||
|
||||
const knowledgeList = await runBuiltCli(['wiki', 'search', 'revenue', '--json', '--project-dir', projectDir]);
|
||||
const knowledgeList = await runBuiltCli(['wiki', 'revenue', '--json', '--project-dir', projectDir]);
|
||||
expect(knowledgeList).toMatchObject({ code: 0, stderr: '' });
|
||||
expect(
|
||||
parseJsonOutput<{ data: { items: Array<{ key: string; summary: string }> } }>(knowledgeList.stdout).data.items,
|
||||
).toContainEqual(expect.objectContaining({ key: 'revenue', summary: 'Paid order value after refunds' }));
|
||||
|
||||
const slList = await runBuiltCli(['sl', 'list', '--json', '--project-dir', projectDir, '--connection-id', 'warehouse']);
|
||||
const slList = await runBuiltCli(['sl', '--json', '--project-dir', projectDir, '--connection-id', 'warehouse']);
|
||||
expect(slList).toMatchObject({ code: 0, stderr: '' });
|
||||
expect(
|
||||
parseJsonOutput<{ data: { items: Array<{ connectionId: string; name: string; columnCount: number }> } }>(
|
||||
|
|
@ -110,7 +110,7 @@ describe('standalone local warehouse example', () => {
|
|||
'fake',
|
||||
]);
|
||||
expect(ingest).toMatchObject({ code: 1, stdout: '' });
|
||||
expect(ingest.stderr).toContain("unknown option '--connection-id'");
|
||||
expect(ingest.stderr).toContain("unknown option '--adapter'");
|
||||
}, 30_000);
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ describe('runKtxCli', () => {
|
|||
const knowledge = vi.fn(async () => 0);
|
||||
|
||||
const listIo = makeIo();
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'list', '--json'], listIo.io, { knowledge }))
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'wiki', '--json'], listIo.io, { knowledge }))
|
||||
.resolves.toBe(0);
|
||||
expect(knowledge).toHaveBeenCalledWith(
|
||||
{
|
||||
|
|
@ -163,7 +163,7 @@ describe('runKtxCli', () => {
|
|||
|
||||
const searchIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'wiki', 'search', 'revenue', '--limit', '5'], searchIo.io, { knowledge }),
|
||||
runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', '--limit', '5'], searchIo.io, { knowledge }),
|
||||
).resolves.toBe(0);
|
||||
expect(knowledge).toHaveBeenLastCalledWith(
|
||||
{
|
||||
|
|
@ -179,7 +179,7 @@ describe('runKtxCli', () => {
|
|||
|
||||
const debugSearchIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, '--debug', 'wiki', 'search', 'revenue'], debugSearchIo.io, { knowledge }),
|
||||
runKtxCli(['--project-dir', tempDir, '--debug', 'wiki', 'revenue'], debugSearchIo.io, { knowledge }),
|
||||
).resolves.toBe(0);
|
||||
expect(knowledge).toHaveBeenLastCalledWith(
|
||||
{
|
||||
|
|
@ -192,47 +192,57 @@ describe('runKtxCli', () => {
|
|||
},
|
||||
debugSearchIo.io,
|
||||
);
|
||||
|
||||
const multiWordIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', 'policy'], multiWordIo.io, { knowledge }),
|
||||
).resolves.toBe(0);
|
||||
expect(knowledge).toHaveBeenLastCalledWith(
|
||||
{
|
||||
command: 'search',
|
||||
projectDir: tempDir,
|
||||
query: 'revenue policy',
|
||||
userId: 'local',
|
||||
json: false,
|
||||
},
|
||||
multiWordIo.io,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects removed public wiki read and write commands', async () => {
|
||||
it('rejects unknown write-style flags on the flattened wiki and sl commands', async () => {
|
||||
const knowledge = vi.fn(async () => 0);
|
||||
|
||||
for (const argv of [
|
||||
['--project-dir', tempDir, 'wiki', 'read', 'revenue', '--json'],
|
||||
['--project-dir', tempDir, 'wiki', 'write', 'revenue', '--summary', 'Revenue', '--content', 'Revenue.'],
|
||||
]) {
|
||||
const io = makeIo();
|
||||
|
||||
await expect(runKtxCli(argv, io.io, { knowledge })).resolves.toBe(1);
|
||||
|
||||
expect(io.stderr()).toMatch(/unknown command|error:/);
|
||||
}
|
||||
|
||||
expect(knowledge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects removed public sl read/write commands', async () => {
|
||||
const sl = vi.fn(async () => 0);
|
||||
|
||||
for (const argv of [
|
||||
['--project-dir', tempDir, 'sl', 'read', 'orders', '--connection-id', 'warehouse'],
|
||||
['--project-dir', tempDir, 'sl', 'write', 'orders', '--connection-id', 'warehouse', '--yaml', 'name: orders'],
|
||||
]) {
|
||||
const io = makeIo();
|
||||
await expect(runKtxCli(argv, io.io, { sl })).resolves.toBe(1);
|
||||
expect(io.stderr()).toMatch(/unknown command|error:/);
|
||||
}
|
||||
const wikiIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'wiki', 'revenue', '--summary', 'Revenue', '--content', 'Revenue.'],
|
||||
wikiIo.io,
|
||||
{ knowledge },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
expect(wikiIo.stderr()).toMatch(/unknown option|error:/);
|
||||
expect(knowledge).not.toHaveBeenCalled();
|
||||
|
||||
const slIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'sl', 'orders', '--yaml', 'name: orders'],
|
||||
slIo.io,
|
||||
{ sl },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
expect(slIo.stderr()).toMatch(/unknown option|error:/);
|
||||
expect(sl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes sl search and rejects the old sl list --query flag', async () => {
|
||||
it('routes sl search via the flattened query positional and rejects unknown flags', async () => {
|
||||
const sl = vi.fn(async () => 0);
|
||||
|
||||
const searchIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'sl', 'search', 'revenue', '--connection-id', 'warehouse', '--limit', '5', '--json'],
|
||||
['--project-dir', tempDir, 'sl', 'revenue', '--connection-id', 'warehouse', '--limit', '5', '--json'],
|
||||
searchIo.io,
|
||||
{ sl },
|
||||
),
|
||||
|
|
@ -250,11 +260,26 @@ describe('runKtxCli', () => {
|
|||
searchIo.io,
|
||||
);
|
||||
|
||||
const listIo = makeIo();
|
||||
const bareIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'sl', 'list', '--query', 'revenue'], listIo.io, { sl }),
|
||||
runKtxCli(['--project-dir', tempDir, 'sl', '--connection-id', 'warehouse', '--json'], bareIo.io, { sl }),
|
||||
).resolves.toBe(0);
|
||||
expect(sl).toHaveBeenLastCalledWith(
|
||||
{
|
||||
command: 'list',
|
||||
projectDir: tempDir,
|
||||
connectionId: 'warehouse',
|
||||
json: true,
|
||||
output: undefined,
|
||||
},
|
||||
bareIo.io,
|
||||
);
|
||||
|
||||
const unknownIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'sl', '--query', 'revenue'], unknownIo.io, { sl }),
|
||||
).resolves.toBe(1);
|
||||
expect(listIo.stderr()).toContain("unknown option '--query'");
|
||||
expect(unknownIo.stderr()).toContain("unknown option '--query'");
|
||||
});
|
||||
|
||||
it('routes runtime management commands with the release runtime version', async () => {
|
||||
|
|
@ -524,7 +549,7 @@ describe('runKtxCli', () => {
|
|||
await initKtxProject({ projectDir });
|
||||
const commands = [
|
||||
['--project-dir', projectDir, 'status', '--json'],
|
||||
['--project-dir', projectDir, 'sl', 'list', '--json'],
|
||||
['--project-dir', projectDir, 'sl', '--json'],
|
||||
];
|
||||
|
||||
for (const argv of commands) {
|
||||
|
|
@ -872,7 +897,8 @@ describe('runKtxCli', () => {
|
|||
expect(testIo.stdout()).toContain('--query-history');
|
||||
expect(testIo.stdout()).toContain('--no-query-history');
|
||||
expect(testIo.stdout()).toContain('--query-history-window-days <days>');
|
||||
expect(testIo.stdout()).toContain('text');
|
||||
expect(testIo.stdout()).toContain('--text');
|
||||
expect(testIo.stdout()).toContain('--file');
|
||||
expect(testIo.stdout()).not.toMatch(/^ status\s/m);
|
||||
expect(testIo.stdout()).not.toMatch(/^ replay\s/m);
|
||||
expect(testIo.stdout()).not.toMatch(/^ run\s/m);
|
||||
|
|
@ -892,7 +918,6 @@ describe('runKtxCli', () => {
|
|||
'--project-dir',
|
||||
tempDir,
|
||||
'ingest',
|
||||
'text',
|
||||
'--text',
|
||||
'Revenue means gross receipts.',
|
||||
'--text',
|
||||
|
|
@ -924,19 +949,42 @@ describe('runKtxCli', () => {
|
|||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('documents text ingest inputs without a manifest option', async () => {
|
||||
it('rejects a positional connection id when --text is supplied', async () => {
|
||||
const textIngest = vi.fn(async () => 0);
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKtxCli(['ingest', 'text', '--help'], testIo.io, { textIngest })).resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'ingest', 'warehouse', '--text', 'hello'],
|
||||
testIo.io,
|
||||
{ textIngest, publicIngest },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: ktx ingest text [options] [files...]');
|
||||
expect(testIo.stdout()).toContain('--text <content>');
|
||||
expect(testIo.stdout()).toContain('--connection-id <connectionId>');
|
||||
expect(testIo.stdout()).toContain('--user-id <id>');
|
||||
expect(testIo.stdout()).toContain('--fail-fast');
|
||||
expect(testIo.stdout()).not.toContain('--manifest');
|
||||
expect(textIngest).not.toHaveBeenCalled();
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
expect(testIo.stderr()).toMatch(/--text\/--file does not accept a positional connection id/);
|
||||
});
|
||||
|
||||
it('treats bare ingest as ingest --all', async () => {
|
||||
const publicIngest = vi.fn().mockResolvedValue(0);
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'ingest', '--no-input'], testIo.io, { publicIngest }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(publicIngest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
command: 'run',
|
||||
projectDir: tempDir,
|
||||
all: true,
|
||||
}),
|
||||
testIo.io,
|
||||
);
|
||||
const args = publicIngest.mock.calls[0]?.[0] as { targetConnectionId?: string };
|
||||
expect(args.targetConnectionId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects old adapter-backed ingest flags at the top level and under admin', async () => {
|
||||
|
|
|
|||
|
|
@ -78,14 +78,14 @@ describe('printList — plain mode', () => {
|
|||
mode: 'plain',
|
||||
command: 'sl search',
|
||||
emptyMessage: 'No sources matched "foo"',
|
||||
emptyHint: 'Run `ktx sl list` to see available sources.',
|
||||
emptyHint: 'Run `ktx sl` to see available sources.',
|
||||
unit: 'source',
|
||||
io: r.io,
|
||||
});
|
||||
expect(r.out()).toBe('');
|
||||
expect(r.err()).toBe(
|
||||
'No sources matched "foo"\n' +
|
||||
'Run `ktx sl list` to see available sources.\n',
|
||||
'Run `ktx sl` to see available sources.\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -188,13 +188,13 @@ describe('printList — pretty mode', () => {
|
|||
mode: 'pretty',
|
||||
command: 'sl search',
|
||||
emptyMessage: 'No sources matched "foo"',
|
||||
emptyHint: 'Run `ktx sl list` to see available sources.',
|
||||
emptyHint: 'Run `ktx sl` to see available sources.',
|
||||
unit: 'source',
|
||||
io: r.io,
|
||||
});
|
||||
const out = stripAnsi(r.out());
|
||||
expect(out).toContain('No sources matched "foo"');
|
||||
expect(out).toContain('Run `ktx sl list` to see available sources.');
|
||||
expect(out).toContain('Run `ktx sl` to see available sources.');
|
||||
});
|
||||
|
||||
it('singularizes the footer when there is one row', () => {
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export async function runKtxKnowledge(
|
|||
}
|
||||
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
|
||||
let emptyMessage = `No local wiki pages matched "${args.query}"`;
|
||||
let emptyHint = 'Run `ktx wiki list` to inspect available pages.';
|
||||
let emptyHint = 'Run `ktx wiki` to inspect available pages.';
|
||||
if (results.length === 0 && mode !== 'json') {
|
||||
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
|
||||
if (pages.length === 0) {
|
||||
|
|
|
|||
|
|
@ -198,8 +198,8 @@ describe('MemoryFlowTuiApp', () => {
|
|||
expect(frame).toContain('order lifecycle');
|
||||
expect(frame).toContain('customer metrics');
|
||||
expect(frame).toContain('KTX finished ingesting your data');
|
||||
expect(frame).toContain('ktx sl list');
|
||||
expect(frame).toContain('ktx wiki list');
|
||||
expect(frame).toContain('ktx sl');
|
||||
expect(frame).toContain('ktx wiki');
|
||||
expect(frame).not.toContain('ktx serve --mcp stdio --user-id local');
|
||||
expect(frame).not.toContain(['ktx', 'ask'].join(' '));
|
||||
expect(frame).not.toContain(['ktx', 'mcp'].join(' '));
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ describe('KTX demo next steps', () => {
|
|||
it('uses supported context-build commands before agent usage', () => {
|
||||
expect(KTX_CONTEXT_BUILD_COMMANDS).toEqual([
|
||||
{
|
||||
command: 'ktx ingest --all',
|
||||
description: 'Build or refresh agent-ready context from configured connections',
|
||||
command: 'ktx ingest',
|
||||
description: 'Build or refresh agent-ready context from all configured connections',
|
||||
},
|
||||
{
|
||||
command: 'ktx status',
|
||||
|
|
@ -27,11 +27,11 @@ describe('KTX demo next steps', () => {
|
|||
description: 'Verify project setup and context readiness',
|
||||
},
|
||||
{
|
||||
command: 'ktx sl list',
|
||||
command: 'ktx sl',
|
||||
description: 'Inspect generated semantic-layer sources',
|
||||
},
|
||||
{
|
||||
command: 'ktx wiki list',
|
||||
command: 'ktx wiki',
|
||||
description: 'Inspect generated wiki pages',
|
||||
},
|
||||
]);
|
||||
|
|
@ -67,7 +67,7 @@ describe('KTX demo next steps', () => {
|
|||
|
||||
expect(rendered).toContain('Build KTX context next.');
|
||||
expect(rendered).toContain('Run ingest to build database schema context before context-source ingest.');
|
||||
expect(rendered).toContain('ktx ingest --all');
|
||||
expect(rendered).toContain('ktx ingest');
|
||||
expect(rendered).not.toContain('resume');
|
||||
expect(rendered).not.toContain('scan');
|
||||
expect(rendered).toContain('ktx status');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export const KTX_CONTEXT_BUILD_COMMANDS = [
|
||||
{
|
||||
command: 'ktx ingest --all',
|
||||
description: 'Build or refresh agent-ready context from configured connections',
|
||||
command: 'ktx ingest',
|
||||
description: 'Build or refresh agent-ready context from all configured connections',
|
||||
},
|
||||
{
|
||||
command: 'ktx status',
|
||||
|
|
@ -15,11 +15,11 @@ export const KTX_NEXT_STEP_DIRECT_COMMANDS = [
|
|||
description: 'Verify project setup and context readiness',
|
||||
},
|
||||
{
|
||||
command: 'ktx sl list',
|
||||
command: 'ktx sl',
|
||||
description: 'Inspect generated semantic-layer sources',
|
||||
},
|
||||
{
|
||||
command: 'ktx wiki list',
|
||||
command: 'ktx wiki',
|
||||
description: 'Inspect generated wiki pages',
|
||||
},
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -124,12 +124,15 @@ describe('buildPublicIngestPlan', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('rejects bare non-interactive ingest until the interactive confirmation slice exists', () => {
|
||||
const project = projectWithConnections({ warehouse: { driver: 'postgres' } });
|
||||
it('treats a bare invocation (no connection id, no --all) as all configured connections', () => {
|
||||
const project = projectWithConnections({
|
||||
warehouse: { driver: 'postgres' },
|
||||
docs: { driver: 'notion' },
|
||||
});
|
||||
|
||||
expect(() => buildPublicIngestPlan(project, { projectDir: '/tmp/project', all: false })).toThrow(
|
||||
'Context build requires a connection id or all targets',
|
||||
);
|
||||
const plan = buildPublicIngestPlan(project, { projectDir: '/tmp/project', all: false });
|
||||
|
||||
expect(plan.targets.map((target) => target.connectionId).sort()).toEqual(['docs', 'warehouse']);
|
||||
});
|
||||
|
||||
it('resolves database depth from flags, stored context, and defaults', () => {
|
||||
|
|
|
|||
|
|
@ -469,14 +469,11 @@ export function buildPublicIngestPlan(
|
|||
scanMode?: Extract<KtxScanArgs, { command: 'run' }>['mode'];
|
||||
},
|
||||
): KtxPublicIngestPlan {
|
||||
if (!args.all && !args.targetConnectionId) {
|
||||
throw new Error('Context build requires a connection id or all targets');
|
||||
}
|
||||
|
||||
const allConnections = args.all || !args.targetConnectionId;
|
||||
const entries = Object.entries(project.config.connections).sort(([a], [b]) => a.localeCompare(b));
|
||||
const selected = args.all ? entries : entries.filter(([connectionId]) => connectionId === args.targetConnectionId);
|
||||
const selected = allConnections ? entries : entries.filter(([connectionId]) => connectionId === args.targetConnectionId);
|
||||
|
||||
if (!args.all && selected.length === 0) {
|
||||
if (!allConnections && selected.length === 0) {
|
||||
throw new Error(`Connection "${args.targetConnectionId}" is not configured in ktx.yaml`);
|
||||
}
|
||||
if (selected.length === 0) {
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ describe('setup agents', () => {
|
|||
expect(skill).toContain(`--project-dir ${tempDir}`);
|
||||
expect(skill).toContain('must not print secrets');
|
||||
expect(skill).toContain('status --json');
|
||||
expect(skill).toContain('sl list --json');
|
||||
expect(skill).toContain('sl --json');
|
||||
expect(skill).toContain('sl query');
|
||||
expect(skill).toContain('--format json');
|
||||
expect(skill).not.toContain('sl query --json');
|
||||
|
|
|
|||
|
|
@ -569,8 +569,8 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun
|
|||
'Available commands:',
|
||||
'',
|
||||
`- \`${ktxCommandLine(input.launcher, ['status', ...jsonProjectDirArgs])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['sl', 'list', ...jsonProjectDirArgs])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['sl', 'search', '<text>', ...jsonProjectDirArgs, '--connection-id', '<id>'])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['sl', ...jsonProjectDirArgs])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['sl', '<text>', ...jsonProjectDirArgs, '--connection-id', '<id>'])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, [
|
||||
'sl',
|
||||
'query',
|
||||
|
|
@ -585,7 +585,7 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun
|
|||
'--max-rows',
|
||||
'100',
|
||||
])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['wiki', 'search', '<query>', ...jsonProjectDirArgs, '--limit', '10'])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['wiki', '<query>', ...jsonProjectDirArgs, '--limit', '10'])}\``,
|
||||
'',
|
||||
'Use semantic-layer queries before direct database access. Do not print secrets or credential references.',
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: Ktx
|
|||
await printSlSources({
|
||||
rows: sources,
|
||||
emptyMessage: `No semantic-layer sources matched "${args.query}" in ${project.projectDir}`,
|
||||
emptyHint: 'Run `ktx sl list` to inspect available sources.',
|
||||
emptyHint: 'Run `ktx sl` to inspect available sources.',
|
||||
command: 'sl search',
|
||||
output: args.output,
|
||||
json: args.json,
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
'fake',
|
||||
]);
|
||||
expect(run).toMatchObject({ code: 1, stdout: '' });
|
||||
expect(run.stderr).toContain("unknown option '--connection-id'");
|
||||
expect(run.stderr).toContain("unknown option '--adapter'");
|
||||
});
|
||||
|
||||
it('rejects the removed agent command through the built binary', async () => {
|
||||
|
|
@ -285,7 +285,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
|
||||
expect(add.code).toBe(1);
|
||||
expect(add.stdout).toBe('');
|
||||
expect(add.stderr).toContain("unknown command 'add'");
|
||||
expect(add.stderr).toMatch(/unknown (command|option)|too many arguments/);
|
||||
|
||||
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(yaml).not.toContain('driver: notion');
|
||||
|
|
|
|||
|
|
@ -165,10 +165,10 @@ describe('standalone example docs', () => {
|
|||
|
||||
for (const command of [
|
||||
'ktx status --json',
|
||||
'ktx sl list --json',
|
||||
'ktx sl search "revenue" --json',
|
||||
'ktx sl --json',
|
||||
'ktx sl "revenue" --json',
|
||||
'ktx sl query',
|
||||
'ktx wiki search "revenue recognition" --json',
|
||||
'ktx wiki "revenue recognition" --json',
|
||||
]) {
|
||||
assert.match(servingAgents, new RegExp(command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
}
|
||||
|
|
@ -252,7 +252,7 @@ describe('standalone example docs', () => {
|
|||
const localWarehouseReadme = await readText('examples/local-warehouse/README.md');
|
||||
|
||||
assert.match(ingestReference, /ktx ingest <connectionId>/);
|
||||
assert.match(ingestReference, /ktx ingest --all --deep/);
|
||||
assert.match(ingestReference, /Build every configured connection/);
|
||||
assert.match(ingestReference, /--query-history-window-days <days>/);
|
||||
assert.match(buildingContext, /ktx ingest <connectionId>/);
|
||||
assert.match(buildingContext, /ktx ingest --all/);
|
||||
|
|
|
|||
|
|
@ -688,7 +688,6 @@ try {
|
|||
'exec',
|
||||
'ktx',
|
||||
'wiki',
|
||||
'search',
|
||||
'revenue',
|
||||
'--json',
|
||||
'--limit',
|
||||
|
|
@ -731,7 +730,6 @@ try {
|
|||
'exec',
|
||||
'ktx',
|
||||
'sl',
|
||||
'search',
|
||||
'orders',
|
||||
'--json',
|
||||
'--connection-id',
|
||||
|
|
|
|||
|
|
@ -475,9 +475,9 @@ describe('verification snippets', () => {
|
|||
assert.doesNotMatch(source, /startSemanticDaemon/);
|
||||
assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'setup'/);
|
||||
assert.match(source, /wiki', 'global', 'revenue\.md'/);
|
||||
assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'wiki',\s*'search'/);
|
||||
assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'wiki',\s*'revenue'/);
|
||||
assert.match(source, /semantic-layer', 'warehouse', 'orders\.yaml'/);
|
||||
assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'sl',\s*'search',\s*'orders'/);
|
||||
assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'sl',\s*'orders'/);
|
||||
assert.match(source, /orders\.order_count/);
|
||||
assert.match(source, /node:sqlite/);
|
||||
assert.match(source, /driver: sqlite/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue