diff --git a/README.md b/README.md
index cfabfbcc..b52a31f6 100644
--- a/README.md
+++ b/README.md
@@ -130,9 +130,7 @@ Scan artifacts are written under
```bash
SCAN_OUTPUT="$(ktx scan warehouse --project-dir "$PROJECT_DIR")"
printf '%s\n' "$SCAN_OUTPUT"
-SCAN_RUN_ID="$(printf '%s\n' "$SCAN_OUTPUT" | awk '/^Run: / { print $2 }')"
-ktx scan status --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
-ktx scan report --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
+ktx status --project-dir "$PROJECT_DIR"
```
For non-SQLite drivers, prefer credential references such as `--url env:NAME`
@@ -147,16 +145,13 @@ version, and is managed by `ktx dev runtime` commands.
KTX requires `uv` on `PATH` to create the managed runtime. Install `uv` with
your system package manager or the official installer before running Python-
backed KTX commands. KTX doesn't download `uv` automatically; run
-`ktx dev runtime doctor` if runtime installation fails:
+`ktx dev runtime status` if runtime installation fails:
```bash
ktx dev runtime install --yes
ktx dev runtime status
-ktx dev runtime doctor
ktx dev runtime start
ktx dev runtime stop
-ktx dev runtime prune --dry-run
-ktx dev runtime prune --yes
```
The release artifact manifest contains the public npm tarball and the bundled `kaelio-ktx`
@@ -223,7 +218,7 @@ KTX provider. Enable it with an environment flag when running an LLM-backed
command:
```bash
-KTX_AI_DEVTOOLS_ENABLED=true ktx dev ingest run \
+KTX_AI_DEVTOOLS_ENABLED=true ktx ingest run \
--connection-id warehouse \
--adapter metabase
```
diff --git a/docs-site/components/terminal-preview.tsx b/docs-site/components/terminal-preview.tsx
index a1f950c8..d430c4ac 100644
--- a/docs-site/components/terminal-preview.tsx
+++ b/docs-site/components/terminal-preview.tsx
@@ -47,7 +47,7 @@ export function TerminalPreview() {
$ {" "}
- ktx agent context --json
+ ktx status --json
diff --git a/docs-site/content/docs/ai-resources/agent-quickstart.mdx b/docs-site/content/docs/ai-resources/agent-quickstart.mdx
index 40983224..6fd6e5ac 100644
--- a/docs-site/content/docs/ai-resources/agent-quickstart.mdx
+++ b/docs-site/content/docs/ai-resources/agent-quickstart.mdx
@@ -22,7 +22,7 @@ Agents should start with the smallest source that answers the task:
| How to check project readiness | [ktx status](/docs/cli-reference/ktx-status) | [Quickstart](/docs/getting-started/quickstart) |
| How context gets built | [Building Context](/docs/guides/building-context) | [ktx ingest](/docs/cli-reference/ktx-ingest) |
| How semantic YAML works | [Writing Context](/docs/guides/writing-context) | [ktx sl](/docs/cli-reference/ktx-sl) |
-| How machine-readable CLI output is shaped | [ktx agent](/docs/cli-reference/ktx-agent) | [Markdown Access](/docs/ai-resources/markdown-access) |
+| How machine-readable CLI output is shaped | [ktx sl](/docs/cli-reference/ktx-sl) | [ktx wiki](/docs/cli-reference/ktx-wiki) |
## Operating workflow
diff --git a/docs-site/content/docs/ai-resources/markdown-access.mdx b/docs-site/content/docs/ai-resources/markdown-access.mdx
index c363a215..12bb7456 100644
--- a/docs-site/content/docs/ai-resources/markdown-access.mdx
+++ b/docs-site/content/docs/ai-resources/markdown-access.mdx
@@ -31,7 +31,8 @@ Every docs page has a Markdown route:
```text
https://docs.kaelio.com/ktx/docs/getting-started/quickstart.md
-https://docs.kaelio.com/ktx/docs/cli-reference/ktx-agent.md
+https://docs.kaelio.com/ktx/docs/cli-reference/ktx-sl.md
+https://docs.kaelio.com/ktx/docs/cli-reference/ktx-wiki.md
https://docs.kaelio.com/ktx/docs/guides/building-context.md
```
diff --git a/docs-site/content/docs/cli-reference/ktx-agent.mdx b/docs-site/content/docs/cli-reference/ktx-agent.mdx
deleted file mode 100644
index cdc4ceac..00000000
--- a/docs-site/content/docs/cli-reference/ktx-agent.mdx
+++ /dev/null
@@ -1,148 +0,0 @@
----
-title: "ktx agent"
-description: "Machine-readable commands for coding agents."
----
-
-Hidden commands that provide machine-readable JSON output for coding agents. These are the commands that agent integrations (Claude Code, Cursor, Codex, OpenCode) call under the hood — you typically won't use them directly.
-
-All `ktx agent` subcommands require `--json` and produce structured JSON output on stdout.
-
-## Command signature
-
-```bash
-ktx agent --json [options]
-```
-
-## Subcommands
-
-| Subcommand | Description |
-|-----------|-------------|
-| `tools` | Print available agent-facing KTX tools |
-| `context` | Print project context for agent planning |
-| `sl list` | List semantic-layer sources |
-| `sl read ` | Read one semantic-layer source |
-| `sl query` | Run a semantic-layer query from a JSON file |
-| `wiki search ` | Search KTX wiki pages |
-| `wiki read ` | Read one KTX wiki page |
-| `sql execute` | Execute read-only SQL with a row limit |
-
-## Options
-
-### `agent tools`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-
-### `agent context`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-
-### `agent sl list`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--connection-id ` | Filter by connection id | — |
-| `--query ` | Search source names and descriptions | — |
-
-### `agent sl read`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--connection-id ` | Connection id containing the source | — |
-
-### `agent sl query`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--connection-id ` | Connection id for execution (required) | — |
-| `--query-file ` | JSON semantic-layer query file (required) | — |
-| `--execute` | Execute the compiled query against the connection | `false` |
-| `--max-rows ` | Maximum rows to return when executing (1-1000) | — |
-
-### `agent wiki search`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--limit ` | Maximum search results | `10` |
-
-### `agent wiki read`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-
-### `agent sql execute`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--connection-id ` | Connection id for execution (required) | — |
-| `--sql-file ` | SQL file to execute (required) | — |
-| `--max-rows ` | Maximum rows to return, 1-1000 (required) | — |
-
-## Examples
-
-```bash
-# List available tools
-ktx agent tools --json
-
-# Get project context for planning
-ktx agent context --json
-
-# List semantic sources
-ktx agent sl list --json
-
-# Search semantic sources by name
-ktx agent sl list --json --query "revenue"
-
-# Read a semantic source
-ktx agent sl read orders --json --connection-id my-warehouse
-
-# Run a semantic-layer query from a file
-ktx agent sl query --json \
- --connection-id my-warehouse \
- --query-file /tmp/query.json \
- --execute \
- --max-rows 100
-
-# Search wiki pages
-ktx agent wiki search "churn definition" --json
-
-# Read a specific wiki page
-ktx agent wiki read page-abc123 --json
-
-# Execute read-only SQL
-ktx agent sql execute --json \
- --connection-id my-warehouse \
- --sql-file /tmp/query.sql \
- --max-rows 500
-```
-
-## Output
-
-Every `ktx agent` command writes JSON to stdout and diagnostic text to stderr. Agents should parse stdout as JSON and treat a non-zero exit code as a failed tool call.
-
-```json
-{
- "ok": true,
- "data": {
- "type": "agent-response"
- }
-}
-```
-
-## Common errors
-
-| Error | Cause | Recovery |
-|-------|-------|----------|
-| Missing JSON output | `--json` was omitted | Re-run the same subcommand with `--json` |
-| Unknown connection id | The requested connection is not configured in `ktx.yaml` | Call `ktx agent context --json` or `ktx connection list` to discover valid ids |
-| Query file cannot be read | `--query-file` points to a missing or invalid JSON file | Write the query payload to a real file and pass its absolute path |
-| SQL execution rejected | SQL is not read-only or `--max-rows` is missing | Use semantic-layer queries first; for direct SQL, pass read-only SQL and an explicit row limit |
diff --git a/docs-site/content/docs/cli-reference/ktx-dev.mdx b/docs-site/content/docs/cli-reference/ktx-dev.mdx
index 82ba9acb..e00a4585 100644
--- a/docs-site/content/docs/cli-reference/ktx-dev.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-dev.mdx
@@ -1,9 +1,9 @@
---
title: "ktx dev"
-description: "Low-level diagnostics, scans, adapter commands, and mapping tools."
+description: "Low-level project initialization and runtime management."
---
-Hidden commands for low-level project management, diagnostics, direct adapter control, and shell completion. Most users interact with these through higher-level commands like [`ktx ingest`](/docs/cli-reference/ktx-ingest) and [`ktx setup`](/docs/cli-reference/ktx-setup), but `ktx dev` provides direct access when you need fine-grained control.
+`ktx dev` contains development-only project initialization and managed runtime commands. Scan and ingest commands live at the root as [`ktx scan`](/docs/cli-reference/ktx-scan) and [`ktx ingest`](/docs/cli-reference/ktx-ingest).
## Command signature
@@ -16,145 +16,42 @@ ktx dev [options]
| Subcommand | Description |
|-----------|-------------|
| `init [directory]` | Initialize a Git-backed KTX project directory |
-| `runtime` | Install, inspect, and prune the KTX-managed Python runtime |
-| `scan` | Run or inspect standalone connection scans |
-| `ingest run` | Run local ingest for one configured connection and source adapter |
-| `ingest status [runId]` | Print status for a stored local ingest run |
-| `ingest watch [runId]` | Open a stored ingest visual report |
-| `ingest replay ` | Replay a stored ingest run through memory-flow output |
-| `mapping` | Manage Metabase warehouse mappings (same as `ktx connection mapping`) |
-| `completion zsh` | Generate zsh completion script |
+| `runtime` | Install, start, stop, and inspect the KTX-managed Python runtime |
-## Options
-
-### `dev init`
+## `dev init`
| Flag | Description | Default |
|------|-------------|---------|
| `--name ` | Project name written to `ktx.yaml` | — |
| `--force` | Rewrite `ktx.yaml` and scaffold files in an existing project | `false` |
-### `dev runtime`
+## `dev runtime`
+
+`ktx dev runtime` supports `install`, `start`, `stop`, and `status`.
| Flag | Description | Default |
|------|-------------|---------|
| `--feature ` | Runtime feature level for `install` and `start` (`core` or `local-embeddings`) | `core` |
-| `--json` | Print JSON output | `false` |
-| `--yes` | Confirm runtime install or prune actions where supported | `false` |
+| `--json` | Print JSON output for `status` | `false` |
+| `--yes` | Confirm runtime install actions where supported | `false` |
| `--force` | Reinstall or restart where supported | `false` |
-### `dev scan`
-
-See [`ktx scan`](/docs/cli-reference/ktx-scan) for the full scan command reference.
-
-### `dev ingest run`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--connection-id ` | KTX connection id (required) | — |
-| `--adapter ` | Ingest source adapter name (required) | — |
-| `--source-dir ` | Directory containing source files | — |
-| `--database-introspection-url ` | Daemon URL for live-database introspection | — |
-| `--debug-llm-request-file ` | Write sanitized LLM request structure to a JSONL file | — |
-| `--plain` | Print plain text output | `false` |
-| `--json` | Print JSON output | `false` |
-| `--viz` | Render memory-flow TUI output | `false` |
-| `--no-input` | Disable interactive terminal input for visualization | — |
-
-### `dev ingest status`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--report-file ` | Bundle ingest report JSON file to render | — |
-| `--plain` | Print plain text output | `false` |
-| `--json` | Print JSON output | `false` |
-| `--viz` | Render memory-flow TUI output | `false` |
-| `--no-input` | Disable interactive terminal input for visualization | — |
-
-### `dev ingest watch`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--report-file ` | Bundle ingest report JSON file to render | — |
-| `--plain` | Print plain text output | `false` |
-| `--json` | Print JSON output | `false` |
-| `--viz` | Render memory-flow TUI output (the default unless `--plain` or `--json` is set) | `true` |
-| `--no-input` | Disable interactive terminal input for visualization | — |
-
-### `dev ingest replay`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--report-file ` | Bundle ingest report JSON file to render | — |
-| `--plain` | Print plain text output | `false` |
-| `--json` | Print JSON output | `false` |
-| `--viz` | Render memory-flow TUI output | `false` |
-| `--no-input` | Disable interactive terminal input for visualization | — |
-
-### `dev completion zsh`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--install` | Install zsh completion into `~/.zfunc` and update `~/.zshrc` | `false` |
-
## Examples
```bash
-# Initialize a new KTX project
ktx dev init
-
-# Initialize in a specific directory with a project name
ktx dev init ./my-project --name "Analytics Context"
-
-# Re-initialize an existing project
ktx dev init --force
-# Check managed Python runtime readiness
-ktx dev runtime doctor
-
-# Start the managed Python daemon
+ktx dev runtime install --yes
+ktx dev runtime status
ktx dev runtime start
-
-# Run a low-level ingest with a specific adapter
-ktx dev ingest run --connection-id my-dbt --adapter dbt
-
-# Run ingest from a specific source directory
-ktx dev ingest run \
- --connection-id my-dbt \
- --adapter dbt \
- --source-dir ./dbt-project
-
-# View ingest status with the visual TUI
-ktx dev ingest watch run-abc123
-
-# Replay a stored ingest session
-ktx dev ingest replay run-abc123
-
-# View ingest status from a report file
-ktx dev ingest status --report-file /tmp/ingest-report.json
-
-# Generate zsh completions
-ktx dev completion zsh
-
-# Install zsh completions
-ktx dev completion zsh --install
+ktx dev runtime stop
```
-## Output
-
-`ktx dev` commands are diagnostic and may print plain text, JSON, or visual reports depending on the selected flags.
-
-| Mode | How to request it | Use case |
-|------|-------------------|----------|
-| Plain text | `--plain` or default diagnostic output | Human-readable terminal inspection |
-| JSON | `--json` | Agent parsing and automation |
-| Visual report | `--viz` | Interactive memory-flow and ingest debugging |
-
## Common errors
| Error | Cause | Recovery |
|-------|-------|----------|
-| Doctor reports missing runtime pieces | Packages, Python environment, or linked CLI are not ready | Run `pnpm install`, `pnpm run setup:dev`, and `uv sync --all-groups` |
-| Ingest run cannot find adapter | `--adapter` does not match a supported source adapter | Use configured source names from `ktx.yaml` or run higher-level `ktx ingest` |
-| Replay/report file cannot be read | The report path is wrong or the run id is not stored locally | Run `ktx dev ingest status --json` to discover stored run ids and report files |
-| Visual output fails in CI | TUI rendering requires an interactive terminal | Use `--plain --no-input` or `--json --no-input` |
+| Runtime status reports missing pieces | Packages, Python environment, or linked CLI are not ready | Run `pnpm install`, `pnpm run setup:dev`, `uv sync --all-groups`, then `ktx dev runtime status` |
+| Runtime daemon does not start | The managed Python runtime is missing or stale | Run `ktx dev runtime install --yes`, then `ktx dev runtime start` |
diff --git a/docs-site/content/docs/cli-reference/ktx-ingest.mdx b/docs-site/content/docs/cli-reference/ktx-ingest.mdx
index 8ce9d9a5..e1c0e339 100644
--- a/docs-site/content/docs/cli-reference/ktx-ingest.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-ingest.mdx
@@ -1,14 +1,13 @@
---
title: "ktx ingest"
-description: "Build and refresh context from configured sources."
+description: "Run and inspect local ingest memory-flow output."
---
-Ingest context from your configured sources — dbt, Looker, Metabase, MetricFlow, LookML, or Notion. The ingest process extracts metadata from your tools, then uses an LLM agent to reconcile it with existing context, writing semantic sources and knowledge pages to your project.
+`ktx ingest` runs adapter-level local ingest and renders stored ingest reports.
## Command signature
```bash
-ktx ingest [connectionId] [options]
ktx ingest [options]
```
@@ -16,80 +15,59 @@ ktx ingest [options]
| Subcommand | Description |
|-----------|-------------|
-| `status [runId]` | Print status for the latest or selected public ingest run |
-| `watch [runId]` | Open the latest or selected public ingest visual report |
+| `run` | Run local ingest for one configured connection and source adapter |
+| `status [runId]` | Print status for the latest or selected stored local ingest run or report file |
+| `watch [runId]` | Open the latest or selected stored ingest visual report |
+| `replay ` | Replay a stored ingest run or bundle report through memory-flow output |
-## Options
-
-### `ingest` (run)
+## `ingest run`
| Flag | Description | Default |
|------|-------------|---------|
-| `--all` | Ingest every eligible configured source | `false` |
+| `--connection-id ` | KTX connection id | Required |
+| `--adapter ` | Ingest source adapter name | Required |
+| `--source-dir ` | Directory containing source files | — |
+| `--database-introspection-url ` | Daemon URL for live-database introspection | — |
+| `--debug-llm-request-file ` | Write sanitized LLM request structure to a JSONL file | — |
+| `--plain` | Print plain text output | `true` |
| `--json` | Print JSON output | `false` |
-| `--no-input` | Disable interactive terminal input | — |
+| `--viz` | Render memory-flow TUI output | `false` |
+| `--yes` | Install the managed Python runtime without prompting when required | `false` |
+| `--no-input` | Disable interactive terminal input for visualization and runtime installation | — |
-### `ingest status`
+## `ingest status`, `watch`, and `replay`
| Flag | Description | Default |
|------|-------------|---------|
+| `--report-file ` | Bundle ingest report JSON file to render | — |
+| `--plain` | Print plain text output | `true` for `status` and `replay` |
| `--json` | Print JSON output | `false` |
-| `--no-input` | Disable interactive terminal input | — |
-
-### `ingest watch`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output instead of the visual report | `false` |
-| `--no-input` | Disable interactive terminal input | — |
+| `--viz` | Render memory-flow TUI output | `true` for `watch` |
+| `--no-input` | Disable interactive terminal input for visualization | — |
## Examples
```bash
-# Ingest from a specific connection
-ktx ingest my-dbt-source
+ktx ingest run --connection-id my-dbt-source --adapter dbt
+ktx ingest run --connection-id prod-metabase --adapter metabase --yes
-# Ingest from all eligible sources
-ktx ingest --all
-
-# Check the status of the latest ingest
ktx ingest status
-
-# Check the status of a specific ingest run
ktx ingest status run-abc123
-
-# Watch the latest ingest report
-ktx ingest watch
-
-# Get ingest status as JSON
ktx ingest status --json
-```
-## Low-level ingest commands
+ktx ingest watch
+ktx ingest watch run-abc123
-For adapter-level control, use `ktx dev ingest`. See [`ktx dev`](/docs/cli-reference/ktx-dev) for the full low-level ingest surface including `run`, `status`, `watch`, and `replay` with output mode options (`--plain`, `--json`, `--viz`).
-
-## Output
-
-Ingest run commands print progress and create a stored ingest report. `ktx ingest status --json` returns the run state, adapter, connection, and summary information.
-
-```json
-{
- "runId": "ingest-local-abc123",
- "status": "completed",
- "connectionId": "dbt-main",
- "summary": {
- "semanticSourcesChanged": 4,
- "knowledgePagesChanged": 2
- }
-}
+ktx ingest replay run-abc123
+ktx ingest replay run-abc123 --viz
+ktx ingest replay run-abc123 --report-file /tmp/ingest-report.json
```
## Common errors
| Error | Cause | Recovery |
|-------|-------|----------|
-| No eligible sources | `ktx.yaml` has no configured context source for ingest | Add a source with `ktx setup` or `ktx connection add`, then rerun ingest |
| Ingest needs credentials | The source adapter requires API or git access | Configure the referenced environment variable or secret file |
-| Latest run not found | No ingest run has been started in this project | Run `ktx ingest ` or `ktx ingest --all` first |
+| Ingest run cannot find adapter | `--adapter` does not match a supported source adapter | Use a configured adapter such as `dbt`, `metabase`, `looker`, `lookml`, `notion`, or `live-database` |
+| Latest run not found | No ingest run has been started in this project | Run `ktx ingest run --connection-id --adapter ` first |
| Report watch fails in a non-interactive shell | Visual report needs a terminal | Use `ktx ingest status --json` for agent and CI workflows |
diff --git a/docs-site/content/docs/cli-reference/ktx-scan.mdx b/docs-site/content/docs/cli-reference/ktx-scan.mdx
index 0c37eccb..2f73ed99 100644
--- a/docs-site/content/docs/cli-reference/ktx-scan.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-scan.mdx
@@ -1,163 +1,39 @@
---
title: "ktx scan"
-description: "Run or inspect database scans."
+description: "Run standalone database scans."
---
-Discover your database schema — tables, columns, types, constraints, and relationships. Scanning is the first step in building context: KTX needs to understand your warehouse structure before it can build semantic sources.
-
-Scan commands live under `ktx dev scan`. See also the [Building Context](/docs/guides/building-context) guide for a walkthrough.
+Discover a configured database connection's schema, including tables, columns, types, constraints, and optional relationship signals.
## Command signature
```bash
-ktx dev scan [options]
-ktx dev scan [options]
+ktx scan [options]
```
-## Subcommands
-
-| Subcommand | Description |
-|-----------|-------------|
-| `status ` | Print status for a local scan run |
-| `report ` | Print a local scan report |
-| `relationships ` | Print relationship artifacts for a local scan run |
-| `relationship-apply ` | Apply accepted relationship review decisions as manual manifest joins |
-| `relationship-feedback` | Export persisted relationship review decisions as calibration labels |
-| `relationship-calibration` | Summarize relationship feedback labels against current score thresholds |
-| `relationship-thresholds` | Evaluate relationship feedback labels for offline threshold advice |
-
## Options
-### `scan` (run)
-
| Flag | Description | Default |
|------|-------------|---------|
| `--mode ` | Scan mode: `structural`, `enriched`, or `relationships` | `structural` |
| `--dry-run` | Run without writing scan results | `false` |
| `--database-introspection-url ` | Daemon URL for live-database introspection | — |
-
-### `scan report`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print the raw scan report JSON | `false` |
-
-### `scan relationships`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--status ` | Filter by status: `accepted`, `review`, `rejected`, `skipped`, or `all` | `review` |
-| `--limit ` | Maximum relationships to print per status | `25` |
-| `--accept ` | Record an accepted decision for a relationship candidate | — |
-| `--reject ` | Record a rejected decision for a relationship candidate | — |
-| `--note ` | Attach a note when recording a relationship review decision | — |
-| `--reviewer ` | Reviewer name for a relationship review decision | — |
-| `--json` | Print relationship artifacts as JSON | `false` |
-
-### `scan relationship-apply`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--all-accepted` | Apply all accepted relationship review decisions for the scan run | `false` |
-| `--candidate ` | Apply one accepted relationship review decision; repeatable | — |
-| `--dry-run` | Preview relationships that would be written without rewriting manifest shards | `false` |
-| `--json` | Print the apply result as JSON | `false` |
-
-### `scan relationship-feedback`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--connection ` | Only export labels for one KTX connection | — |
-| `--decision ` | Filter: `accepted`, `rejected`, or `all` | `all` |
-| `--json` | Print the export as JSON | `false` |
-| `--jsonl` | Print labels as newline-delimited JSON | `false` |
-
-### `scan relationship-calibration`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--connection ` | Only calibrate labels for one KTX connection | — |
-| `--decision ` | Filter: `accepted`, `rejected`, or `all` | `all` |
-| `--accept-threshold ` | Score threshold treated as predicted accepted (0–1) | `0.85` |
-| `--review-threshold ` | Score threshold treated as predicted review (0–1) | `0.55` |
-| `--json` | Print the calibration report as JSON | `false` |
-
-### `scan relationship-thresholds`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--connection ` | Only evaluate labels for one KTX connection | — |
-| `--min-total-labels ` | Minimum scored labels before advice can be ready | `20` |
-| `--min-accepted-labels ` | Minimum accepted labels before advice can be ready | `5` |
-| `--min-rejected-labels ` | Minimum rejected labels before advice can be ready | `5` |
-| `--json` | Print the threshold advice report as JSON | `false` |
+| `--yes` | Install the managed Python runtime without prompting when required | `false` |
+| `--no-input` | Disable interactive managed runtime installation | — |
## Examples
```bash
-# Run a structural scan of a connection
-ktx dev scan my-warehouse
-
-# Run a scan with LLM enrichment
-ktx dev scan my-warehouse --mode enriched
-
-# Run a scan with relationship detection
-ktx dev scan my-warehouse --mode relationships
-
-# Dry-run a scan (don't write results)
-ktx dev scan my-warehouse --dry-run
-
-# Check the status of a scan run
-ktx dev scan status run-abc123
-
-# View the scan report
-ktx dev scan report run-abc123
-
-# View scan report as JSON
-ktx dev scan report run-abc123 --json
-
-# List relationship candidates pending review
-ktx dev scan relationships run-abc123
-
-# List all relationships regardless of status
-ktx dev scan relationships run-abc123 --status all
-
-# Accept a relationship candidate
-ktx dev scan relationships run-abc123 --accept candidate-xyz
-
-# Reject a relationship candidate with a note
-ktx dev scan relationships run-abc123 --reject candidate-xyz --note "false positive"
-
-# Apply all accepted relationships to the manifest
-ktx dev scan relationship-apply run-abc123 --all-accepted
-
-# Preview what would be applied
-ktx dev scan relationship-apply run-abc123 --all-accepted --dry-run
-
-# Export relationship feedback as calibration labels
-ktx dev scan relationship-feedback --json
-
-# Calibrate relationship detection thresholds
-ktx dev scan relationship-calibration --accept-threshold 0.9 --review-threshold 0.6
-
-# Get threshold advice based on review decisions
-ktx dev scan relationship-thresholds
+ktx scan my-warehouse
+ktx scan my-warehouse --mode enriched
+ktx scan my-warehouse --mode relationships
+ktx scan my-warehouse --dry-run
+ktx scan my-warehouse --database-introspection-url http://127.0.0.1:8765
```
## Output
-Scan commands write scan artifacts under the KTX project directory and print status or report summaries. Use `--json` on report and relationship commands when an agent needs structured output.
-
-```json
-{
- "runId": "scan-local-abc123",
- "status": "completed",
- "mode": "structural",
- "changes": {
- "tablesAdded": 42
- }
-}
-```
+`ktx scan` prints a human summary and writes scan artifacts under the KTX project directory unless `--dry-run` is set. Use `ktx status` after a scan to inspect project readiness and next setup work.
## Common errors
@@ -165,5 +41,4 @@ Scan commands write scan artifacts under the KTX project directory and print sta
|-------|-------|----------|
| Scan cannot connect | Connection credentials or network access are invalid | Run `ktx connection test ` and update the connection before scanning |
| Enriched scan cannot describe columns | LLM credentials are missing or invalid | Complete LLM setup with `ktx setup` before enriched scans |
-| Relationship apply writes nothing | No accepted candidates match the provided run id or candidate ids | Inspect `ktx dev scan relationships --status accepted` first |
-| Calibration is not ready | Too few reviewed relationship labels exist | Review and accept/reject more candidates, then rerun calibration |
+| Relationship scan has limited evidence | The connector cannot provide optional validation or statistics | Re-run with a connector that supports the missing capability, or treat relationship output as lower-confidence context |
diff --git a/docs-site/content/docs/cli-reference/ktx-sl.mdx b/docs-site/content/docs/cli-reference/ktx-sl.mdx
index 4ec7bdd1..f5a31b27 100644
--- a/docs-site/content/docs/cli-reference/ktx-sl.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-sl.mdx
@@ -28,6 +28,7 @@ ktx sl [options]
| Flag | Description | Default |
|------|-------------|---------|
| `--connection-id ` | Filter by KTX connection id | — |
+| `--query ` | Search source names and descriptions | — |
| `--output ` | Output mode: `pretty` (default in TTY), `plain` (TSV), or `json` | `pretty` |
| `--json` | Shortcut for `--output=json` (overrides `--output`) | `false` |
@@ -36,6 +37,7 @@ ktx sl [options]
| Flag | Description | Default |
|------|-------------|---------|
| `--connection-id ` | KTX connection id (required) | — |
+| `--json` | Print JSON output | `false` |
### `sl validate`
@@ -55,6 +57,7 @@ ktx sl [options]
| Flag | Description | Default |
|------|-------------|---------|
| `--connection-id ` | KTX connection id | — |
+| `--query-file ` | JSON semantic-layer query file | — |
| `--measure ` | Measure to query; repeatable (at least one required) | — |
| `--dimension ` | Dimension to include; repeatable | — |
| `--filter ` | Filter expression; repeatable | — |
@@ -78,9 +81,15 @@ ktx sl list --connection-id my-warehouse
# List sources as JSON
ktx sl list --json
+# Search sources as JSON
+ktx sl list --json --query "revenue"
+
# Read a source definition
ktx sl read orders --connection-id my-warehouse
+# Read a source definition as JSON
+ktx sl read orders --connection-id my-warehouse --json
+
# Validate a source against the live schema
ktx sl validate orders --connection-id my-warehouse
@@ -119,6 +128,13 @@ ktx sl query \
--dimension orders.created_date \
--execute \
--max-rows 1000
+
+# Execute a query from a JSON file
+ktx sl query \
+ --connection-id my-warehouse \
+ --query-file query.json \
+ --execute \
+ --max-rows 100
```
## Output
diff --git a/docs-site/content/docs/cli-reference/ktx-wiki.mdx b/docs-site/content/docs/cli-reference/ktx-wiki.mdx
index a709ac07..7e45420e 100644
--- a/docs-site/content/docs/cli-reference/ktx-wiki.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-wiki.mdx
@@ -26,19 +26,23 @@ ktx wiki [options]
| Flag | Description | Default |
|------|-------------|---------|
+| `--json` | Print JSON output | `false` |
| `--user-id ` | Local user id | `local` |
### `wiki read`
| Flag | Description | Default |
|------|-------------|---------|
+| `--json` | Print JSON output | `false` |
| `--user-id ` | Local user id | `local` |
### `wiki search`
| Flag | Description | Default |
|------|-------------|---------|
+| `--json` | Print JSON output | `false` |
| `--user-id ` | Local user id | `local` |
+| `--limit ` | Maximum search results | — |
### `wiki write`
@@ -58,12 +62,21 @@ ktx wiki [options]
# List all wiki pages
ktx wiki list
+# List all wiki pages as JSON
+ktx wiki list --json
+
# Read a specific wiki page
ktx wiki read revenue-definitions
+# Read a specific wiki page as JSON
+ktx wiki read revenue-definitions --json
+
# Search wiki pages
ktx wiki search "monthly recurring revenue"
+# Search wiki pages as JSON
+ktx wiki search "monthly recurring revenue" --json --limit 10
+
# Write a global knowledge page
ktx wiki write revenue-definitions \
--summary "Canonical revenue metric definitions" \
@@ -97,13 +110,16 @@ Wiki commands print local knowledge pages and search results. Agents should sear
```json
{
- "results": [
- {
- "key": "revenue-definitions",
- "summary": "Canonical revenue metric definitions",
- "score": 0.92
- }
- ]
+ "kind": "list",
+ "data": {
+ "items": [
+ {
+ "key": "revenue-definitions",
+ "summary": "Canonical revenue metric definitions",
+ "score": 0.92
+ }
+ ]
+ }
}
```
diff --git a/docs-site/content/docs/cli-reference/meta.json b/docs-site/content/docs/cli-reference/meta.json
index a5d7a95f..bed3f98c 100644
--- a/docs-site/content/docs/cli-reference/meta.json
+++ b/docs-site/content/docs/cli-reference/meta.json
@@ -9,7 +9,6 @@
"ktx-sl",
"ktx-wiki",
"ktx-status",
- "ktx-agent",
"ktx-dev"
]
}
diff --git a/docs-site/content/docs/concepts/context-as-code.mdx b/docs-site/content/docs/concepts/context-as-code.mdx
index e40665ec..3c43082e 100644
--- a/docs-site/content/docs/concepts/context-as-code.mdx
+++ b/docs-site/content/docs/concepts/context-as-code.mdx
@@ -59,7 +59,7 @@ dbt / Looker / Metabase / Notion
A typical branch shows a semantic diff: "this ingest added 3 new sources from dbt, updated 2 join definitions based on schema changes, and created 1 knowledge page from a Notion doc." Analytics engineers review the diff, verify that the new sources look correct, and merge.
-Teams usually run this on demand while setting up a source, then schedule it once the source is stable. A cron job or CI schedule can run `ktx ingest --all --no-input` overnight on an ingest branch so the latest dbt manifests, BI metadata, and documentation updates are ready for review each morning.
+Teams usually run this on demand while setting up a source, then schedule it once the source is stable. A cron job or CI schedule can run `ktx ingest run --connection-id --adapter --no-input` overnight on an ingest branch so the latest dbt manifests, BI metadata, and documentation updates are ready for review each morning.
Once merged, agents querying through the KTX CLI see the updated context immediately. No deployment step, no cache invalidation, no restart. The files are the source of truth, and agents read them on every request.
diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx
index ece3ceac..6aef2b14 100644
--- a/docs-site/content/docs/getting-started/quickstart.mdx
+++ b/docs-site/content/docs/getting-started/quickstart.mdx
@@ -211,7 +211,7 @@ KTX writes project state as plain files so agents can inspect and edit changes i
| `semantic-layer//*.yaml` | context build, ingestion, or `ktx sl write` | Semantic source definitions agents use for SQL generation |
| `knowledge/global/*.md` | ingestion or `ktx wiki write --scope global` | Shared business context and metric definitions |
| `knowledge/user//*.md` | `ktx wiki write --scope user` | User-scoped notes for one agent/user context |
-| `.claude/skills/ktx/SKILL.md`, `.agents/skills/ktx/SKILL.md` | CLI-mode agent integration setup | Agent instructions for calling `ktx agent` commands |
+| `.claude/skills/ktx/SKILL.md`, `.agents/skills/ktx/SKILL.md` | CLI-mode agent integration setup | Agent instructions for calling public `ktx` commands |
## Verify it worked
@@ -239,7 +239,7 @@ Agent integration ready: yes (claude-code:project)
| `ktx: command not found` | The KTX package is not installed globally, or the shell cannot find the global binary | Run `npm install -g @kaelio/ktx` and open a new shell |
| LLM health check fails | Missing, invalid, or unauthorized Anthropic API key | Export `ANTHROPIC_API_KEY` or rerun `ktx setup` and choose the file-backed secret option |
| OpenAI embedding check fails | `OPENAI_API_KEY` is missing when OpenAI embeddings are selected | Export `OPENAI_API_KEY`, or rerun setup and choose local sentence-transformers embeddings |
-| Local embeddings hang or fail | The managed Python runtime cannot start or the local model runtime is unavailable | Install `uv`, run `ktx dev runtime doctor`, then run `ktx dev runtime install --feature local-embeddings --yes` and rerun setup |
+| Local embeddings hang or fail | The managed Python runtime cannot start or the local model runtime is unavailable | Install `uv`, run `ktx dev runtime status`, then run `ktx dev runtime install --feature local-embeddings --yes` and rerun setup |
| Database connection test fails | Credentials, network access, warehouse, database, or schema value is wrong | Test the same URL with the database's native client, then rerun `ktx connection add ... --force` or rerun setup |
| `KTX context built: no` in `ktx status` | Setup saved configuration but did not build context | Run `ktx setup` and choose to build context now |
| Agent integration is incomplete | Setup skipped the agents step or the target was not installed | Run `ktx setup --agents --target codex --project` using the target you need |
diff --git a/docs-site/content/docs/guides/building-context.mdx b/docs-site/content/docs/guides/building-context.mdx
index 31d55bac..25d873d9 100644
--- a/docs-site/content/docs/guides/building-context.mdx
+++ b/docs-site/content/docs/guides/building-context.mdx
@@ -12,7 +12,7 @@ Scanning connects to your database and extracts structural metadata. KTX stores
### Running a scan
```bash
-ktx dev scan
+ktx scan
```
This runs a structural scan by default. You can control what the scan does with the `--mode` flag:
@@ -25,25 +25,18 @@ This runs a structural scan by default. You can control what the scan does with
```bash
# Scan with relationship detection
-ktx dev scan my-postgres --mode relationships
+ktx scan my-postgres --mode relationships
# Preview without writing results
-ktx dev scan my-postgres --dry-run
+ktx scan my-postgres --dry-run
```
-### Checking scan status
+### Checking scan results
-Every scan produces a run ID. Use it to check progress or review results:
+Every scan prints a summary and writes local artifacts. Use `ktx status` after a scan to review project readiness and follow-up setup work:
```bash
-# Check status of a scan run
-ktx dev scan status
-
-# Print the full scan report
-ktx dev scan report
-
-# Get the report as JSON for scripting
-ktx dev scan report --json
+ktx status
```
### Relationship detection
@@ -56,49 +49,7 @@ Many databases lack declared foreign keys. KTX infers relationships by scoring c
| 0.55 – 0.84 | `review` | Plausible — needs human review |
| < 0.55 | `rejected` | Low confidence — not applied |
-After a relationship scan, review the candidates:
-
-```bash
-# Show candidates pending review (default)
-ktx dev scan relationships
-
-# Show all candidates regardless of status
-ktx dev scan relationships --status all
-
-# Accept a specific candidate
-ktx dev scan relationships --accept
-
-# Reject a candidate with a note
-ktx dev scan relationships --reject --note "These columns share a name but are unrelated"
-```
-
-Once you've reviewed candidates, apply the accepted ones as joins in your semantic layer:
-
-```bash
-# Apply all accepted relationships
-ktx dev scan relationship-apply --all-accepted
-
-# Preview what would be applied
-ktx dev scan relationship-apply --all-accepted --dry-run
-
-# Apply a specific candidate
-ktx dev scan relationship-apply --candidate
-```
-
-### Calibrating thresholds
-
-As you review more relationships, KTX can evaluate whether the default thresholds (0.85 accept, 0.55 review) are optimal for your schema:
-
-```bash
-# See how your feedback aligns with current thresholds
-ktx dev scan relationship-calibration --connection my-postgres
-
-# Get threshold recommendations (needs 20+ labels, 5+ accepted, 5+ rejected)
-ktx dev scan relationship-thresholds --connection my-postgres
-
-# Export your review decisions as calibration labels
-ktx dev scan relationship-feedback --connection my-postgres
-```
+Relationship scans run with `ktx scan --mode relationships`. This command only executes the scan; relationship review and calibration subcommands are not part of the current CLI surface.
## Ingestion
@@ -115,19 +66,7 @@ Each ingest run follows this flow:
### Running an ingest
```bash
-# Ingest one configured context source
-ktx ingest my-dbt-source
-
-# Ingest every configured context source
-ktx ingest --all
-```
-
-The public `ktx ingest` command uses the source configuration in `ktx.yaml`, including the source `driver` and any adapter-specific paths or credentials.
-
-For adapter-level debugging, use the low-level `ktx dev ingest run` command:
-
-```bash
-ktx dev ingest run --connection-id my-dbt-source --adapter dbt
+ktx ingest run --connection-id my-dbt-source --adapter dbt
```
Useful low-level flags:
@@ -152,7 +91,7 @@ ktx ingest status
ktx ingest watch
# Replay a past ingest run
-ktx dev ingest replay
+ktx ingest replay
```
The `watch` command opens an interactive TUI that shows the memory-flow output — every tool call, LLM decision, and artifact written during the ingest.
@@ -235,7 +174,7 @@ Orders in "pending" status for more than 48 hours are flagged for review.
Every ingest session records a full transcript — tool calls, LLM responses, and write decisions. You can replay any session to debug why a source was written a certain way:
```bash
-ktx dev ingest replay --viz
+ktx ingest replay --viz
```
This opens the same TUI view as the original run, letting you step through the agent's reasoning.
diff --git a/docs-site/content/docs/guides/serving-agents.mdx b/docs-site/content/docs/guides/serving-agents.mdx
index 4285611b..b6f073b8 100644
--- a/docs-site/content/docs/guides/serving-agents.mdx
+++ b/docs-site/content/docs/guides/serving-agents.mdx
@@ -3,37 +3,36 @@ title: Serving Agents
description: Expose your context to Claude Code, Cursor, Codex, and other coding agents.
---
-Once you've built and refined your context, the final step is exposing it to
-coding agents. KTX provides machine-readable CLI commands for direct terminal
-access from Claude Code, Cursor, Codex, OpenCode, and custom agent workflows.
+Once you've built and refined your context, expose it to coding agents through
+the public KTX CLI. Claude Code, Cursor, Codex, OpenCode, and custom agent
+workflows can call the same commands you use at a terminal.
## CLI Commands
-KTX provides a set of machine-readable commands under `ktx agent`. These return
-JSON output designed for programmatic consumption.
+KTX public commands support JSON output for the context reads that agents use
+most often. Use `--project-dir` when the agent is not already running inside the
+KTX project directory.
### Available commands
```bash
-# List available tools and their descriptions
-ktx agent tools --json
-
-# Get project context for planning
-ktx agent context --json
+# Check setup and context readiness
+ktx status --json
```
**Semantic layer:**
```bash
# List sources
-ktx agent sl list --json
-ktx agent sl list --json --connection-id my-postgres
+ktx sl list --json
+ktx sl list --json --connection-id my-postgres
+ktx sl list --json --query "revenue"
# Read a source
-ktx agent sl read orders --json --connection-id my-postgres
+ktx sl read orders --json --connection-id my-postgres
# Run a query from a JSON file
-ktx agent sl query --json \
+ktx sl query --json \
--connection-id my-postgres \
--query-file query.json \
--execute \
@@ -44,20 +43,10 @@ ktx agent sl query --json \
```bash
# Search knowledge pages
-ktx agent wiki search "revenue recognition" --json --limit 10
+ktx wiki search "revenue recognition" --json --limit 10
# Read a specific page
-ktx agent wiki read order-status-definitions --json
-```
-
-**SQL execution:**
-
-```bash
-# Execute read-only SQL with a row limit
-ktx agent sql execute --json \
- --connection-id my-postgres \
- --sql-file query.sql \
- --max-rows 500
+ktx wiki read order-status-definitions --json
```
## Setting Up Your Agent
diff --git a/docs-site/content/docs/integrations/agent-clients.mdx b/docs-site/content/docs/integrations/agent-clients.mdx
index 1c105e1f..8a055fda 100644
--- a/docs-site/content/docs/integrations/agent-clients.mdx
+++ b/docs-site/content/docs/integrations/agent-clients.mdx
@@ -3,7 +3,9 @@ title: Agent Clients
description: Set up KTX with Claude Code, Cursor, Codex, and OpenCode.
---
-KTX integrates with coding agents through CLI skills and command files. These files teach agents to call `ktx agent ...` commands directly from the terminal for semantic-layer context, wiki knowledge, and safe SQL execution.
+KTX integrates with coding agents through CLI skills and command files. These
+files teach agents to call public `ktx` commands directly from the terminal for
+semantic-layer context and wiki knowledge.
Run `ktx setup` and select your agent targets, or configure manually using the snippets below.
@@ -26,17 +28,17 @@ Create `.claude/skills/ktx/SKILL.md`:
```markdown title=".claude/skills/ktx/SKILL.md"
---
name: ktx
-description: Use local KTX semantic context, wiki knowledge, and safe SQL execution for this project.
+description: Use local KTX semantic context and wiki knowledge for this project.
---
Available commands:
-- `ktx agent context --json --project-dir /path/to/project`
-- `ktx agent sl list --json --project-dir /path/to/project`
-- `ktx agent sl read '' --json --project-dir /path/to/project`
-- `ktx agent sl query --json --project-dir /path/to/project --connection-id '' --query-file '' --execute --max-rows 100`
-- `ktx agent wiki search '' --json --project-dir /path/to/project`
-- `ktx agent wiki read '' --json --project-dir /path/to/project`
-- `ktx agent sql execute --json --project-dir /path/to/project --connection-id '' --sql-file '' --max-rows 100`
+- `ktx status --json --project-dir /path/to/project`
+- `ktx sl list --json --project-dir /path/to/project`
+- `ktx sl list --json --project-dir /path/to/project --query ''`
+- `ktx sl read '' --json --project-dir /path/to/project --connection-id ''`
+- `ktx sl query --json --project-dir /path/to/project --connection-id '' --query-file '' --execute --max-rows 100`
+- `ktx wiki search '' --json --project-dir /path/to/project --limit 10`
+- `ktx wiki read '' --json --project-dir /path/to/project`
```
### Workflow tips
@@ -123,22 +125,19 @@ All supported agent clients call the same KTX CLI commands:
| Command | Description |
|---------|-------------|
-| `ktx agent context --json` | Return a compact project context summary |
-| `ktx agent tools --json` | List available agent-facing commands |
-| `ktx agent wiki search --json` | Search knowledge pages |
-| `ktx agent wiki read --json` | Read a knowledge page |
-| `ktx agent wiki write --json` | Write or update a knowledge page |
-| `ktx agent sl list --json` | List semantic layer sources |
-| `ktx agent sl read --json` | Read a semantic source definition |
-| `ktx agent sl write --json` | Write or update a semantic source |
-| `ktx agent sl validate --json` | Validate semantic source definitions |
-| `ktx agent sl query --json` | Execute a semantic layer query when semantic compute is configured |
-| `ktx agent sql execute --json` | Execute read-only SQL with an explicit row limit |
+| `ktx status --json` | Return project setup and context readiness |
+| `ktx wiki search --json` | Search knowledge pages |
+| `ktx wiki read --json` | Read a knowledge page |
+| `ktx wiki write ` | Write or update a knowledge page |
+| `ktx sl list --json` | List semantic-layer sources |
+| `ktx sl list --query --json` | Search semantic-layer sources |
+| `ktx sl read --json --connection-id ` | Read a semantic source definition |
+| `ktx sl write --connection-id ` | Write or update a semantic source |
+| `ktx sl validate --connection-id ` | Validate semantic source definitions |
+| `ktx sl query --json` | Execute a semantic-layer query when semantic compute is configured |
### Security constraints
-- SQL execution is always read-only.
-- Agent SQL execution requires an explicit `--max-rows` limit from 1 to 1000.
- Secrets and credentials are never exposed in command output.
- Commands resolve the project from `--project-dir`, `KTX_PROJECT_DIR`, or the nearest `ktx.yaml`.
diff --git a/docs-site/content/docs/integrations/context-sources.mdx b/docs-site/content/docs/integrations/context-sources.mdx
index 02554e08..904e3f95 100644
--- a/docs-site/content/docs/integrations/context-sources.mdx
+++ b/docs-site/content/docs/integrations/context-sources.mdx
@@ -13,7 +13,7 @@ Agents should configure and ingest context sources in this order:
1. Add the context source connection in `ktx.yaml` or with `ktx setup`.
2. Store tokens as `env:NAME` or `file:/path/to/secret`.
-3. Run `ktx ingest ` for one source or `ktx ingest --all`.
+3. Run `ktx ingest run --connection-id --adapter ` for one source or `ktx ingest run --connection-id --adapter `.
4. Check progress with `ktx ingest status --json`.
5. Review generated `semantic-layer/` YAML and `knowledge/` Markdown files in git.
6. Validate changed semantic sources with `ktx sl validate`.
diff --git a/docs-site/content/docs/integrations/primary-sources.mdx b/docs-site/content/docs/integrations/primary-sources.mdx
index 49200d47..94dc4e44 100644
--- a/docs-site/content/docs/integrations/primary-sources.mdx
+++ b/docs-site/content/docs/integrations/primary-sources.mdx
@@ -511,4 +511,4 @@ No authentication required — SQLite is file-based. The file must be readable b
| Scan returns no tables | Schema/database/project filter is wrong or the user lacks metadata permissions | Verify the schema list and grant metadata read permissions |
| Historic SQL is empty | Query history extension or warehouse history view is unavailable | Enable the warehouse-specific history feature, then rerun scan or setup |
| Column statistics are missing | Connector cannot access stats tables or the warehouse does not expose them | Grant stats permissions where supported; otherwise rely on structural scan output |
-| SQL execution fails through agents | Connection is missing, unreachable, or query execution is disabled | Run `ktx connection test ` and check the agent command flags |
+| Semantic query execution fails | Connection is missing, unreachable, or query execution is disabled | Run `ktx connection test ` and check the `ktx sl query` flags |
diff --git a/docs-site/lib/llm-docs.ts b/docs-site/lib/llm-docs.ts
index 9d9b5c74..69aac698 100644
--- a/docs-site/lib/llm-docs.ts
+++ b/docs-site/lib/llm-docs.ts
@@ -67,12 +67,12 @@ ${link("/docs/guides/writing-context", "Writing Context", "Write semantic source
- [Full documentation](${absoluteUrl("/llms-full.txt")}): All docs pages in one plain-text markdown response
- [Markdown access guide](${absoluteUrl("/docs/ai-resources/markdown-access.md")}): How to fetch llms.txt, llms-full.txt, and per-page Markdown
- [Quickstart markdown](${absoluteUrl("/docs/getting-started/quickstart.md")}): Human setup walkthrough
-- [Agent CLI markdown](${absoluteUrl("/docs/cli-reference/ktx-agent.md")}): Machine-readable agent commands
+- [Semantic-layer CLI markdown](${absoluteUrl("/docs/cli-reference/ktx-sl.md")}): Semantic-layer commands and JSON output
+- [Wiki CLI markdown](${absoluteUrl("/docs/cli-reference/ktx-wiki.md")}): Knowledge page commands and JSON output
## CLI Reference
${link("/docs/cli-reference/ktx-setup", "ktx setup", "Interactive project setup")}
-${link("/docs/cli-reference/ktx-agent", "ktx agent", "Machine-readable commands for coding agents")}
${link("/docs/cli-reference/ktx-sl", "ktx sl", "Semantic-layer commands")}
${link("/docs/cli-reference/ktx-wiki", "ktx wiki", "Knowledge page commands")}
${link("/docs/cli-reference/ktx-connection", "ktx connection", "Connection management commands")}
diff --git a/examples/package-artifacts/README.md b/examples/package-artifacts/README.md
index be161d88..22ecaf92 100644
--- a/examples/package-artifacts/README.md
+++ b/examples/package-artifacts/README.md
@@ -13,10 +13,8 @@ generated local project.
The managed Python runtime smoke requires `uv` on `PATH`, isolates
`KTX_RUNTIME_ROOT`, verifies `ktx dev runtime status`, runs `ktx sl query --yes` to
-install the core runtime from the bundled wheel, checks `ktx dev runtime doctor`,
-starts and reuses the managed daemon, stops it, previews a stale runtime with
-`ktx dev runtime prune --dry-run`, verifies confirmation is required, and removes
-the stale runtime with `ktx dev runtime prune --yes`.
+install the core runtime from the bundled wheel, checks `ktx dev runtime status`,
+starts and reuses the managed daemon, and stops it.
The artifact manifest contains the public `@kaelio/ktx` npm tarball and the
bundled `kaelio-ktx` runtime wheel. The smoke does not install standalone
diff --git a/examples/postgres-historic/README.md b/examples/postgres-historic/README.md
index c8c85cdc..40ae1674 100644
--- a/examples/postgres-historic/README.md
+++ b/examples/postgres-historic/README.md
@@ -95,7 +95,7 @@ note, not a warning.
Run local historic-SQL ingest:
```bash
-pnpm run ktx -- dev ingest run --project-dir /tmp/ktx-postgres-historic \
+pnpm run ktx -- ingest run --project-dir /tmp/ktx-postgres-historic \
--connection-id warehouse \
--adapter historic-sql \
--plain \
@@ -103,7 +103,7 @@ pnpm run ktx -- dev ingest run --project-dir /tmp/ktx-postgres-historic \
--no-input
```
-The full `dev ingest run` path also runs curation WorkUnits, so it requires a
+The full `ingest run` path also runs curation WorkUnits, so it requires a
configured LLM provider.
Inspect the latest manifest:
@@ -127,6 +127,6 @@ table.
- Missing grants: confirm `GRANT pg_read_all_stats TO ktx_reader;`.
- Empty snapshot: rerun `scripts/generate-workload.sh base` and keep
`--historic-sql-min-executions 2` for the smoke.
-- SQL-analysis failures: run `pnpm run ktx -- dev runtime doctor` from the KTX
+- SQL-analysis failures: run `pnpm run ktx -- dev runtime status` from the KTX
repository root and confirm `uv`, the bundled Python wheel, and the managed
runtime all pass.
diff --git a/packages/cli/src/agent-runtime.test.ts b/packages/cli/src/agent-runtime.test.ts
deleted file mode 100644
index 36258f5b..00000000
--- a/packages/cli/src/agent-runtime.test.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { mkdtemp, rm, writeFile } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import {
- KTX_AGENT_MAX_ROWS_CAP,
- createKtxAgentRuntime,
- parseAgentMaxRows,
- readAgentJsonFile,
- writeAgentJson,
- writeAgentJsonError,
-} from './agent-runtime.js';
-
-function makeIo() {
- let stdout = '';
- let stderr = '';
- return {
- io: {
- stdout: { write: (chunk: string) => (stdout += chunk) },
- stderr: { write: (chunk: string) => (stderr += chunk) },
- },
- stdout: () => stdout,
- stderr: () => stderr,
- };
-}
-
-describe('agent runtime helpers', () => {
- let tempDir: string;
-
- beforeEach(async () => {
- tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-runtime-'));
- });
-
- afterEach(async () => {
- await rm(tempDir, { recursive: true, force: true });
- });
-
- it('writes JSON success and error envelopes without color or spinners', () => {
- const successIo = makeIo();
- const errorIo = makeIo();
-
- writeAgentJson(successIo.io, { ok: true });
- writeAgentJsonError(errorIo.io, 'missing source', { code: 'NOT_FOUND' });
-
- expect(JSON.parse(successIo.stdout())).toEqual({ ok: true });
- expect(successIo.stderr()).toBe('');
- expect(JSON.parse(errorIo.stderr())).toEqual({
- ok: false,
- error: { message: 'missing source', code: 'NOT_FOUND' },
- });
- expect(errorIo.stdout()).toBe('');
- });
-
- it('reads JSON query files as objects', async () => {
- const path = join(tempDir, 'query.json');
- await writeFile(path, '{"measures":["revenue"],"limit":50}', 'utf-8');
-
- await expect(readAgentJsonFile(path)).resolves.toEqual({ measures: ['revenue'], limit: 50 });
- });
-
- it('rejects non-object JSON query files', async () => {
- const path = join(tempDir, 'query.json');
- await writeFile(path, '["revenue"]', 'utf-8');
-
- await expect(readAgentJsonFile(path)).rejects.toThrow('must contain a JSON object');
- });
-
- it('requires positive row limits and enforces the agent cap', () => {
- expect(parseAgentMaxRows(100)).toBe(100);
- expect(() => parseAgentMaxRows(undefined)).toThrow('maxRows is required');
- expect(() => parseAgentMaxRows(0)).toThrow('positive integer');
- expect(() => parseAgentMaxRows(KTX_AGENT_MAX_ROWS_CAP + 1)).toThrow(String(KTX_AGENT_MAX_ROWS_CAP));
- });
-
- it('constructs local context ports with semantic compute and query executor', async () => {
- const project = {
- projectDir: tempDir,
- configPath: join(tempDir, 'ktx.yaml'),
- config: { project: 'revenue', connections: {} },
- coreConfig: {},
- git: {},
- fileStore: {},
- } as never;
- const ports = { knowledge: {}, semanticLayer: {} } as never;
- const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
- const queryExecutor = { execute: vi.fn() };
- const loadProject = vi.fn(async () => project);
- const createContextTools = vi.fn(() => ports);
-
- await expect(
- createKtxAgentRuntime(
- { projectDir: tempDir, enableSemanticCompute: true, enableQueryExecution: true },
- {
- loadProject,
- createContextTools,
- createSemanticLayerCompute: () => semanticLayerCompute,
- createQueryExecutor: () => queryExecutor,
- },
- ),
- ).resolves.toMatchObject({ project, ports, queryExecutor });
-
- expect(loadProject).toHaveBeenCalledWith({ projectDir: tempDir });
- expect(createContextTools).toHaveBeenCalledWith(project, {
- semanticLayerCompute,
- queryExecutor,
- });
- });
-
- it('creates managed semantic compute when no test override is injected', async () => {
- const project = {
- projectDir: tempDir,
- configPath: join(tempDir, 'ktx.yaml'),
- config: { project: 'revenue', connections: {} },
- coreConfig: {},
- git: {},
- fileStore: {},
- } as never;
- const ports = { semanticLayer: {} } as never;
- const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
- const loadProject = vi.fn(async () => project);
- const createContextTools = vi.fn(() => ports);
- const createManagedSemanticLayerCompute = vi.fn(async () => semanticLayerCompute);
- const { io } = makeIo();
-
- await expect(
- createKtxAgentRuntime(
- {
- projectDir: tempDir,
- enableSemanticCompute: true,
- enableQueryExecution: false,
- cliVersion: '0.2.0',
- runtimeInstallPolicy: 'auto',
- io,
- },
- {
- loadProject,
- createContextTools,
- createManagedSemanticLayerCompute,
- },
- ),
- ).resolves.toMatchObject({ project, ports, semanticLayerCompute });
-
- expect(createManagedSemanticLayerCompute).toHaveBeenCalledWith({
- cliVersion: '0.2.0',
- installPolicy: 'auto',
- io,
- });
- expect(createContextTools).toHaveBeenCalledWith(project, {
- semanticLayerCompute,
- });
- });
-});
diff --git a/packages/cli/src/agent-runtime.ts b/packages/cli/src/agent-runtime.ts
deleted file mode 100644
index feccae7c..00000000
--- a/packages/cli/src/agent-runtime.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { readFile } from 'node:fs/promises';
-import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
-import type { KtxSemanticLayerComputePort } from '@ktx/context/daemon';
-import { createLocalProjectMcpContextPorts, type KtxMcpContextPorts } from '@ktx/context/mcp';
-import { type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
-import type { KtxCliIo } from './cli-runtime.js';
-import {
- createManagedPythonSemanticLayerComputePort,
- type KtxManagedPythonInstallPolicy,
-} from './managed-python-command.js';
-
-export const KTX_AGENT_MAX_ROWS_CAP = 1000;
-
-export interface KtxAgentRuntimeOptions {
- projectDir: string;
- enableSemanticCompute: boolean;
- enableQueryExecution: boolean;
- cliVersion?: string;
- runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
- io?: KtxCliIo;
-}
-
-export interface KtxAgentRuntime {
- project: KtxLocalProject;
- ports: KtxMcpContextPorts;
- semanticLayerCompute?: KtxSemanticLayerComputePort;
- queryExecutor?: KtxSqlQueryExecutorPort;
-}
-
-export interface KtxAgentRuntimeDeps {
- loadProject?: typeof loadKtxProject;
- createContextTools?: typeof createLocalProjectMcpContextPorts;
- createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
- createManagedSemanticLayerCompute?: typeof createManagedPythonSemanticLayerComputePort;
- createQueryExecutor?: () => KtxSqlQueryExecutorPort;
-}
-
-export function writeAgentJson(io: KtxCliIo, value: unknown): void {
- io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
-}
-
-export function writeAgentJsonError(
- io: KtxCliIo,
- message: string,
- detail: Record = {},
-): void {
- io.stderr.write(`${JSON.stringify({ ok: false, error: { message, ...detail } }, null, 2)}\n`);
-}
-
-export async function readAgentJsonFile(path: string): Promise> {
- const parsed = JSON.parse(await readFile(path, 'utf-8')) as unknown;
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
- throw new Error(`${path} must contain a JSON object.`);
- }
- return parsed as Record;
-}
-
-export function parseAgentMaxRows(value: number | undefined): number {
- if (!Number.isInteger(value) || value === undefined || value <= 0) {
- throw new Error('maxRows is required and must be a positive integer.');
- }
- if (value > KTX_AGENT_MAX_ROWS_CAP) {
- throw new Error(`maxRows must be less than or equal to ${KTX_AGENT_MAX_ROWS_CAP}.`);
- }
- return value;
-}
-
-async function createAgentSemanticLayerCompute(
- options: KtxAgentRuntimeOptions,
- deps: KtxAgentRuntimeDeps,
-): Promise {
- if (!options.enableSemanticCompute) {
- return undefined;
- }
- if (deps.createSemanticLayerCompute) {
- return deps.createSemanticLayerCompute();
- }
- if (!options.cliVersion || !options.runtimeInstallPolicy || !options.io) {
- throw new Error('Managed Python semantic compute requires cliVersion, runtimeInstallPolicy, and io.');
- }
- const createManagedSemanticLayerCompute =
- deps.createManagedSemanticLayerCompute ?? createManagedPythonSemanticLayerComputePort;
- return createManagedSemanticLayerCompute({
- cliVersion: options.cliVersion,
- installPolicy: options.runtimeInstallPolicy,
- io: options.io,
- });
-}
-
-export async function createKtxAgentRuntime(
- options: KtxAgentRuntimeOptions,
- deps: KtxAgentRuntimeDeps = {},
-): Promise {
- const project = await (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
- const semanticLayerCompute = await createAgentSemanticLayerCompute(options, deps);
- const queryExecutor = options.enableQueryExecution
- ? (deps.createQueryExecutor ?? createDefaultLocalQueryExecutor)()
- : undefined;
- const ports = (deps.createContextTools ?? createLocalProjectMcpContextPorts)(project, {
- ...(semanticLayerCompute ? { semanticLayerCompute } : {}),
- ...(queryExecutor ? { queryExecutor } : {}),
- });
- return {
- project,
- ports,
- ...(semanticLayerCompute ? { semanticLayerCompute } : {}),
- ...(queryExecutor ? { queryExecutor } : {}),
- };
-}
diff --git a/packages/cli/src/agent-search-readiness.test.ts b/packages/cli/src/agent-search-readiness.test.ts
deleted file mode 100644
index cfb2999e..00000000
--- a/packages/cli/src/agent-search-readiness.test.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import {
- isMissingProjectConfigError,
- missingConnectionSlSearchReadiness,
- missingProjectSlSearchReadiness,
- noConnectionsSlSearchReadiness,
- noIndexedSourcesSlSearchReadiness,
-} from './agent-search-readiness.js';
-
-describe('agent semantic-layer search readiness guidance', () => {
- it('formats missing project guidance with exact recovery commands', () => {
- expect(missingProjectSlSearchReadiness('/tmp/ktx-search', 'gross revenue')).toEqual({
- code: 'agent_sl_search_missing_project',
- message: 'Semantic-layer search needs an initialized KTX project at /tmp/ktx-search.',
- nextSteps: [
- 'ktx setup --project-dir /tmp/ktx-search',
- 'ktx status --project-dir /tmp/ktx-search',
- 'ktx ingest ',
- 'ktx agent sl list --json --query "gross revenue" --project-dir /tmp/ktx-search',
- ],
- });
- });
-
- it('formats no-connection and no-index guidance without hiding the project path', () => {
- expect(noConnectionsSlSearchReadiness('/tmp/ktx-search', 'revenue')).toMatchObject({
- code: 'agent_sl_search_no_connections',
- message: 'Semantic-layer search found no configured connections in /tmp/ktx-search.',
- });
- expect(noIndexedSourcesSlSearchReadiness('/tmp/ktx-search', 'orders')).toMatchObject({
- code: 'agent_sl_search_no_indexed_sources',
- message: 'Semantic-layer search found no indexed semantic-layer sources in /tmp/ktx-search.',
- });
- });
-
- it('formats unknown connection guidance', () => {
- expect(missingConnectionSlSearchReadiness('/tmp/ktx-search', 'warehouse', 'revenue')).toMatchObject({
- code: 'agent_sl_search_unknown_connection',
- message: 'Semantic-layer search connection "warehouse" is not configured in /tmp/ktx-search.',
- });
- });
-
- it('detects missing ktx.yaml read errors', () => {
- const error = Object.assign(new Error('ENOENT: no such file or directory'), {
- code: 'ENOENT',
- path: '/tmp/ktx-search/ktx.yaml',
- });
-
- expect(isMissingProjectConfigError(error)).toBe(true);
- expect(isMissingProjectConfigError(new Error('other'))).toBe(false);
- });
-});
diff --git a/packages/cli/src/agent-search-readiness.ts b/packages/cli/src/agent-search-readiness.ts
deleted file mode 100644
index c3927613..00000000
--- a/packages/cli/src/agent-search-readiness.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-export type KtxAgentSlSearchReadinessCode =
- | 'agent_sl_search_missing_project'
- | 'agent_sl_search_no_connections'
- | 'agent_sl_search_unknown_connection'
- | 'agent_sl_search_no_indexed_sources';
-
-export interface KtxAgentSlSearchReadinessDetail {
- code: KtxAgentSlSearchReadinessCode;
- message: string;
- nextSteps: string[];
-}
-
-function queryForCommand(query: string | undefined): string {
- const trimmed = query?.trim();
- return trimmed && trimmed.length > 0 ? trimmed : 'revenue';
-}
-
-function projectSearchCommand(projectDir: string, query: string | undefined): string {
- return `ktx agent sl list --json --query ${JSON.stringify(queryForCommand(query))} --project-dir ${projectDir}`;
-}
-
-function baseNextSteps(projectDir: string, query: string | undefined): string[] {
- return [
- `ktx setup --project-dir ${projectDir}`,
- `ktx status --project-dir ${projectDir}`,
- 'ktx ingest ',
- projectSearchCommand(projectDir, query),
- ];
-}
-
-export function missingProjectSlSearchReadiness(
- projectDir: string,
- query: string | undefined,
-): KtxAgentSlSearchReadinessDetail {
- return {
- code: 'agent_sl_search_missing_project',
- message: `Semantic-layer search needs an initialized KTX project at ${projectDir}.`,
- nextSteps: baseNextSteps(projectDir, query),
- };
-}
-
-export function noConnectionsSlSearchReadiness(
- projectDir: string,
- query: string | undefined,
-): KtxAgentSlSearchReadinessDetail {
- return {
- code: 'agent_sl_search_no_connections',
- message: `Semantic-layer search found no configured connections in ${projectDir}.`,
- nextSteps: baseNextSteps(projectDir, query),
- };
-}
-
-export function missingConnectionSlSearchReadiness(
- projectDir: string,
- connectionId: string,
- query: string | undefined,
-): KtxAgentSlSearchReadinessDetail {
- return {
- code: 'agent_sl_search_unknown_connection',
- message: `Semantic-layer search connection "${connectionId}" is not configured in ${projectDir}.`,
- nextSteps: baseNextSteps(projectDir, query),
- };
-}
-
-export function noIndexedSourcesSlSearchReadiness(
- projectDir: string,
- query: string | undefined,
-): KtxAgentSlSearchReadinessDetail {
- return {
- code: 'agent_sl_search_no_indexed_sources',
- message: `Semantic-layer search found no indexed semantic-layer sources in ${projectDir}.`,
- nextSteps: baseNextSteps(projectDir, query),
- };
-}
-
-function errorCode(error: unknown): string | undefined {
- if (typeof error !== 'object' || error === null || !('code' in error)) {
- return undefined;
- }
- const code = (error as { code?: unknown }).code;
- return typeof code === 'string' ? code : undefined;
-}
-
-function errorPath(error: unknown): string | undefined {
- if (typeof error !== 'object' || error === null || !('path' in error)) {
- return undefined;
- }
- const path = (error as { path?: unknown }).path;
- return typeof path === 'string' ? path : undefined;
-}
-
-export function isMissingProjectConfigError(error: unknown): boolean {
- return errorCode(error) === 'ENOENT' && (errorPath(error)?.endsWith('ktx.yaml') ?? false);
-}
diff --git a/packages/cli/src/agent.test.ts b/packages/cli/src/agent.test.ts
deleted file mode 100644
index 2c86598d..00000000
--- a/packages/cli/src/agent.test.ts
+++ /dev/null
@@ -1,428 +0,0 @@
-import { mkdtemp, rm, writeFile } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { buildDefaultKtxProjectConfig } from '@ktx/context/project';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { runKtxAgent } from './agent.js';
-import type { KtxAgentRuntime } from './agent-runtime.js';
-
-function makeIo() {
- let stdout = '';
- let stderr = '';
- return {
- io: {
- stdout: { write: (chunk: string) => (stdout += chunk) },
- stderr: { write: (chunk: string) => (stderr += chunk) },
- },
- stdout: () => stdout,
- stderr: () => stderr,
- };
-}
-
-function runtime(overrides: Record = {}): KtxAgentRuntime {
- const config = buildDefaultKtxProjectConfig('revenue');
- return {
- project: {
- projectDir: '/tmp/revenue',
- configPath: '/tmp/revenue/ktx.yaml',
- config: {
- ...config,
- connections: {
- warehouse: { driver: 'sqlite', path: 'warehouse.sqlite', readonly: true as const },
- },
- },
- coreConfig: {} as KtxAgentRuntime['project']['coreConfig'],
- git: {} as KtxAgentRuntime['project']['git'],
- fileStore: {} as KtxAgentRuntime['project']['fileStore'],
- },
- ports: {
- connections: { list: vi.fn(async () => [{ id: 'warehouse', name: 'warehouse', connectionType: 'sqlite' }]) },
- semanticLayer: {
- listSources: vi.fn(async () => ({
- sources: [
- {
- connectionId: 'warehouse',
- connectionName: 'warehouse',
- name: 'orders',
- columnCount: 2,
- measureCount: 1,
- joinCount: 0,
- },
- ],
- totalSources: 1,
- })),
- readSource: vi.fn(async () => ({ sourceName: 'orders', yaml: 'name: orders\n' })),
- writeSource: vi.fn(async () => ({ success: true, sourceName: 'orders' })),
- validate: vi.fn(async () => ({ success: true, errors: [], warnings: [] })),
- query: vi.fn(async () => ({ sql: 'select 1', headers: ['x'], rows: [[1]], totalRows: 1, plan: {} })),
- },
- knowledge: {
- search: vi.fn(async () => ({
- results: [
- {
- key: 'page-1',
- path: 'knowledge/global/page-1.md',
- scope: 'GLOBAL' as const,
- summary: 'Revenue logic',
- score: 0.9,
- matchReasons: ['lexical' as const],
- },
- ],
- totalFound: 1,
- })),
- read: vi.fn(async () => ({
- key: 'page-1',
- scope: 'GLOBAL' as const,
- summary: 'Revenue logic',
- content: 'Use net revenue.',
- })),
- write: vi.fn(async () => ({ success: true, key: 'page-1', action: 'created' as const })),
- },
- },
- queryExecutor: {
- execute: vi.fn(async () => ({ headers: ['x'], rows: [[1]], totalRows: 1, command: 'SELECT', rowCount: 1 })),
- },
- ...overrides,
- };
-}
-
-function runtimeWithoutConnections(): KtxAgentRuntime {
- const base = runtime();
- return {
- ...base,
- project: {
- ...base.project,
- config: {
- ...base.project.config,
- connections: {},
- },
- },
- ports: {
- ...base.ports,
- semanticLayer: {
- ...base.ports.semanticLayer!,
- listSources: vi.fn(async () => ({ sources: [], totalSources: 0 })),
- },
- },
- };
-}
-
-describe('runKtxAgent', () => {
- let tempDir: string;
-
- beforeEach(async () => {
- tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-'));
- });
-
- afterEach(async () => {
- await rm(tempDir, { recursive: true, force: true });
- });
-
- it('prints tool discovery with every stable command', async () => {
- const io = makeIo();
-
- await expect(runKtxAgent({ command: 'tools', projectDir: tempDir, json: true }, io.io)).resolves.toBe(0);
-
- const body = JSON.parse(io.stdout());
- expect(body.projectDir).toBe(tempDir);
- expect(body.tools.map((tool: { name: string }) => tool.name)).toEqual([
- 'context',
- 'sl.list',
- 'sl.read',
- 'sl.query',
- 'wiki.search',
- 'wiki.read',
- 'sql.execute',
- ]);
- expect(io.stderr()).toBe('');
- });
-
- it('prints project context from setup status, connections, and SL summaries', async () => {
- const io = makeIo();
- const createRuntime = vi.fn(async () => runtime());
- const readSetupStatus = vi.fn(async () => ({ project: { path: tempDir, ready: true }, agents: [] }));
-
- await expect(
- runKtxAgent({ command: 'context', projectDir: tempDir, json: true }, io.io, { createRuntime, readSetupStatus }),
- ).resolves.toBe(0);
-
- expect(JSON.parse(io.stdout())).toMatchObject({
- projectDir: tempDir,
- status: { project: { ready: true } },
- connections: [{ id: 'warehouse' }],
- semanticLayer: { totalSources: 1 },
- });
- });
-
- it('dispatches SL list, SL read, wiki search, and wiki read through local ports', async () => {
- for (const args of [
- { command: 'sl-list' as const, projectDir: tempDir, json: true as const, connectionId: 'warehouse' },
- {
- command: 'sl-read' as const,
- projectDir: tempDir,
- json: true as const,
- connectionId: 'warehouse',
- sourceName: 'orders',
- },
- { command: 'wiki-search' as const, projectDir: tempDir, json: true as const, query: 'revenue', limit: 10 },
- { command: 'wiki-read' as const, projectDir: tempDir, json: true as const, pageId: 'page-1' },
- ]) {
- const io = makeIo();
- await expect(runKtxAgent(args, io.io, { createRuntime: async () => runtime() })).resolves.toBe(0);
- expect(JSON.parse(io.stdout())).toBeTruthy();
- expect(io.stderr()).toBe('');
- }
- });
-
- it('prints wiki hybrid search metadata from the hidden agent wiki search command', async () => {
- const fakeRuntime = runtime();
- const knowledge = fakeRuntime.ports.knowledge;
- if (!knowledge) {
- throw new Error('Expected runtime knowledge port');
- }
- fakeRuntime.ports.knowledge = {
- ...knowledge,
- search: vi.fn(async () => ({
- results: [
- {
- key: 'metrics-revenue',
- path: 'knowledge/global/metrics-revenue.md',
- scope: 'GLOBAL' as const,
- summary: 'Revenue metric definition',
- score: 0.02459016393442623,
- matchReasons: ['lexical' as const, 'token' as const],
- },
- ],
- totalFound: 1,
- })),
- };
- const io = makeIo();
-
- await expect(
- runKtxAgent({ command: 'wiki-search', projectDir: tempDir, json: true, query: 'paid order', limit: 5 }, io.io, {
- createRuntime: async () => fakeRuntime,
- }),
- ).resolves.toBe(0);
-
- expect(JSON.parse(io.stdout())).toEqual({
- results: [
- expect.objectContaining({
- key: 'metrics-revenue',
- path: 'knowledge/global/metrics-revenue.md',
- matchReasons: ['lexical', 'token'],
- }),
- ],
- totalFound: 1,
- });
- });
-
- it('executes SL queries from a JSON query file', async () => {
- const queryFile = join(tempDir, 'sl-query.json');
- const io = makeIo();
- await writeFile(queryFile, '{"measures":["total_revenue"],"dimensions":[]}', 'utf-8');
-
- await expect(
- runKtxAgent(
- {
- command: 'sl-query',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- queryFile,
- execute: true,
- maxRows: 100,
- cliVersion: '0.2.0',
- runtimeInstallPolicy: 'never',
- },
- io.io,
- { createRuntime: async () => runtime() },
- ),
- ).resolves.toBe(0);
-
- expect(JSON.parse(io.stdout())).toMatchObject({ sql: 'select 1', rows: [[1]] });
- });
-
- it('passes managed runtime options into default SL query runtime creation', async () => {
- const queryFile = join(tempDir, 'sl-query.json');
- const io = makeIo();
- const createRuntime = vi.fn(async () => runtime());
- await writeFile(queryFile, '{"measures":["total_revenue"],"dimensions":[]}', 'utf-8');
-
- await expect(
- runKtxAgent(
- {
- command: 'sl-query',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- queryFile,
- execute: false,
- cliVersion: '0.2.0',
- runtimeInstallPolicy: 'auto',
- },
- io.io,
- { createRuntime },
- ),
- ).resolves.toBe(0);
-
- expect(createRuntime).toHaveBeenCalledWith({
- projectDir: tempDir,
- enableSemanticCompute: true,
- enableQueryExecution: false,
- cliVersion: '0.2.0',
- runtimeInstallPolicy: 'auto',
- io: io.io,
- });
- });
-
- it('executes read-only SQL from a SQL file with an explicit row limit', async () => {
- const sqlFile = join(tempDir, 'query.sql');
- const fakeRuntime = runtime();
- const io = makeIo();
- await writeFile(sqlFile, 'select 1', 'utf-8');
-
- await expect(
- runKtxAgent(
- {
- command: 'sql-execute',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- sqlFile,
- maxRows: 100,
- },
- io.io,
- { createRuntime: async () => fakeRuntime as never },
- ),
- ).resolves.toBe(0);
-
- expect(fakeRuntime.queryExecutor?.execute).toHaveBeenCalledWith({
- connectionId: 'warehouse',
- projectDir: '/tmp/revenue',
- connection: { driver: 'sqlite', path: 'warehouse.sqlite', readonly: true },
- sql: 'select 1',
- maxRows: 100,
- });
- });
-
- it('prints guided JSON when semantic-layer search runs outside a project', async () => {
- const io = makeIo();
- const missingProjectError = Object.assign(new Error('ENOENT: no such file or directory'), {
- code: 'ENOENT',
- path: join(tempDir, 'ktx.yaml'),
- });
-
- await expect(
- runKtxAgent(
- { command: 'sl-list', projectDir: tempDir, json: true, query: 'gross revenue' },
- io.io,
- { createRuntime: vi.fn(async () => Promise.reject(missingProjectError)) },
- ),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toEqual({
- ok: false,
- error: {
- code: 'agent_sl_search_missing_project',
- message: `Semantic-layer search needs an initialized KTX project at ${tempDir}.`,
- nextSteps: [
- `ktx setup --project-dir ${tempDir}`,
- `ktx status --project-dir ${tempDir}`,
- 'ktx ingest ',
- `ktx agent sl list --json --query "gross revenue" --project-dir ${tempDir}`,
- ],
- },
- });
- expect(io.stdout()).toBe('');
- });
-
- it('prints guided JSON when semantic-layer search has no configured connections', async () => {
- const io = makeIo();
-
- await expect(
- runKtxAgent(
- { command: 'sl-list', projectDir: tempDir, json: true, query: 'revenue' },
- io.io,
- { createRuntime: async () => runtimeWithoutConnections() },
- ),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toMatchObject({
- ok: false,
- error: {
- code: 'agent_sl_search_no_connections',
- message: `Semantic-layer search found no configured connections in ${tempDir}.`,
- nextSteps: [
- `ktx setup --project-dir ${tempDir}`,
- `ktx status --project-dir ${tempDir}`,
- 'ktx ingest ',
- `ktx agent sl list --json --query "revenue" --project-dir ${tempDir}`,
- ],
- },
- });
- });
-
- it('prints guided JSON when semantic-layer search asks for an unknown connection', async () => {
- const io = makeIo();
-
- await expect(
- runKtxAgent(
- { command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'missing', query: 'revenue' },
- io.io,
- { createRuntime: async () => runtime() },
- ),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toMatchObject({
- ok: false,
- error: {
- code: 'agent_sl_search_unknown_connection',
- message: `Semantic-layer search connection "missing" is not configured in ${tempDir}.`,
- },
- });
- });
-
- it('prints guided JSON when semantic-layer search has no indexed sources', async () => {
- const fakeRuntime = runtime();
- const semanticLayer = fakeRuntime.ports.semanticLayer!;
- fakeRuntime.ports.semanticLayer = {
- ...semanticLayer,
- listSources: vi.fn(async () => ({ sources: [], totalSources: 0 })),
- };
- const io = makeIo();
-
- await expect(
- runKtxAgent(
- { command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'warehouse', query: 'revenue' },
- io.io,
- { createRuntime: async () => fakeRuntime },
- ),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toMatchObject({
- ok: false,
- error: {
- code: 'agent_sl_search_no_indexed_sources',
- message: `Semantic-layer search found no indexed semantic-layer sources in ${tempDir}.`,
- },
- });
- });
-
- it('returns JSON errors when required ports or records are missing', async () => {
- const io = makeIo();
-
- await expect(
- runKtxAgent({ command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'missing' }, io.io, {
- createRuntime: async () =>
- runtime({
- ports: { knowledge: { read: vi.fn(async () => null) } },
- }) as never,
- }),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toMatchObject({
- ok: false,
- error: { message: expect.stringContaining('missing') },
- });
- });
-});
diff --git a/packages/cli/src/agent.ts b/packages/cli/src/agent.ts
deleted file mode 100644
index 61d85b8c..00000000
--- a/packages/cli/src/agent.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-import { readFile } from 'node:fs/promises';
-import type { KtxCliIo } from './cli-runtime.js';
-import {
- createKtxAgentRuntime,
- parseAgentMaxRows,
- readAgentJsonFile,
- writeAgentJson,
- writeAgentJsonError,
- type KtxAgentRuntime,
- type KtxAgentRuntimeDeps,
-} from './agent-runtime.js';
-import {
- isMissingProjectConfigError,
- missingConnectionSlSearchReadiness,
- missingProjectSlSearchReadiness,
- noConnectionsSlSearchReadiness,
- noIndexedSourcesSlSearchReadiness,
- type KtxAgentSlSearchReadinessDetail,
-} from './agent-search-readiness.js';
-import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
-import { readKtxSetupStatus, type KtxSetupStatus } from './setup.js';
-
-export type KtxAgentArgs =
- | { command: 'tools'; projectDir: string; json: true }
- | { command: 'context'; projectDir: string; json: true }
- | { command: 'sl-list'; projectDir: string; json: true; connectionId?: string; query?: string }
- | { command: 'sl-read'; projectDir: string; json: true; connectionId?: string; sourceName: string }
- | {
- command: 'sl-query';
- projectDir: string;
- json: true;
- connectionId: string;
- queryFile: string;
- execute: boolean;
- maxRows?: number;
- cliVersion: string;
- runtimeInstallPolicy: KtxManagedPythonInstallPolicy;
- }
- | { command: 'wiki-search'; projectDir: string; json: true; query: string; limit: number }
- | { command: 'wiki-read'; projectDir: string; json: true; pageId: string }
- | { command: 'sql-execute'; projectDir: string; json: true; connectionId: string; sqlFile: string; maxRows?: number };
-
-export interface KtxAgentDeps extends KtxAgentRuntimeDeps {
- createRuntime?: (options: {
- projectDir: string;
- enableSemanticCompute: boolean;
- enableQueryExecution: boolean;
- cliVersion?: string;
- runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
- io?: KtxCliIo;
- }) => Promise;
- readSetupStatus?: (
- projectDir: string,
- ) => Promise;
-}
-
-const AGENT_TOOLS = [
- { name: 'context', command: 'ktx agent context --json' },
- { name: 'sl.list', command: 'ktx agent sl list --json [--connection-id ] [--query ]' },
- { name: 'sl.read', command: 'ktx agent sl read --json [--connection-id ]' },
- {
- name: 'sl.query',
- command: 'ktx agent sl query --json --connection-id --query-file --execute --max-rows 100',
- },
- { name: 'wiki.search', command: 'ktx agent wiki search --json [--limit 10]' },
- { name: 'wiki.read', command: 'ktx agent wiki read --json' },
- {
- name: 'sql.execute',
- command: 'ktx agent sql execute --json --connection-id --sql-file --max-rows 100',
- },
-] as const;
-
-function writeAgentSlSearchReadinessError(io: KtxCliIo, detail: KtxAgentSlSearchReadinessDetail): void {
- writeAgentJsonError(io, detail.message, { code: detail.code, nextSteps: detail.nextSteps });
-}
-
-async function runtimeFor(args: KtxAgentArgs, deps: KtxAgentDeps, io: KtxCliIo): Promise {
- const needsSemanticCompute = args.command === 'sl-query';
- const needsQueryExecution = args.command === 'sql-execute' || (args.command === 'sl-query' && args.execute);
- const runtimeOptions = {
- projectDir: args.projectDir,
- enableSemanticCompute: needsSemanticCompute,
- enableQueryExecution: needsQueryExecution,
- ...(args.command === 'sl-query'
- ? {
- cliVersion: args.cliVersion,
- runtimeInstallPolicy: args.runtimeInstallPolicy,
- io,
- }
- : {}),
- };
- return deps.createRuntime ? deps.createRuntime(runtimeOptions) : createKtxAgentRuntime(runtimeOptions, deps);
-}
-
-function connectionIdForSource(runtime: KtxAgentRuntime, requested: string | undefined): string {
- if (requested) return requested;
- const ids = Object.keys(runtime.project.config.connections ?? {});
- if (ids.length === 1) return ids[0] as string;
- throw new Error('Use --connection-id when the project has zero or multiple connections.');
-}
-
-export async function runKtxAgent(args: KtxAgentArgs, io: KtxCliIo, deps: KtxAgentDeps = {}): Promise {
- try {
- if (args.command === 'tools') {
- writeAgentJson(io, { projectDir: args.projectDir, tools: AGENT_TOOLS });
- return 0;
- }
-
- const runtime = await runtimeFor(args, deps, io);
-
- if (args.command === 'context') {
- const [status, connections, semanticLayer] = await Promise.all([
- (deps.readSetupStatus ?? readKtxSetupStatus)(args.projectDir),
- runtime.ports.connections?.list() ?? [],
- runtime.ports.semanticLayer?.listSources({}) ?? { sources: [], totalSources: 0 },
- ]);
- writeAgentJson(io, { projectDir: args.projectDir, status, connections, semanticLayer, tools: AGENT_TOOLS });
- return 0;
- }
-
- if (args.command === 'sl-list') {
- const semanticLayer = runtime.ports.semanticLayer;
- if (!semanticLayer) throw new Error('Semantic-layer tools are not available for this project.');
- if (args.query) {
- const connectionIds = Object.keys(runtime.project.config.connections ?? {});
- if (args.connectionId && !runtime.project.config.connections[args.connectionId]) {
- writeAgentSlSearchReadinessError(
- io,
- missingConnectionSlSearchReadiness(args.projectDir, args.connectionId, args.query),
- );
- return 1;
- }
- if (connectionIds.length === 0) {
- writeAgentSlSearchReadinessError(io, noConnectionsSlSearchReadiness(args.projectDir, args.query));
- return 1;
- }
- }
-
- const listed = await semanticLayer.listSources({ connectionId: args.connectionId, query: args.query });
- if (args.query && listed.sources.length === 0) {
- const allSources = await semanticLayer.listSources({ connectionId: args.connectionId });
- if (allSources.totalSources === 0) {
- writeAgentSlSearchReadinessError(io, noIndexedSourcesSlSearchReadiness(args.projectDir, args.query));
- return 1;
- }
- }
-
- writeAgentJson(io, listed);
- return 0;
- }
-
- if (args.command === 'sl-read') {
- const semanticLayer = runtime.ports.semanticLayer;
- if (!semanticLayer) throw new Error('Semantic-layer tools are not available for this project.');
- const source = await semanticLayer.readSource({
- connectionId: connectionIdForSource(runtime, args.connectionId),
- sourceName: args.sourceName,
- });
- if (!source) throw new Error(`Semantic-layer source "${args.sourceName}" was not found.`);
- writeAgentJson(io, source);
- return 0;
- }
-
- if (args.command === 'sl-query') {
- const semanticLayer = runtime.ports.semanticLayer;
- if (!semanticLayer) throw new Error('Semantic-layer tools are not available for this project.');
- const query = await readAgentJsonFile(args.queryFile);
- const maxRows = args.execute ? parseAgentMaxRows(args.maxRows) : args.maxRows;
- writeAgentJson(
- io,
- await semanticLayer.query({
- connectionId: args.connectionId,
- query: { ...query, ...(maxRows !== undefined ? { limit: maxRows } : {}) } as never,
- }),
- );
- return 0;
- }
-
- if (args.command === 'wiki-search') {
- const knowledge = runtime.ports.knowledge;
- if (!knowledge) throw new Error('Wiki tools are not available for this project.');
- writeAgentJson(io, await knowledge.search({ userId: 'agent', query: args.query, limit: args.limit }));
- return 0;
- }
-
- if (args.command === 'wiki-read') {
- const knowledge = runtime.ports.knowledge;
- if (!knowledge) throw new Error('Wiki tools are not available for this project.');
- const page = await knowledge.read({ userId: 'agent', key: args.pageId });
- if (!page) throw new Error(`Wiki page "${args.pageId}" was not found.`);
- writeAgentJson(io, page);
- return 0;
- }
-
- const queryExecutor = runtime.queryExecutor;
- if (!queryExecutor) throw new Error('SQL execution is not available for this project.');
- const connection = runtime.project.config.connections[args.connectionId];
- if (!connection) throw new Error(`Connection "${args.connectionId}" was not found.`);
- const maxRows = parseAgentMaxRows(args.maxRows);
- writeAgentJson(
- io,
- await queryExecutor.execute({
- connectionId: args.connectionId,
- projectDir: runtime.project.projectDir,
- connection,
- sql: await readFile(args.sqlFile, 'utf-8'),
- maxRows,
- }),
- );
- return 0;
- } catch (error) {
- if (args.command === 'sl-list' && args.query && isMissingProjectConfigError(error)) {
- writeAgentSlSearchReadinessError(io, missingProjectSlSearchReadiness(args.projectDir, args.query));
- return 1;
- }
- writeAgentJsonError(io, error instanceof Error ? error.message : String(error));
- return 1;
- }
-}
diff --git a/packages/cli/src/cli-program.ts b/packages/cli/src/cli-program.ts
index e2091bef..7d6a98f3 100644
--- a/packages/cli/src/cli-program.ts
+++ b/packages/cli/src/cli-program.ts
@@ -1,9 +1,9 @@
import { Command, InvalidArgumentError } from '@commander-js/extra-typings';
import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js';
-import { registerAgentCommands } from './commands/agent-commands.js';
import { registerConnectionCommands } from './commands/connection-commands.js';
+import { registerIngestCommands } from './commands/ingest-commands.js';
import { registerWikiCommands } from './commands/knowledge-commands.js';
-import { registerPublicIngestCommands } from './commands/public-ingest-commands.js';
+import { registerScanCommands } from './commands/scan-commands.js';
import { registerSetupCommands } from './commands/setup-commands.js';
import { registerSlCommands } from './commands/sl-commands.js';
import { registerStatusCommands } from './commands/status-commands.js';
@@ -53,7 +53,7 @@ type CommandPathNode = CommandWithGlobalOptions & {
parent?: CommandPathNode | null;
};
-const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']);
+const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'status', 'scan']);
export interface CommandWithGlobalOptions {
opts: () => object;
@@ -151,7 +151,7 @@ function isProjectAwareCommand(path: string[]): boolean {
const rootCommand = path[1];
if (rootCommand === 'dev') {
- return path[2] !== undefined && path[2] !== 'completion' && path[2] !== 'runtime';
+ return path[2] !== undefined && path[2] !== 'runtime';
}
return rootCommand !== undefined && PROJECT_AWARE_ROOT_COMMANDS.has(rootCommand);
}
@@ -176,9 +176,6 @@ function shouldSuppressProjectDirLine(path: string[], options: Record
+ await (ingestDeps.ingest ?? defaultRunIngest)(ingestArgs, ingestIo),
+ });
+ registerScanCommands(program, context);
registerWikiCommands(program, context);
registerSlCommands(program, context);
registerStatusCommands(program, context);
- registerAgentCommands(program, context);
registerDevCommands(program, context);
return program;
diff --git a/packages/cli/src/cli-runtime.ts b/packages/cli/src/cli-runtime.ts
index 8b143373..f303309a 100644
--- a/packages/cli/src/cli-runtime.ts
+++ b/packages/cli/src/cli-runtime.ts
@@ -2,12 +2,10 @@ import { createRequire } from 'node:module';
import type { KtxConnectionMetabaseSetupArgs } from './commands/connection-metabase-setup.js';
import type { KtxConnectionNotionArgs } from './commands/connection-notion.js';
-import type { KtxAgentArgs } from './agent.js';
import type { KtxConnectionArgs } from './connection.js';
import type { KtxDoctorArgs } from './doctor.js';
import type { KtxIngestArgs } from './ingest.js';
import type { KtxKnowledgeArgs } from './knowledge.js';
-import type { KtxPublicIngestArgs } from './public-ingest.js';
import type { KtxRuntimeArgs } from './runtime.js';
import type { KtxScanArgs } from './scan.js';
import type { KtxSetupArgs } from './setup.js';
@@ -31,13 +29,11 @@ export interface KtxCliIo {
export interface KtxCliDeps {
setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise;
- agent?: (args: KtxAgentArgs, io: KtxCliIo) => Promise;
connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise;
connectionNotion?: (args: KtxConnectionNotionArgs, io: KtxCliIo) => Promise;
connectionMetabaseSetup?: (args: KtxConnectionMetabaseSetupArgs, io: KtxCliIo) => Promise;
doctor?: (args: KtxDoctorArgs, io: KtxCliIo) => Promise;
ingest?: (args: KtxIngestArgs, io: KtxCliIo) => Promise;
- publicIngest?: (args: KtxPublicIngestArgs, io: KtxCliIo) => Promise;
runtime?: (args: KtxRuntimeArgs, io: KtxCliIo) => Promise;
scan?: (args: KtxScanArgs, io: KtxCliIo) => Promise;
knowledge?: (args: KtxKnowledgeArgs, io: KtxCliIo) => Promise;
diff --git a/packages/cli/src/command-schemas.ts b/packages/cli/src/command-schemas.ts
index 1a442af7..cb11f2eb 100644
--- a/packages/cli/src/command-schemas.ts
+++ b/packages/cli/src/command-schemas.ts
@@ -53,35 +53,21 @@ export const slQueryCommandSchema = z.object({
command: z.literal('query'),
projectDir: projectDirSchema,
connectionId: z.string().min(1).optional(),
- query: z.object({
- measures: z.array(z.string().min(1)).min(1),
- dimensions: stringArraySchema,
- filters: stringArraySchema.optional(),
- segments: stringArraySchema.optional(),
- order_by: z.array(orderBySchema).optional(),
- limit: z.number().int().positive().optional(),
- include_empty: z.literal(true).optional(),
- }),
+ query: z
+ .object({
+ measures: z.array(z.string().min(1)).min(1),
+ dimensions: stringArraySchema,
+ filters: stringArraySchema.optional(),
+ segments: stringArraySchema.optional(),
+ order_by: z.array(orderBySchema).optional(),
+ limit: z.number().int().positive().optional(),
+ include_empty: z.literal(true).optional(),
+ })
+ .optional(),
+ queryFile: z.string().min(1).optional(),
format: z.enum(['json', 'sql']),
execute: z.boolean(),
cliVersion: z.string().min(1),
runtimeInstallPolicy: z.enum(['prompt', 'auto', 'never']),
maxRows: z.number().int().positive().optional(),
});
-
-export const publicIngestRunCommandSchema = z.object({
- command: z.literal('run'),
- projectDir: projectDirSchema,
- targetConnectionId: safeConnectionIdSchema.optional(),
- all: z.boolean(),
- json: z.boolean(),
- inputMode: z.enum(['auto', 'disabled']),
-});
-
-export const publicIngestReadCommandSchema = z.object({
- command: z.enum(['status', 'watch']),
- projectDir: projectDirSchema,
- runId: z.string().min(1).optional(),
- json: z.boolean(),
- inputMode: z.enum(['auto', 'disabled']),
-});
diff --git a/packages/cli/src/commands/agent-commands.ts b/packages/cli/src/commands/agent-commands.ts
deleted file mode 100644
index 2593991a..00000000
--- a/packages/cli/src/commands/agent-commands.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { Option, type Command } from '@commander-js/extra-typings';
-import type { KtxAgentArgs } from '../agent.js';
-import type { KtxCliCommandContext } from '../cli-program.js';
-import { parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
-import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';
-
-async function runAgent(context: KtxCliCommandContext, args: KtxAgentArgs): Promise {
- const runner = context.deps.agent ?? (await import('../agent.js')).runKtxAgent;
- context.setExitCode(await runner(args, context.io));
-}
-
-function jsonOption(): Option {
- return new Option('--json', 'Print JSON output').makeOptionMandatory();
-}
-
-export function registerAgentCommands(program: Command, context: KtxCliCommandContext): void {
- const agent = program
- .command('agent', { hidden: true })
- .description('Machine-readable KTX commands for coding agents')
- .showHelpAfterError();
-
- agent.hook('preAction', (_thisCommand, actionCommand) => {
- context.writeDebug?.('agent', actionCommand);
- });
-
- agent
- .command('tools')
- .description('Print available agent-facing KTX tools')
- .addOption(jsonOption())
- .action(async (_options, command) => {
- await runAgent(context, { command: 'tools', projectDir: resolveCommandProjectDir(command), json: true });
- });
-
- agent
- .command('context')
- .description('Print project context for agent planning')
- .addOption(jsonOption())
- .action(async (_options, command) => {
- await runAgent(context, { command: 'context', projectDir: resolveCommandProjectDir(command), json: true });
- });
-
- const sl = agent.command('sl').description('Semantic-layer agent commands');
- sl.command('list')
- .description('List semantic-layer sources')
- .addOption(jsonOption())
- .option('--connection-id ', 'Filter by connection id')
- .option('--query ', 'Search source names and descriptions')
- .action(async (options: { connectionId?: string; query?: string }, command) => {
- await runAgent(context, {
- command: 'sl-list',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- ...(options.connectionId ? { connectionId: options.connectionId } : {}),
- ...(options.query ? { query: options.query } : {}),
- });
- });
- sl.command('read')
- .description('Read one semantic-layer source')
- .argument('')
- .addOption(jsonOption())
- .option('--connection-id ', 'Connection id containing the source')
- .action(async (sourceName: string, options: { connectionId?: string }, command) => {
- await runAgent(context, {
- command: 'sl-read',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- sourceName,
- ...(options.connectionId ? { connectionId: options.connectionId } : {}),
- });
- });
- sl.command('query')
- .description('Run a semantic-layer query JSON file')
- .addOption(jsonOption())
- .requiredOption('--connection-id ', 'Connection id for execution')
- .requiredOption('--query-file ', 'JSON semantic-layer query file')
- .option('--execute', 'Execute the compiled query against the connection', false)
- .option('--yes', 'Install the managed Python runtime without prompting when required', false)
- .option('--no-input', 'Disable interactive managed runtime installation')
- .option('--max-rows ', 'Maximum rows to return when executing', parsePositiveIntegerOption)
- .action(
- async (
- options: {
- connectionId: string;
- queryFile: string;
- execute: boolean;
- maxRows?: number;
- yes?: boolean;
- input?: boolean;
- },
- command,
- ) => {
- await runAgent(context, {
- command: 'sl-query',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- connectionId: options.connectionId,
- queryFile: options.queryFile,
- execute: options.execute,
- cliVersion: context.packageInfo.version,
- runtimeInstallPolicy: runtimeInstallPolicyFromFlags(options),
- ...(options.maxRows !== undefined ? { maxRows: options.maxRows } : {}),
- });
- },
- );
-
- const wiki = agent.command('wiki').description('KTX wiki agent commands');
- wiki
- .command('search')
- .description('Search KTX wiki pages')
- .argument('')
- .addOption(jsonOption())
- .option('--limit ', 'Maximum search results', parsePositiveIntegerOption, 10)
- .action(async (query: string, options: { limit: number }, command) => {
- await runAgent(context, {
- command: 'wiki-search',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- query,
- limit: options.limit,
- });
- });
- wiki
- .command('read')
- .description('Read one KTX wiki page')
- .argument('')
- .addOption(jsonOption())
- .action(async (pageId: string, _options, command) => {
- await runAgent(context, { command: 'wiki-read', projectDir: resolveCommandProjectDir(command), json: true, pageId });
- });
-
- const sql = agent.command('sql').description('Safe SQL execution commands');
- sql
- .command('execute')
- .description('Execute read-only SQL with a row limit')
- .addOption(jsonOption())
- .requiredOption('--connection-id ', 'Connection id for execution')
- .requiredOption('--sql-file ', 'SQL file to execute')
- .requiredOption('--max-rows ', 'Maximum rows to return', parsePositiveIntegerOption)
- .action(async (options: { connectionId: string; sqlFile: string; maxRows: number }, command) => {
- await runAgent(context, {
- command: 'sql-execute',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- connectionId: options.connectionId,
- sqlFile: options.sqlFile,
- maxRows: options.maxRows,
- });
- });
-}
diff --git a/packages/cli/src/commands/completion-commands.ts b/packages/cli/src/commands/completion-commands.ts
deleted file mode 100644
index 23c45429..00000000
--- a/packages/cli/src/commands/completion-commands.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { CommandUnknownOpts } from '@commander-js/extra-typings';
-import type { KtxCliCommandContext } from '../cli-program.js';
-import { completeCommanderInput, installZshCompletion, zshCompletionScript } from '../completion.js';
-
-export function registerCompletionCommands(
- program: CommandUnknownOpts,
- context: KtxCliCommandContext,
- completionRoot: CommandUnknownOpts = program,
-): void {
- program
- .command('completion')
- .description('Generate shell completion scripts')
- .command('zsh')
- .description('Generate zsh completion script')
- .option('--install', 'Install zsh completion into ~/.zfunc and update ~/.zshrc', false)
- .action(async (options: { install?: boolean }) => {
- if (options.install === true) {
- const result = await installZshCompletion();
- context.io.stdout.write(`Installed zsh completion: ${result.completionPath}\n`);
- context.io.stdout.write(`Updated zsh config: ${result.zshrcPath}\n`);
- context.io.stdout.write('Restart your shell or run: source ~/.zshrc\n');
- context.setExitCode(0);
- return;
- }
- context.io.stdout.write(zshCompletionScript());
- context.setExitCode(0);
- });
-
- program
- .command('__complete', { hidden: true })
- .description('Internal shell completion endpoint')
- .requiredOption('--shell ', 'Shell requesting completions')
- .requiredOption('--position ', 'Current shell word position', (value) => Number(value))
- .argument('[words...]', 'Current shell words')
- .allowUnknownOption()
- .allowExcessArguments()
- .action((words: string[], options: { shell: string; position: number }) => {
- if (options.shell !== 'zsh') {
- context.setExitCode(1);
- return;
- }
- for (const completion of completeCommanderInput(completionRoot, { position: options.position, words })) {
- context.io.stdout.write(`${completion}\n`);
- }
- context.setExitCode(0);
- });
-}
diff --git a/packages/cli/src/commands/connection-commands.ts b/packages/cli/src/commands/connection-commands.ts
index f3c87709..4ce75057 100644
--- a/packages/cli/src/commands/connection-commands.ts
+++ b/packages/cli/src/commands/connection-commands.ts
@@ -188,7 +188,7 @@ export function registerConnectionCommands(program: Command, context: KtxCliComm
registerConnectionNotionCommands(connection, context);
}
-export function registerConnectionMappingCommands(connection: Command, context: KtxCliCommandContext): void {
+function registerConnectionMappingCommands(connection: Command, context: KtxCliCommandContext): void {
const mapping = connection
.command('mapping')
.description('Manage Metabase warehouse mappings')
diff --git a/packages/cli/src/commands/connection-metabase-commands.ts b/packages/cli/src/commands/connection-metabase-commands.ts
index 1a07be3a..c20b8e86 100644
--- a/packages/cli/src/commands/connection-metabase-commands.ts
+++ b/packages/cli/src/commands/connection-metabase-commands.ts
@@ -88,7 +88,7 @@ export function registerConnectionMetabaseCommands(connection: Command, context:
' ktx connection mapping refresh --auto-accept\n' +
' ktx connection mapping set databaseMappings =\n' +
' ktx connection mapping set-sync-enabled --enabled true\n' +
- ' ktx ingest \n',
+ ' ktx ingest run --connection-id --adapter metabase\n',
)
.option(
'--map ',
diff --git a/packages/cli/src/commands/connection-metabase-setup.test.ts b/packages/cli/src/commands/connection-metabase-setup.test.ts
index cf7308d7..9d462bbd 100644
--- a/packages/cli/src/commands/connection-metabase-setup.test.ts
+++ b/packages/cli/src/commands/connection-metabase-setup.test.ts
@@ -230,7 +230,7 @@ describe('runKtxConnectionMetabaseSetup', () => {
expect(io.stdout()).toContain('Connection: metabase');
expect(io.stdout()).toContain('Discovered 1 database');
- expect(io.stdout()).toContain(`ktx ingest metabase --project-dir ${projectDir}`);
+ expect(io.stdout()).toContain(`ktx ingest run --connection-id metabase --adapter metabase --project-dir ${projectDir}`);
expect(io.stdout()).not.toContain('mb_example');
expect(io.stderr()).not.toContain('mb_example');
@@ -784,7 +784,7 @@ describe('runKtxConnectionMetabaseSetup', () => {
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
- expect(io.stderr()).toContain(`ktx ingest metabase --project-dir ${projectDir}`);
+ expect(io.stderr()).toContain(`ktx ingest run --connection-id metabase --adapter metabase --project-dir ${projectDir}`);
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
diff --git a/packages/cli/src/commands/connection-metabase-setup.ts b/packages/cli/src/commands/connection-metabase-setup.ts
index 9b5e21d7..2321ea3d 100644
--- a/packages/cli/src/commands/connection-metabase-setup.ts
+++ b/packages/cli/src/commands/connection-metabase-setup.ts
@@ -743,7 +743,9 @@ export async function runKtxConnectionMetabaseSetup(
io.stdout.write(`Connection: ${connectionId}\n`);
io.stdout.write(`Discovered ${discovered.length} ${discovered.length === 1 ? 'database' : 'databases'}\n`);
- io.stdout.write(`Next: ktx ingest ${connectionId} --project-dir ${args.projectDir}\n`);
+ io.stdout.write(
+ `Next: ktx ingest run --connection-id ${connectionId} --adapter metabase --project-dir ${args.projectDir}\n`,
+ );
if (args.runIngest) {
const ingestRunner = deps.runPublicIngest ?? runKtxPublicIngest;
@@ -759,7 +761,9 @@ export async function runKtxConnectionMetabaseSetup(
io,
);
if (exitCode !== 0) {
- io.stderr.write(`Ingest failed; re-run: ktx ingest ${connectionId} --project-dir ${args.projectDir}\n`);
+ io.stderr.write(
+ `Ingest failed; re-run: ktx ingest run --connection-id ${connectionId} --adapter metabase --project-dir ${args.projectDir}\n`,
+ );
return 1;
}
}
diff --git a/packages/cli/src/commands/knowledge-commands.ts b/packages/cli/src/commands/knowledge-commands.ts
index c85a118c..f8d716f7 100644
--- a/packages/cli/src/commands/knowledge-commands.ts
+++ b/packages/cli/src/commands/knowledge-commands.ts
@@ -1,5 +1,10 @@
import { type Command, Option } from '@commander-js/extra-typings';
-import { collectOption, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
+import {
+ collectOption,
+ type KtxCliCommandContext,
+ parsePositiveIntegerOption,
+ resolveCommandProjectDir,
+} from '../cli-program.js';
import { wikiWriteCommandSchema } from '../command-schemas.js';
import type { KtxKnowledgeArgs } from '../knowledge.js';
import { profileMark } from '../startup-profile.js';
@@ -24,12 +29,14 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
wiki
.command('list')
.description('List local wiki pages')
+ .option('--json', 'Print JSON output', false)
.option('--user-id ', 'Local user id', 'local')
- .action(async (options: { userId: string }, command) => {
+ .action(async (options: { userId: string; json?: boolean }, command) => {
await runKnowledgeArgs(context, {
command: 'list',
projectDir: resolveCommandProjectDir(command),
userId: options.userId,
+ json: options.json,
});
});
@@ -37,13 +44,15 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
.command('read')
.description('Read one local wiki page')
.argument('', 'Wiki page key')
+ .option('--json', 'Print JSON output', false)
.option('--user-id ', 'Local user id', 'local')
- .action(async (key: string, options: { userId: string }, command) => {
+ .action(async (key: string, options: { userId: string; json?: boolean }, command) => {
await runKnowledgeArgs(context, {
command: 'read',
projectDir: resolveCommandProjectDir(command),
key,
userId: options.userId,
+ json: options.json,
});
});
@@ -51,13 +60,17 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
.command('search')
.description('Search local wiki pages')
.argument('', 'Search query')
+ .option('--json', 'Print JSON output', false)
.option('--user-id ', 'Local user id', 'local')
- .action(async (query: string, options: { userId: string }, command) => {
+ .option('--limit ', 'Maximum search results', parsePositiveIntegerOption)
+ .action(async (query: string, options: { userId: string; json?: boolean; limit?: number }, command) => {
await runKnowledgeArgs(context, {
command: 'search',
projectDir: resolveCommandProjectDir(command),
query,
userId: options.userId,
+ json: options.json,
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
});
});
diff --git a/packages/cli/src/commands/public-ingest-commands.ts b/packages/cli/src/commands/public-ingest-commands.ts
deleted file mode 100644
index dfe63c42..00000000
--- a/packages/cli/src/commands/public-ingest-commands.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { InvalidArgumentError, type Command } from '@commander-js/extra-typings';
-import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
-import { publicIngestReadCommandSchema, publicIngestRunCommandSchema } from '../command-schemas.js';
-import type { KtxPublicIngestArgs, KtxPublicIngestInputMode } from '../public-ingest.js';
-import { profileMark } from '../startup-profile.js';
-
-profileMark('module:commands/public-ingest-commands');
-
-interface PublicIngestOptions {
- all?: boolean;
- json?: boolean;
- input?: boolean;
-}
-
-function inputMode(options: { input?: boolean }): KtxPublicIngestInputMode {
- return options.input === false ? 'disabled' : 'auto';
-}
-
-async function runPublicIngestArgs(context: KtxCliCommandContext, args: KtxPublicIngestArgs): Promise {
- const runner = context.deps.publicIngest ?? (await import('../public-ingest.js')).runKtxPublicIngest;
- context.setExitCode(await runner(args, context.io));
-}
-
-function parsePublicIngestConnectionId(value: string): string {
- if (value === 'run') {
- throw new InvalidArgumentError('run is reserved; use ktx dev ingest run for low-level adapter syntax');
- }
- return value;
-}
-
-export function registerPublicIngestCommands(program: Command, context: KtxCliCommandContext): void {
- const ingest = program
- .command('ingest')
- .description('Build and refresh KTX context from configured sources')
- .usage('[options] [connectionId]')
- .argument('[connectionId]', 'Connection id to ingest', parsePublicIngestConnectionId)
- .option('--all', 'Ingest every eligible configured source', false)
- .option('--json', 'Print JSON output', false)
- .option('--no-input', 'Disable interactive terminal input')
- .addHelpText(
- 'after',
- [
- '',
- 'Examples:',
- ' ktx ingest [options]',
- ' ktx ingest --all [options]',
- ' ktx ingest status [runId] [options]',
- ' ktx ingest watch [runId] [options]',
- '',
- 'Project directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.',
- '',
- ].join('\n'),
- )
- .showHelpAfterError()
- .hook('preAction', (_thisCommand, actionCommand) => {
- context.writeDebug?.('ingest', actionCommand);
- })
- .action(async (connectionId: string | undefined, _options: PublicIngestOptions, command) => {
- const options = command.opts();
- if (options.all === true && connectionId) {
- throw new Error('ktx ingest accepts either --all or , not both');
- }
- const args = publicIngestRunCommandSchema.parse({
- command: 'run',
- projectDir: resolveCommandProjectDir(command),
- ...(connectionId ? { targetConnectionId: connectionId } : {}),
- all: options.all === true,
- json: options.json === true,
- inputMode: inputMode(options),
- });
- await runPublicIngestArgs(context, args);
- });
-
- ingest
- .command('status')
- .description('Print status for the latest or selected public ingest run')
- .argument('[runId]', 'Public ingest run id')
- .option('--json', 'Print JSON output', false)
- .option('--no-input', 'Disable interactive terminal input')
- .action(async (runId: string | undefined, _options: PublicIngestOptions, command) => {
- const options = (command.optsWithGlobals ? command.optsWithGlobals() : command.opts()) as PublicIngestOptions;
- const args = publicIngestReadCommandSchema.parse({
- command: 'status',
- projectDir: resolveCommandProjectDir(command),
- ...(runId ? { runId } : {}),
- json: options.json === true,
- inputMode: inputMode(options),
- });
- await runPublicIngestArgs(context, args);
- });
-
- ingest
- .command('watch')
- .description('Open the latest or selected public ingest visual report')
- .argument('[runId]', 'Public ingest run id')
- .option('--json', 'Print JSON output instead of the visual report', false)
- .option('--no-input', 'Disable interactive terminal input')
- .action(async (runId: string | undefined, _options: PublicIngestOptions, command) => {
- const options = (command.optsWithGlobals ? command.optsWithGlobals() : command.opts()) as PublicIngestOptions;
- const args = publicIngestReadCommandSchema.parse({
- command: 'watch',
- projectDir: resolveCommandProjectDir(command),
- ...(runId ? { runId } : {}),
- json: options.json === true,
- inputMode: inputMode(options),
- });
- await runPublicIngestArgs(context, args);
- });
-}
diff --git a/packages/cli/src/commands/runtime-commands.ts b/packages/cli/src/commands/runtime-commands.ts
index 3ce7d9ba..cf0abb42 100644
--- a/packages/cli/src/commands/runtime-commands.ts
+++ b/packages/cli/src/commands/runtime-commands.ts
@@ -18,7 +18,7 @@ async function runRuntimeArgs(context: KtxCliCommandContext, args: KtxRuntimeArg
export function registerRuntimeCommands(program: Command, context: KtxCliCommandContext): void {
const runtime = program
.command('runtime')
- .description('Install, inspect, and prune the KTX-managed Python runtime')
+ .description('Install, start, stop, and inspect the KTX-managed Python runtime')
.showHelpAfterError();
runtime
@@ -64,7 +64,7 @@ export function registerRuntimeCommands(program: Command, context: KtxCliCommand
runtime
.command('status')
- .description('Show managed Python runtime status')
+ .description('Show managed Python runtime status and readiness checks')
.option('--json', 'Print JSON output', false)
.action(async (options: { json?: boolean }) => {
await runRuntimeArgs(context, {
@@ -73,30 +73,4 @@ export function registerRuntimeCommands(program: Command, context: KtxCliCommand
json: options.json === true,
});
});
-
- runtime
- .command('doctor')
- .description('Check managed Python runtime prerequisites and installation')
- .option('--json', 'Print JSON output', false)
- .action(async (options: { json?: boolean }) => {
- await runRuntimeArgs(context, {
- command: 'doctor',
- cliVersion: context.packageInfo.version,
- json: options.json === true,
- });
- });
-
- runtime
- .command('prune')
- .description('Remove stale managed Python runtimes for older CLI versions')
- .option('--dry-run', 'List stale runtimes without deleting them', false)
- .option('--yes', 'Confirm deletion of stale runtime directories', false)
- .action(async (options: { dryRun?: boolean; yes?: boolean }) => {
- await runRuntimeArgs(context, {
- command: 'prune',
- cliVersion: context.packageInfo.version,
- dryRun: options.dryRun === true,
- yes: options.yes === true,
- });
- });
}
diff --git a/packages/cli/src/commands/scan-commands.ts b/packages/cli/src/commands/scan-commands.ts
index fc30fafa..2c19bcdf 100644
--- a/packages/cli/src/commands/scan-commands.ts
+++ b/packages/cli/src/commands/scan-commands.ts
@@ -1,5 +1,5 @@
-import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
-import { type KtxCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
+import { type Command, InvalidArgumentError } from '@commander-js/extra-typings';
+import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';
import type { KtxScanArgs } from '../scan.js';
import { profileMark } from '../startup-profile.js';
@@ -13,6 +13,16 @@ async function runScanArgs(context: KtxCliCommandContext, args: KtxScanArgs): Pr
type KtxScanModeOption = Extract['mode'];
+const REMOVED_SCAN_SUBCOMMAND_NAMES = new Set([
+ 'status',
+ 'report',
+ 'relationships',
+ 'relationship-apply',
+ 'relationship-feedback',
+ 'relationship-calibration',
+ 'relationship-thresholds',
+]);
+
function parseScanModeOption(value: string): KtxScanModeOption {
if (value === 'structural' || value === 'enriched' || value === 'relationships') {
return value;
@@ -20,82 +30,18 @@ function parseScanModeOption(value: string): KtxScanModeOption {
throw new InvalidArgumentError('Allowed choices are structural, enriched, relationships');
}
-type KtxRelationshipStatusOption = Extract['status'];
-type KtxRelationshipFeedbackDecisionOption = Extract['decision'];
-
-function parseRelationshipStatusOption(value: string): KtxRelationshipStatusOption {
- if (value === 'accepted' || value === 'review' || value === 'rejected' || value === 'skipped' || value === 'all') {
- return value;
- }
- throw new InvalidArgumentError('Allowed choices are accepted, review, rejected, skipped, all');
-}
-
-function parseRelationshipFeedbackDecisionOption(value: string): KtxRelationshipFeedbackDecisionOption {
- if (value === 'accepted' || value === 'rejected' || value === 'all') {
- return value;
- }
- throw new InvalidArgumentError('Allowed choices are accepted, rejected, all');
-}
-
-function parseNonEmptyOption(value: string): string {
- if (value.trim().length === 0) {
- throw new InvalidArgumentError('must not be empty');
+function parseConnectionId(value: string): string {
+ if (REMOVED_SCAN_SUBCOMMAND_NAMES.has(value)) {
+ throw new InvalidArgumentError(`"${value}" is not a scan connection id`);
}
return value;
}
-function parseRelationshipCalibrationThreshold(value: string): number {
- const parsed = Number(value);
- if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 1) {
- return parsed;
- }
- throw new InvalidArgumentError('Allowed range is 0 through 1');
-}
-
-function relationshipDecisionArgs(options: {
- accept?: string;
- reject?: string;
- reviewer?: string;
- note?: string;
- json?: boolean;
-}): Pick<
- Extract,
- 'candidateId' | 'decision' | 'reviewer' | 'note' | 'json'
-> | null {
- const decisionCount = [options.accept !== undefined, options.reject !== undefined].filter(Boolean).length;
- if (decisionCount > 1) {
- throw new Error('Only one relationship review decision option can be used: --accept and --reject conflict');
- }
- if (options.accept !== undefined) {
- return {
- candidateId: options.accept,
- decision: 'accepted',
- reviewer: options.reviewer ?? 'ktx',
- note: options.note ?? null,
- json: options.json === true,
- };
- }
- if (options.reject !== undefined) {
- return {
- candidateId: options.reject,
- decision: 'rejected',
- reviewer: options.reviewer ?? 'ktx',
- note: options.note ?? null,
- json: options.json === true,
- };
- }
- return null;
-}
-
-function collectRelationshipCandidateOption(value: string, previous: string[]): string[] {
- return [...previous, parseNonEmptyOption(value)];
-}
-
export function registerScanCommands(program: Command, context: KtxCliCommandContext): void {
- const scan = program
+ program
.command('scan')
- .description('Run or inspect standalone connection scans')
- .argument('[connectionId]', 'KTX connection id to scan')
+ .description('Run a standalone connection scan')
+ .argument('', 'KTX connection id to scan', parseConnectionId)
.option(
'--mode ',
'Scan mode: structural, enriched, relationships (default: structural)',
@@ -113,13 +59,7 @@ export function registerScanCommands(program: Command, context: KtxCliCommandCon
.hook('preAction', (_thisCommand, actionCommand) => {
context.writeDebug?.('scan', actionCommand);
})
- .action(async (connectionId: string | undefined, options, command) => {
- if (!connectionId) {
- scan.outputHelp();
- context.io.stderr.write('ktx dev scan requires or a subcommand\n');
- context.setExitCode(1);
- return;
- }
+ .action(async (connectionId: string, options, command) => {
const mode = options.mode ?? 'structural';
await runScanArgs(context, {
command: 'run',
@@ -133,226 +73,4 @@ export function registerScanCommands(program: Command, context: KtxCliCommandCon
runtimeInstallPolicy: runtimeInstallPolicyFromFlags(options),
});
});
-
- scan
- .command('status')
- .description('Print status for a local scan run')
- .argument('', 'Local scan run id')
- .addHelpText(
- 'after',
- '\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
- )
- .action(async (runId: string, _options: unknown, command) => {
- await runScanArgs(context, {
- command: 'status',
- projectDir: resolveCommandProjectDir(command),
- runId,
- });
- });
-
- scan
- .command('report')
- .description('Print a local scan report')
- .argument('', 'Local scan run id')
- .option('--json', 'Print the raw scan report JSON', false)
- .addHelpText(
- 'after',
- '\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
- )
- .action(async (runId: string, options, command) => {
- await runScanArgs(context, {
- command: 'report',
- projectDir: resolveCommandProjectDir(command),
- runId,
- json: options.json === true,
- });
- });
-
- scan
- .command('relationships')
- .description('Print relationship artifacts for a local scan run')
- .argument('', 'Local scan run id')
- .option(
- '--status ',
- 'Relationship status: accepted, review, rejected, skipped, all',
- parseRelationshipStatusOption,
- 'review',
- )
- .option('--limit ', 'Maximum relationships to print per status', parsePositiveIntegerOption, 25)
- .addOption(
- new Option('--accept ', 'Record a reviewer accepted decision for a relationship candidate')
- .argParser(parseNonEmptyOption)
- .conflicts('reject'),
- )
- .addOption(
- new Option('--reject ', 'Record a reviewer rejected decision for a relationship candidate')
- .argParser(parseNonEmptyOption)
- .conflicts('accept'),
- )
- .option('--note ', 'Attach a note when recording a relationship review decision')
- .option('--reviewer ', 'Reviewer name for a relationship review decision')
- .option('--json', 'Print relationship artifacts as JSON', false)
- .addHelpText(
- 'after',
- '\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
- )
- .action(async (runId: string, options, command) => {
- const decision = relationshipDecisionArgs(options);
- if (decision) {
- await runScanArgs(context, {
- command: 'relationshipDecision',
- projectDir: resolveCommandProjectDir(command),
- runId,
- candidateId: decision.candidateId,
- decision: decision.decision,
- reviewer: decision.reviewer,
- note: decision.note,
- json: decision.json,
- });
- return;
- }
- await runScanArgs(context, {
- command: 'relationships',
- projectDir: resolveCommandProjectDir(command),
- runId,
- status: options.status,
- json: options.json === true,
- limit: options.limit,
- });
- });
-
- scan
- .command('relationship-apply')
- .description('Apply accepted relationship review decisions as manual manifest joins')
- .argument('', 'Local scan run id')
- .option('--all-accepted', 'Apply all accepted relationship review decisions for the scan run', false)
- .option(
- '--candidate ',
- 'Apply one accepted relationship review decision',
- collectRelationshipCandidateOption,
- [],
- )
- .option('--dry-run', 'Preview relationships that would be written without rewriting manifest shards', false)
- .option('--json', 'Print the apply result as JSON', false)
- .addHelpText(
- 'after',
- '\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
- )
- .action(async (runId: string, options, command) => {
- const parentOptions = command.parent?.opts() as { dryRun?: boolean } | undefined;
- await runScanArgs(context, {
- command: 'relationshipApply',
- projectDir: resolveCommandProjectDir(command),
- runId,
- applyAllAccepted: options.allAccepted === true,
- candidateIds: options.candidate,
- dryRun: options.dryRun === true || parentOptions?.dryRun === true,
- json: options.json === true,
- });
- });
-
- scan
- .command('relationship-feedback')
- .description('Export persisted relationship review decisions as calibration labels')
- .option('--connection ', 'Only export labels for one KTX connection')
- .option(
- '--decision ',
- 'Relationship feedback decision: accepted, rejected, all',
- parseRelationshipFeedbackDecisionOption,
- 'all',
- )
- .addOption(new Option('--json', 'Print the export as JSON').default(false).conflicts('jsonl'))
- .addOption(new Option('--jsonl', 'Print labels as newline-delimited JSON').default(false).conflicts('json'))
- .addHelpText(
- 'after',
- '\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
- )
- .action(async (options, command) => {
- await runScanArgs(context, {
- command: 'relationshipFeedback',
- projectDir: resolveCommandProjectDir(command),
- connectionId: options.connection ?? null,
- decision: options.decision,
- json: options.json === true,
- jsonl: options.jsonl === true,
- });
- });
-
- scan
- .command('relationship-calibration')
- .description('Summarize relationship feedback labels against current score thresholds')
- .option('--connection ', 'Only calibrate labels for one KTX connection')
- .option(
- '--decision ',
- 'Relationship feedback decision: accepted, rejected, all',
- parseRelationshipFeedbackDecisionOption,
- 'all',
- )
- .option(
- '--accept-threshold ',
- 'Score threshold treated as predicted accepted',
- parseRelationshipCalibrationThreshold,
- 0.85,
- )
- .option(
- '--review-threshold ',
- 'Score threshold treated as predicted review',
- parseRelationshipCalibrationThreshold,
- 0.55,
- )
- .option('--json', 'Print the calibration report as JSON', false)
- .addHelpText(
- 'after',
- '\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
- )
- .action(async (options, command) => {
- await runScanArgs(context, {
- command: 'relationshipCalibration',
- projectDir: resolveCommandProjectDir(command),
- connectionId: options.connection ?? null,
- decision: options.decision,
- acceptThreshold: options.acceptThreshold,
- reviewThreshold: options.reviewThreshold,
- json: options.json === true,
- });
- });
-
- scan
- .command('relationship-thresholds')
- .description('Evaluate relationship feedback labels for offline threshold advice')
- .option('--connection ', 'Only evaluate labels for one KTX connection')
- .option(
- '--min-total-labels ',
- 'Minimum scored labels before advice can be ready',
- parsePositiveIntegerOption,
- 20,
- )
- .option(
- '--min-accepted-labels ',
- 'Minimum accepted labels before advice can be ready',
- parsePositiveIntegerOption,
- 5,
- )
- .option(
- '--min-rejected-labels ',
- 'Minimum rejected labels before advice can be ready',
- parsePositiveIntegerOption,
- 5,
- )
- .option('--json', 'Print the threshold advice report as JSON', false)
- .addHelpText(
- 'after',
- '\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
- )
- .action(async (options, command) => {
- await runScanArgs(context, {
- command: 'relationshipThresholds',
- projectDir: resolveCommandProjectDir(command),
- connectionId: options.connection ?? null,
- minTotalLabels: options.minTotalLabels,
- minAcceptedLabels: options.minAcceptedLabels,
- minRejectedLabels: options.minRejectedLabels,
- json: options.json === true,
- });
- });
}
diff --git a/packages/cli/src/commands/sl-commands.ts b/packages/cli/src/commands/sl-commands.ts
index 36d75fac..e1b985a3 100644
--- a/packages/cli/src/commands/sl-commands.ts
+++ b/packages/cli/src/commands/sl-commands.ts
@@ -51,6 +51,7 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
sl.command('list')
.description('List semantic-layer sources')
.option('--connection-id ', 'KTX connection id')
+ .option('--query ', 'Search source names and descriptions')
.addOption(
new Option('--output ', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
'pretty',
@@ -59,26 +60,34 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
]),
)
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
- .action(async (options: { connectionId?: string; output?: 'pretty' | 'plain' | 'json'; json?: boolean }, command) => {
+ .action(
+ async (
+ options: { connectionId?: string; query?: string; output?: 'pretty' | 'plain' | 'json'; json?: boolean },
+ command,
+ ) => {
await runSlArgs(context, {
command: 'list',
projectDir: resolveCommandProjectDir(command),
connectionId: options.connectionId,
+ query: options.query,
output: options.output,
json: options.json,
});
- });
+ },
+ );
sl.command('read')
.description('Read a semantic-layer source')
.argument('', 'Semantic-layer source name')
.requiredOption('--connection-id ', 'KTX connection id')
- .action(async (sourceName: string, options: { connectionId: string }, command) => {
+ .option('--json', 'Print JSON output', false)
+ .action(async (sourceName: string, options: { connectionId: string; json?: boolean }, command) => {
await runSlArgs(context, {
command: 'read',
projectDir: resolveCommandProjectDir(command),
connectionId: options.connectionId,
sourceName,
+ json: options.json,
});
});
@@ -113,6 +122,7 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
sl.command('query')
.description('Compile or execute a semantic-layer query')
.option('--connection-id ', 'KTX connection id')
+ .option('--query-file ', 'JSON semantic-layer query file')
.option('--measure ', 'Measure to query; repeatable', collectOption, [])
.option('--dimension ', 'Dimension to include; repeatable', collectOption, [])
.option('--filter ', 'Filter expression; repeatable', collectOption, [])
@@ -126,22 +136,26 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
.option('--no-input', 'Disable interactive managed runtime installation')
.option('--max-rows ', 'Maximum rows to return when executing', parsePositiveIntegerOption)
.action(async (options, command) => {
- if (options.measure.length === 0) {
+ if (options.measure.length === 0 && !options.queryFile) {
throw new Error('sl query requires at least one --measure');
}
const args = slQueryCommandSchema.parse({
command: 'query',
projectDir: resolveCommandProjectDir(command),
connectionId: options.connectionId,
- query: {
- measures: options.measure,
- dimensions: options.dimension,
- ...(options.filter.length > 0 ? { filters: options.filter } : {}),
- ...(options.segment.length > 0 ? { segments: options.segment } : {}),
- ...(options.orderBy.length > 0 ? { order_by: options.orderBy } : {}),
- ...(options.limit !== undefined ? { limit: options.limit } : {}),
- ...(options.includeEmpty === true ? { include_empty: true } : {}),
- },
+ ...(options.queryFile
+ ? { queryFile: options.queryFile }
+ : {
+ query: {
+ measures: options.measure,
+ dimensions: options.dimension,
+ ...(options.filter.length > 0 ? { filters: options.filter } : {}),
+ ...(options.segment.length > 0 ? { segments: options.segment } : {}),
+ ...(options.orderBy.length > 0 ? { order_by: options.orderBy } : {}),
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
+ ...(options.includeEmpty === true ? { include_empty: true } : {}),
+ },
+ }),
format: options.format,
execute: options.execute === true,
cliVersion: context.packageInfo.version,
diff --git a/packages/cli/src/completion.ts b/packages/cli/src/completion.ts
deleted file mode 100644
index 10e787f6..00000000
--- a/packages/cli/src/completion.ts
+++ /dev/null
@@ -1,353 +0,0 @@
-import { mkdir, readFile, writeFile } from 'node:fs/promises';
-import { homedir } from 'node:os';
-import { dirname, join } from 'node:path';
-import type { CommandUnknownOpts, Option } from '@commander-js/extra-typings';
-
-export interface CompletionRequest {
- position: number;
- words: string[];
-}
-
-interface CompletionCandidate {
- value: string;
- description?: string;
-}
-
-interface CommandWithHiddenFlag extends CommandUnknownOpts {
- _hidden?: boolean;
-}
-
-interface ResolveState {
- command: CommandUnknownOpts;
- pendingOption?: Option;
- positionalIndex: number;
-}
-
-export interface ZshCompletionInstallResult {
- completionPath: string;
- zshrcPath: string;
-}
-
-const KTX_COMPLETION_BLOCK_START = '# >>> ktx completion >>>';
-const KTX_COMPLETION_BLOCK_END = '# <<< ktx completion <<<';
-const KTX_COMPLETION_BLOCK_PATTERN = new RegExp(
- `\\n?${escapeRegExp(KTX_COMPLETION_BLOCK_START)}[\\s\\S]*?${escapeRegExp(KTX_COMPLETION_BLOCK_END)}\\n?`,
- 'g',
-);
-
-export function zshCompletionScript(): string {
- const zshWords = '$' + '{words[@]}';
- const zshCompletionCapture = [
- '$',
- `{(@f)$("${'$'}{ktx_completion_command[@]}" dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}" 2>/dev/null)}`,
- ].join('');
- const zshCompletionsCount = '$' + '{#completions[@]}';
- const zshCompletionCommand = '$' + '(eval "print -r -- $' + '{KTX_COMPLETION_COMMAND:-ktx}")';
-
- return [
- '#compdef ktx',
- '',
- '_ktx() {',
- ' local -a completions',
- ' local -a ktx_completion_command',
- ` ktx_completion_command=("\${(@z)${zshCompletionCommand}}")`,
- ` completions=("${zshCompletionCapture}")`,
- ` if (( ${zshCompletionsCount} )); then`,
- " _describe 'ktx completions' completions",
- ' else',
- ' _files',
- ' fi',
- '}',
- '',
- 'compdef _ktx ktx',
- '',
- ].join('\n');
-}
-
-export async function installZshCompletion(): Promise {
- const homeDir = process.env.HOME || homedir();
- const zshConfigDir = process.env.ZDOTDIR || homeDir;
- const completionDir = join(homeDir, '.zfunc');
- const completionPath = join(completionDir, '_ktx');
- const zshrcPath = join(zshConfigDir, '.zshrc');
-
- await mkdir(completionDir, { recursive: true });
- await mkdir(dirname(zshrcPath), { recursive: true });
- await writeFile(completionPath, zshCompletionScript(), 'utf-8');
-
- const existingZshrc = await readOptionalTextFile(zshrcPath);
- const nextZshrc = updateZshrcCompletionBlock(existingZshrc);
- await writeFile(zshrcPath, nextZshrc, 'utf-8');
-
- return { completionPath, zshrcPath };
-}
-
-export function completeCommanderInput(program: CommandUnknownOpts, request: CompletionRequest): string[] {
- const words = completionWordsForPosition(request.words, request.position);
- const tokens = stripProgramName(program, words);
- const current = tokens.at(-1) ?? '';
- const previous = tokens.slice(0, -1);
- const state = resolveCommandState(program, previous);
-
- return candidatesForState(state, current).map(formatZshCandidate);
-}
-
-function completionWordsForPosition(words: string[], position: number): string[] {
- if (!Number.isInteger(position) || position < 1) {
- return words;
- }
- return words.slice(0, position);
-}
-
-function stripProgramName(program: CommandUnknownOpts, words: string[]): string[] {
- const [first, ...rest] = words;
- if (!first) {
- return [];
- }
- return first === program.name() || first.endsWith(`/${program.name()}`) ? rest : words;
-}
-
-function resolveCommandState(program: CommandUnknownOpts, tokens: string[]): ResolveState {
- let command = program;
- let positionalIndex = 0;
- let pendingOption: Option | undefined;
- let positionalOnly = false;
-
- for (let index = 0; index < tokens.length; index += 1) {
- const token = tokens[index];
- if (pendingOption) {
- pendingOption = undefined;
- continue;
- }
-
- if (token === '--') {
- positionalOnly = true;
- continue;
- }
-
- if (!positionalOnly && token.startsWith('-')) {
- const option = findOption(command, optionNameFromToken(token));
- if (option && !token.includes('=') && optionTakesValue(option)) {
- if (index === tokens.length - 1) {
- pendingOption = option;
- } else if (option.required || !tokens[index + 1]?.startsWith('-')) {
- index += 1;
- }
- }
- continue;
- }
-
- const child = findVisibleSubcommand(command, token);
- if (child) {
- command = child;
- positionalIndex = 0;
- continue;
- }
-
- positionalIndex += 1;
- }
-
- return { command, pendingOption, positionalIndex };
-}
-
-function candidatesForState(state: ResolveState, current: string): CompletionCandidate[] {
- const optionValue = splitOptionValueToken(current);
- if (optionValue) {
- const option = findOption(state.command, optionValue.optionName);
- return choiceCandidates(option?.argChoices, optionValue.valuePrefix, optionValue.optionPrefix);
- }
-
- if (state.pendingOption) {
- return choiceCandidates(state.pendingOption.argChoices, current);
- }
-
- if (current.startsWith('-')) {
- return visibleOptions(state.command)
- .map(optionCandidate)
- .filter((candidate) => candidate.value.startsWith(current));
- }
-
- const commandCandidates = visibleSubcommands(state.command)
- .map(commandCandidate)
- .filter((candidate) => candidate.value.startsWith(current));
- const argument = state.command.registeredArguments[state.positionalIndex];
- return [...commandCandidates, ...choiceCandidates(argument?.argChoices, current)];
-}
-
-function visibleSubcommands(command: CommandUnknownOpts): CommandUnknownOpts[] {
- return command.commands.filter((subcommand) => (subcommand as CommandWithHiddenFlag)._hidden !== true);
-}
-
-function findVisibleSubcommand(command: CommandUnknownOpts, name: string): CommandUnknownOpts | undefined {
- return visibleSubcommands(command).find(
- (subcommand) => subcommand.name() === name || subcommand.aliases().includes(name),
- );
-}
-
-function visibleOptions(command: CommandUnknownOpts): Option[] {
- const options: Option[] = [];
- const seen = new Set