feat(cli): smart defaults and flatter command surface for ktx

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:
Andrey Avtomonov 2026-05-20 01:37:44 +02:00
parent 6738defb81
commit 7380311501
33 changed files with 438 additions and 380 deletions

View file

@ -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
```

View file

@ -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

View file

@ -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 dev 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 |

View file

@ -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 dev runtime install --feature core --yes`, or rerun `ktx sl query --yes` |

View file

@ -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

View file

@ -90,11 +90,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"

View file

@ -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` |

View file

@ -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 |

View file

@ -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.

View file

@ -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 |

View file

@ -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,
});
});
}

View 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' };
}

View file

@ -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,
),
);
});
}

View file

@ -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,

View file

@ -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

View file

@ -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 }
: {

View file

@ -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;

View file

@ -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);
});

View file

@ -148,7 +148,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(
{
@ -162,7 +162,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(
{
@ -178,7 +178,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(
{
@ -191,47 +191,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 },
),
@ -249,11 +259,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 () => {
@ -523,7 +548,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) {
@ -871,7 +896,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);
@ -891,7 +917,6 @@ describe('runKtxCli', () => {
'--project-dir',
tempDir,
'ingest',
'text',
'--text',
'Revenue means gross receipts.',
'--text',
@ -923,19 +948,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 dev', async () => {

View file

@ -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', () => {

View file

@ -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) {

View file

@ -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(' '));

View file

@ -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');

View file

@ -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;

View file

@ -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', () => {

View file

@ -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) {

View file

@ -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');

View file

@ -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.',
'',

View file

@ -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,

View file

@ -153,7 +153,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 () => {
@ -280,7 +280,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');

View file

@ -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/);

View file

@ -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',

View file

@ -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/);