From 73803115015c888080e99cd3fe6a50520c6d308e Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 20 May 2026 01:37:44 +0200 Subject: [PATCH] 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. --- README.md | 20 +-- .../docs/cli-reference/ktx-connection.mdx | 30 ++-- .../content/docs/cli-reference/ktx-ingest.mdx | 70 ++++----- .../content/docs/cli-reference/ktx-sl.mdx | 51 ++++--- .../content/docs/cli-reference/ktx-wiki.mdx | 46 +++--- docs-site/content/docs/cli-reference/ktx.mdx | 6 +- .../content/docs/guides/building-context.mdx | 20 +-- .../content/docs/guides/serving-agents.mdx | 18 +-- .../content/docs/guides/writing-context.mdx | 12 +- .../docs/integrations/agent-clients.mdx | 12 +- .../cli/src/commands/connection-commands.ts | 21 +-- .../cli/src/commands/connection-selection.ts | 18 +++ packages/cli/src/commands/ingest-commands.ts | 75 +++++----- .../cli/src/commands/knowledge-commands.ts | 70 ++++----- packages/cli/src/commands/mcp-commands.ts | 29 ++-- packages/cli/src/commands/sl-commands.ts | 88 ++++++------ packages/cli/src/doctor.test.ts | 8 +- packages/cli/src/example-smoke.test.ts | 6 +- packages/cli/src/index.test.ts | 136 ++++++++++++------ packages/cli/src/io/print-list.test.ts | 8 +- packages/cli/src/knowledge.ts | 2 +- packages/cli/src/memory-flow-tui.test.tsx | 4 +- packages/cli/src/next-steps.test.ts | 10 +- packages/cli/src/next-steps.ts | 8 +- packages/cli/src/public-ingest.test.ts | 13 +- packages/cli/src/public-ingest.ts | 9 +- packages/cli/src/setup-agents.test.ts | 2 +- packages/cli/src/setup-agents.ts | 6 +- packages/cli/src/sl.ts | 2 +- packages/cli/src/standalone-smoke.test.ts | 4 +- scripts/examples-docs.test.mjs | 8 +- scripts/package-artifacts.mjs | 2 - scripts/package-artifacts.test.mjs | 4 +- 33 files changed, 438 insertions(+), 380 deletions(-) create mode 100644 packages/cli/src/commands/connection-selection.ts diff --git a/README.md b/README.md index 5726a53f..ec6aecb2 100644 --- a/README.md +++ b/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 ` | Test one connection | +| `ktx ingest` | Build context for every configured connection | | `ktx ingest ` | Build context for one connection | -| `ktx ingest --all` | Build context for every configured connection | -| `ktx ingest text --connection-id ` | 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 ` | Capture a text file into memory | +| `ktx sl` | List semantic-layer sources | +| `ktx sl "revenue"` | Search semantic-layer sources | | `ktx sl validate --connection-id ` | Validate a semantic source | | `ktx sl query --measure --format sql` | Compile semantic-layer SQL | | `ktx sql --connection "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 ``` diff --git a/docs-site/content/docs/cli-reference/ktx-connection.mdx b/docs-site/content/docs/cli-reference/ktx-connection.mdx index 2d61451f..5c4c5324 100644 --- a/docs-site/content/docs/cli-reference/ktx-connection.mdx +++ b/docs-site/content/docs/cli-reference/ktx-connection.mdx @@ -10,15 +10,21 @@ systems. Use `ktx setup` to add, remove, or reconfigure them. ## Command signature ```bash -ktx connection [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 [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 diff --git a/docs-site/content/docs/cli-reference/ktx-ingest.mdx b/docs-site/content/docs/cli-reference/ktx-ingest.mdx index 49485d10..971a9d18 100644 --- a/docs-site/content/docs/cli-reference/ktx-ingest.mdx +++ b/docs-site/content/docs/cli-reference/ktx-ingest.mdx @@ -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 ` ingests one configured connection. +- `ktx ingest --text "..."` (or `--file `) 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 ` | BigQuery/Snowflake query-history lookback window for this run | Stored connection default | +| `--text ` | Capture inline text into KTX memory; repeatable | `[]` | +| `--file ` | Capture a text file into KTX memory; use `-` for stdin; repeatable | `[]` | +| `--connection-id ` | KTX connection id to tag captured text/file notes | - | +| `--user-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 ` | Text content to ingest; repeat for a batch | `[]` | -| `--connection-id ` | Optional KTX connection id for semantic-layer capture | - | -| `--user-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 ` 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 ` 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 | diff --git a/docs-site/content/docs/cli-reference/ktx-sl.mdx b/docs-site/content/docs/cli-reference/ktx-sl.mdx index f395a170..19c90632 100644 --- a/docs-site/content/docs/cli-reference/ktx-sl.mdx +++ b/docs-site/content/docs/cli-reference/ktx-sl.mdx @@ -10,34 +10,33 @@ the vocabulary agents use to generate correct SQL. ## Command signature ```bash -ktx sl [options] +ktx sl [options] [query...] # list (bare) or search (with query) +ktx sl validate [options] +ktx sl query [options] ``` +- Bare `ktx sl` lists semantic-layer sources. +- `ktx sl ` 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 ` | Search semantic-layer sources | +| (none, no query) | List semantic-layer sources | +| (none, with query) | Search semantic-layer sources | | `validate ` | 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 ` | Filter by KTX connection id | - | -| `--output ` | 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 ` | Filter by KTX connection id | - | -| `--limit ` | Maximum search results | - | +| `--limit ` | Maximum search results (search mode only) | - | | `--output ` | 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 [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 ` (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 `, 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` | diff --git a/docs-site/content/docs/cli-reference/ktx-wiki.mdx b/docs-site/content/docs/cli-reference/ktx-wiki.mdx index e1caabb5..aac60a07 100644 --- a/docs-site/content/docs/cli-reference/ktx-wiki.mdx +++ b/docs-site/content/docs/cli-reference/ktx-wiki.mdx @@ -10,42 +10,28 @@ them for context when answering questions about your data. ## Command signature ```bash -ktx wiki [options] +ktx wiki [options] [query...] ``` -## Subcommands +- Bare `ktx wiki` lists local wiki pages. +- `ktx wiki ` searches local wiki pages (multi-word queries are + joined with a space). -| Subcommand | Description | -|-----------|-------------| -| `list` | List local wiki pages | -| `search ` | 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 ` | Local user id | `local` | +| `--limit ` | Maximum search results (search mode only) | - | | `--output ` | 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 ` | Local user id | `local` | -| `--limit ` | Maximum search results | - | -| `--output ` | 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 ` 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 diff --git a/docs-site/content/docs/cli-reference/ktx.mdx b/docs-site/content/docs/cli-reference/ktx.mdx index 937d2529..42679ec9 100644 --- a/docs-site/content/docs/cli-reference/ktx.mdx +++ b/docs-site/content/docs/cli-reference/ktx.mdx @@ -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" diff --git a/docs-site/content/docs/guides/building-context.mdx b/docs-site/content/docs/guides/building-context.mdx index 39edaa85..b1d5ed36 100644 --- a/docs-site/content/docs/guides/building-context.mdx +++ b/docs-site/content/docs/guides/building-context.mdx @@ -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 ` | Capture inline text into memory; repeatable | +| `--file ` | Capture a text file (or `-` for stdin) into memory; repeatable | | `--connection-id ` | Attach the captured memory to a KTX connection | | `--user-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 ` 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` | diff --git a/docs-site/content/docs/guides/serving-agents.mdx b/docs-site/content/docs/guides/serving-agents.mdx index 8710d3ba..df6eba2a 100644 --- a/docs-site/content/docs/guides/serving-agents.mdx +++ b/docs-site/content/docs/guides/serving-agents.mdx @@ -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 ` and `ktx wiki ` 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 ` and restart the agent session | | Agent command cannot find the project | Agent is running outside the KTX directory | Add `--project-dir ` 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 `, 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 | diff --git a/docs-site/content/docs/guides/writing-context.mdx b/docs-site/content/docs/guides/writing-context.mdx index 2b9824c8..066c4dea 100644 --- a/docs-site/content/docs/guides/writing-context.mdx +++ b/docs-site/content/docs/guides/writing-context.mdx @@ -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//` 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. diff --git a/docs-site/content/docs/integrations/agent-clients.mdx b/docs-site/content/docs/integrations/agent-clients.mdx index 4a670315..9f1ad659 100644 --- a/docs-site/content/docs/integrations/agent-clients.mdx +++ b/docs-site/content/docs/integrations/agent-clients.mdx @@ -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 '' --json --project-dir /path/to/project --connection-id ''` +- `ktx sl --json --project-dir /path/to/project` +- `ktx sl '' --json --project-dir /path/to/project --connection-id ''` - `ktx sl query --project-dir /path/to/project --connection-id '' --query-file '' --format json --execute --max-rows 100` -- `ktx wiki search '' --json --project-dir /path/to/project --limit 10` +- `ktx wiki '' --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 --json` | Search wiki pages | -| `ktx sl list --json` | List semantic-layer sources | -| `ktx sl search --json` | Search semantic-layer sources | +| `ktx wiki --json` | Search wiki pages | +| `ktx sl --json` | List semantic-layer sources | +| `ktx sl --json` | Search semantic-layer sources | | `ktx sl validate --connection-id ` | Validate semantic source definitions | | `ktx sl query --format json` | Execute a Semantic Query when semantic compute is configured | diff --git a/packages/cli/src/commands/connection-commands.ts b/packages/cli/src/commands/connection-commands.ts index 181e8905..213bf608 100644 --- a/packages/cli/src/commands/connection-commands.ts +++ b/packages/cli/src/commands/connection-commands.ts @@ -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 (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, }); }); } diff --git a/packages/cli/src/commands/connection-selection.ts b/packages/cli/src/commands/connection-selection.ts new file mode 100644 index 00000000..5ea0ca46 --- /dev/null +++ b/packages/cli/src/commands/connection-selection.ts @@ -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' }; +} diff --git a/packages/cli/src/commands/ingest-commands.ts b/packages/cli/src/commands/ingest-commands.ts index 8e3bd7f2..9ffd2562 100644 --- a/packages/cli/src/commands/ingest-commands.ts +++ b/packages/cli/src/commands/ingest-commands.ts @@ -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 ', 'Query-history lookback window for this run', parsePositiveIntegerOption) + .option('--text ', 'Capture inline text into KTX memory; repeatable', collectOption, []) + .option('--file ', 'Capture a text file into KTX memory; use - for stdin; repeatable', collectOption, []) + .option('--connection-id ', 'KTX connection id to tag captured text/file notes') + .option('--user-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 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 ', 'Text content to ingest; repeat for a batch', collectOption, []) - .option('--connection-id ', 'Optional KTX connection id for semantic-layer capture') - .option('--user-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, - ), - ); - }); } diff --git a/packages/cli/src/commands/knowledge-commands.ts b/packages/cli/src/commands/knowledge-commands.ts index 1c61a836..c0fe4f06 100644 --- a/packages/cli/src/commands/knowledge-commands.ts +++ b/packages/cli/src/commands/knowledge-commands.ts @@ -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 ', 'Local user id', 'local') + .option('--limit ', 'Maximum search results (search mode only)', parsePositiveIntegerOption) + .addOption( + new Option('--output ', '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 ', 'Local user id', 'local') - .addOption( - new Option('--output ', '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('', 'Search query') - .option('--user-id ', 'Local user id', 'local') - .option('--limit ', 'Maximum search results', parsePositiveIntegerOption) - .addOption( - new Option('--output ', '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, diff --git a/packages/cli/src/commands/mcp-commands.ts b/packages/cli/src/commands/mcp-commands.ts index be7044a7..94b17498 100644 --- a/packages/cli/src/commands/mcp-commands.ts +++ b/packages/cli/src/commands/mcp-commands.ts @@ -36,8 +36,24 @@ function formatMcpStartResultMessage(input: { status: 'started' | 'already-runni ].join('\n'); } +async function printMcpStatus(context: KtxCliCommandContext, projectDir: string): Promise { + 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 diff --git a/packages/cli/src/commands/sl-commands.ts b/packages/cli/src/commands/sl-commands.ts index d23674cd..6a03eb67 100644 --- a/packages/cli/src/commands/sl-commands.ts +++ b/packages/cli/src/commands/sl-commands.ts @@ -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 ', 'KTX connection id') + .option('--limit ', 'Maximum search results (search mode only)', parsePositiveIntegerOption) + .addOption( + new Option('--output ', '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 ', 'KTX connection id') - .addOption( - new Option('--output ', '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('', 'Search query') - .option('--connection-id ', 'KTX connection id') - .option('--limit ', 'Maximum search results', parsePositiveIntegerOption) - .addOption( - new Option('--output ', '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('', 'Semantic-layer source name') - .requiredOption('--connection-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 ' 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 ', 'KTX connection id') + .description('Compile or execute a semantic-layer query (set --connection-id on `ktx sl`)') .option('--query-file ', 'JSON semantic-layer query file') .option('--measure ', 'Measure to query; repeatable', collectOption, []) .option('--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 } : { diff --git a/packages/cli/src/doctor.test.ts b/packages/cli/src/doctor.test.ts index 2a397653..a6bfbff6 100644 --- a/packages/cli/src/doctor.test.ts +++ b/packages/cli/src/doctor.test.ts @@ -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; diff --git a/packages/cli/src/example-smoke.test.ts b/packages/cli/src/example-smoke.test.ts index e59d7d7e..22f1eafa 100644 --- a/packages/cli/src/example-smoke.test.ts +++ b/packages/cli/src/example-smoke.test.ts @@ -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); }); diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 3c12e583..c1e055f8 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -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 '); - 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 '); - expect(testIo.stdout()).toContain('--connection-id '); - expect(testIo.stdout()).toContain('--user-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 () => { diff --git a/packages/cli/src/io/print-list.test.ts b/packages/cli/src/io/print-list.test.ts index 0cb5a537..f084e519 100644 --- a/packages/cli/src/io/print-list.test.ts +++ b/packages/cli/src/io/print-list.test.ts @@ -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', () => { diff --git a/packages/cli/src/knowledge.ts b/packages/cli/src/knowledge.ts index 8213d05d..f12d3567 100644 --- a/packages/cli/src/knowledge.ts +++ b/packages/cli/src/knowledge.ts @@ -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) { diff --git a/packages/cli/src/memory-flow-tui.test.tsx b/packages/cli/src/memory-flow-tui.test.tsx index e525a834..405fa18c 100644 --- a/packages/cli/src/memory-flow-tui.test.tsx +++ b/packages/cli/src/memory-flow-tui.test.tsx @@ -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(' ')); diff --git a/packages/cli/src/next-steps.test.ts b/packages/cli/src/next-steps.test.ts index c2b15530..8a3e5e2a 100644 --- a/packages/cli/src/next-steps.test.ts +++ b/packages/cli/src/next-steps.test.ts @@ -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'); diff --git a/packages/cli/src/next-steps.ts b/packages/cli/src/next-steps.ts index 06ef3412..5410eee8 100644 --- a/packages/cli/src/next-steps.ts +++ b/packages/cli/src/next-steps.ts @@ -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; diff --git a/packages/cli/src/public-ingest.test.ts b/packages/cli/src/public-ingest.test.ts index d7f853c8..47b80074 100644 --- a/packages/cli/src/public-ingest.test.ts +++ b/packages/cli/src/public-ingest.test.ts @@ -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', () => { diff --git a/packages/cli/src/public-ingest.ts b/packages/cli/src/public-ingest.ts index 537dcec7..2c0a2856 100644 --- a/packages/cli/src/public-ingest.ts +++ b/packages/cli/src/public-ingest.ts @@ -469,14 +469,11 @@ export function buildPublicIngestPlan( scanMode?: Extract['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) { diff --git a/packages/cli/src/setup-agents.test.ts b/packages/cli/src/setup-agents.test.ts index bd9c9458..e6ca39ed 100644 --- a/packages/cli/src/setup-agents.test.ts +++ b/packages/cli/src/setup-agents.test.ts @@ -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'); diff --git a/packages/cli/src/setup-agents.ts b/packages/cli/src/setup-agents.ts index 6fcd06c1..ecac2917 100644 --- a/packages/cli/src/setup-agents.ts +++ b/packages/cli/src/setup-agents.ts @@ -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', '', ...jsonProjectDirArgs, '--connection-id', ''])}\``, + `- \`${ktxCommandLine(input.launcher, ['sl', ...jsonProjectDirArgs])}\``, + `- \`${ktxCommandLine(input.launcher, ['sl', '', ...jsonProjectDirArgs, '--connection-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', '', ...jsonProjectDirArgs, '--limit', '10'])}\``, + `- \`${ktxCommandLine(input.launcher, ['wiki', '', ...jsonProjectDirArgs, '--limit', '10'])}\``, '', 'Use semantic-layer queries before direct database access. Do not print secrets or credential references.', '', diff --git a/packages/cli/src/sl.ts b/packages/cli/src/sl.ts index d77fa4f9..4049936a 100644 --- a/packages/cli/src/sl.ts +++ b/packages/cli/src/sl.ts @@ -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, diff --git a/packages/cli/src/standalone-smoke.test.ts b/packages/cli/src/standalone-smoke.test.ts index 688e69f2..effb078a 100644 --- a/packages/cli/src/standalone-smoke.test.ts +++ b/packages/cli/src/standalone-smoke.test.ts @@ -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'); diff --git a/scripts/examples-docs.test.mjs b/scripts/examples-docs.test.mjs index bebe4965..a3f326c7 100644 --- a/scripts/examples-docs.test.mjs +++ b/scripts/examples-docs.test.mjs @@ -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 /); - assert.match(ingestReference, /ktx ingest --all --deep/); + assert.match(ingestReference, /Build every configured connection/); assert.match(ingestReference, /--query-history-window-days /); assert.match(buildingContext, /ktx ingest /); assert.match(buildingContext, /ktx ingest --all/); diff --git a/scripts/package-artifacts.mjs b/scripts/package-artifacts.mjs index 3c885ac9..38f92dd9 100644 --- a/scripts/package-artifacts.mjs +++ b/scripts/package-artifacts.mjs @@ -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', diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index 89d1a760..47254e0b 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -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/);