Merge pull request #41 from Kaelio/cli-dev-runtime-status

feat(cli): clean up command surface
This commit is contained in:
Andrey Avtomonov 2026-05-12 23:52:25 +02:00 committed by GitHub
commit 80f298d652
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 406 additions and 2076 deletions

View file

@ -152,32 +152,31 @@ or `--url file:PATH` over literal credential URLs.
KTX installs its Python runtime only when a Python-backed command needs it.
The runtime lives outside the npm cache, is versioned by the installed CLI
version, and is managed by `ktx runtime` commands.
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 runtime doctor` if runtime installation fails:
`ktx dev runtime doctor` if runtime installation fails:
```bash
ktx runtime install --yes
ktx runtime status
ktx runtime doctor
ktx runtime start
ktx runtime stop
ktx runtime prune --dry-run
ktx runtime prune --yes
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`
runtime wheel. The `python/ktx-sl` and `python/ktx-daemon` directories remain
source packages for development, not public release artifacts.
## Serve agents
## Use KTX with agents
KTX integrates with coding agents through CLI skills, an MCP server, or both.
The setup wizard configures this automatically — here's what each mode looks
like.
KTX integrates with coding agents through CLI skills. The setup wizard
configures this automatically.
**CLI skills** — the agent calls `ktx` commands directly through a skill file
installed in your agent's config (e.g., `.claude/skills/ktx/SKILL.md`):
@ -188,27 +187,8 @@ ktx wiki search "revenue definition"
ktx sl validate orders
```
**MCP server** — the agent calls KTX tools over the Model Context Protocol:
```bash
ktx serve --mcp stdio \
--user-id local \
--semantic-compute \
--execute-queries \
--yes
```
This exposes tools for connections, knowledge search, semantic-layer sources,
validation, queries, ingestion, and replay. The `--semantic-compute` flag starts
the managed Python runtime for query planning automatically.
The standalone MCP server exposes `connection_list`, `knowledge_search`,
`knowledge_read`, `knowledge_write`, `sl_list_sources`, `sl_read_source`,
`sl_write_source`, `sl_validate`, `sl_query`, `ingest_trigger`,
`ingest_status`, `ingest_report`, and `ingest_replay`.
Supported agents: Claude Code, Codex, Cursor, OpenCode, and any agent that
reads `.agents/` skills or MCP configuration.
reads `.agents/` skills.
## Workspace packages

View file

@ -47,7 +47,7 @@ export function TerminalPreview() {
<div className="h-2" />
<div>
<span className="term-prompt">$</span>{" "}
<span className="term-cmd">ktx serve</span>
<span className="term-cmd">ktx agent context --json</span>
<span className="term-cursor ml-1" />
</div>
</div>

View file

@ -16,8 +16,7 @@ ktx dev <subcommand> [options]
| Subcommand | Description |
|-----------|-------------|
| `init [directory]` | Initialize a Git-backed KTX project directory |
| `doctor` | Check KTX setup, project, and demo readiness |
| `doctor setup` | Check KTX install, build, and local runtime readiness |
| `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 |
@ -35,19 +34,14 @@ ktx dev <subcommand> [options]
| `--name <name>` | Project name written to `ktx.yaml` | — |
| `--force` | Rewrite `ktx.yaml` and scaffold files in an existing project | `false` |
### `dev doctor`
### `dev runtime`
| Flag | Description | Default |
|------|-------------|---------|
| `--feature <feature>` | Runtime feature level for `install` and `start` (`core` or `local-embeddings`) | `core` |
| `--json` | Print JSON output | `false` |
| `--no-input` | Disable interactive terminal input | — |
### `dev doctor setup`
| Flag | Description | Default |
|------|-------------|---------|
| `--json` | Print JSON output | `false` |
| `--no-input` | Disable interactive terminal input | — |
| `--yes` | Confirm runtime install or prune actions where supported | `false` |
| `--force` | Reinstall or restart where supported | `false` |
### `dev scan`
@ -115,11 +109,11 @@ ktx dev init ./my-project --name "Analytics Context"
# Re-initialize an existing project
ktx dev init --force
# Check project readiness
ktx dev doctor
# Check managed Python runtime readiness
ktx dev runtime doctor
# Check CLI install readiness
ktx dev doctor setup
# Start the managed Python daemon
ktx dev runtime start
# Run a low-level ingest with a specific adapter
ktx dev ingest run --connection-id my-dbt --adapter dbt

View file

@ -1,74 +0,0 @@
---
title: "ktx serve"
description: "Run the MCP stdio server."
---
Start a Model Context Protocol (MCP) server that exposes your KTX project's context to coding agents. The server runs over stdio and provides tools for querying semantic sources, searching knowledge, managing connections, and running ingests.
## Command signature
```bash
ktx serve --mcp stdio [options]
```
## Options
| Flag | Description | Default |
|------|-------------|---------|
| `--mcp <mode>` | MCP transport mode (required; only `stdio` is supported) | — |
| `--user-id <id>` | Local user id | `local` |
| `--semantic-compute` | Enable semantic-layer compute | `false` |
| `--semantic-compute-url <url>` | HTTP semantic-layer compute URL | — |
| `--database-introspection-url <url>` | Daemon URL for live-database introspection | — |
| `--execute-queries` | Allow semantic-layer query execution (requires `--semantic-compute`) | `false` |
| `--memory-capture` | Enable memory capture | `false` |
| `--memory-model <model>` | Memory capture model | — |
## Examples
```bash
# Start the MCP server over stdio
ktx serve --mcp stdio
# Start with semantic-layer compute enabled
ktx serve --mcp stdio --semantic-compute
# Start with query execution enabled
ktx serve --mcp stdio --semantic-compute --execute-queries
# Start with a remote semantic compute backend
ktx serve --mcp stdio --semantic-compute-url http://localhost:8080
# Start with memory capture
ktx serve --mcp stdio --memory-capture
# Use a specific project directory
ktx serve --mcp stdio --project-dir /path/to/my-project
```
## Agent integration
The MCP server is typically configured through `ktx setup --agents` rather than started manually. See the [Serving Agents](/docs/guides/serving-agents) guide and [Agent Clients](/docs/integrations/agent-clients) integration page for per-tool configuration.
## Output
`ktx serve --mcp stdio` communicates through MCP messages on stdio. It is meant to be launched by an agent client, not read directly by a human terminal session.
```json
{
"command": "ktx",
"args": ["serve", "--mcp", "stdio", "--semantic-compute", "--execute-queries"],
"env": {
"KTX_PROJECT_DIR": "/home/user/analytics"
}
}
```
## Common errors
| Error | Cause | Recovery |
|-------|-------|----------|
| Agent cannot start server | The agent config cannot find the `ktx` binary | Install `@kaelio/ktx` globally with `npm install -g @kaelio/ktx` or use an absolute command path in the agent config |
| Semantic tools are unavailable | Server was started without `--semantic-compute` | Add `--semantic-compute` or `--semantic-compute-url` to the server args |
| Query execution is denied | Server was started without `--execute-queries` | Add `--execute-queries` only for trusted projects where read-only execution is intended |
| Context resolves to wrong project | `KTX_PROJECT_DIR` is missing or points elsewhere | Set `KTX_PROJECT_DIR` to the project containing the intended `ktx.yaml` |

View file

@ -49,7 +49,6 @@ ktx setup [options]
| `--agents` | Install agent integration only | `false` |
| `--target <target>` | Agent target (`claude-code`, `codex`, `cursor`, `opencode`, `universal`) | — |
| `--agent-scope <scope>` | Agent install scope (`project` or `global`) | `project` |
| `--agent-install-mode <mode>` | Agent install mode (`cli`, `mcp`, or `both`) | `cli` |
| `--project` | Install agent integration into the project scope | `false` |
| `--global` | Install agent integration into the global target scope (Claude Code and Codex only) | `false` |
| `--skip-agents` | Leave agent integration incomplete for now | `false` |
@ -196,4 +195,4 @@ Agent integration ready: yes (codex:project)
| Setup resumes an unexpected project | `KTX_PROJECT_DIR` or nearest `ktx.yaml` points to another directory | Pass `--project-dir <path>` explicitly |
| Health check for model fails | Provider key or model id is invalid | Set the correct environment variable or secret file and rerun setup |
| Setup cannot run in CI | Interactive prompts need a TTY | Use `--yes --no-input` with explicit flags for required values |
| Agent integration missing | Setup skipped the agents step | Run `ktx setup --agents --target <target> --agent-install-mode both` |
| Agent integration missing | Setup skipped the agents step | Run `ktx setup --agents --target <target>` |

View file

@ -1,9 +1,11 @@
---
title: "ktx status"
description: "Show current project status."
description: "Check KTX setup and project readiness."
---
Print the current setup status of your KTX project — which steps are complete, which need attention, and whether the project is ready for agents.
Run the KTX readiness doctor. Inside a KTX project, this checks setup,
project configuration, semantic search, connections, and related diagnostics.
Outside a project, it checks local CLI setup readiness.
## Command signature
@ -16,6 +18,7 @@ ktx status [options]
| Flag | Description | Default |
|------|-------------|---------|
| `--json` | Print JSON output | `false` |
| `--no-input` | Disable interactive terminal input | — |
## Examples
@ -23,22 +26,26 @@ ktx status [options]
# Show project status
ktx status
# Get status as JSON (useful for scripting)
ktx status --json
# Get status as JSON without interactive input
ktx status --json --no-input
```
## Output
`ktx status` prints readiness for each setup area. Agents should use `ktx status --json` when they need to branch on readiness state.
`ktx status` prints doctor checks. Agents should use `ktx status --json --no-input`
when they need to branch on readiness state.
```json
{
"projectReady": true,
"llmReady": true,
"embeddingsReady": true,
"primarySourcesConfigured": true,
"contextBuilt": true,
"agentIntegrationReady": true
"title": "KTX project doctor",
"checks": [
{
"id": "project-config",
"label": "Project config",
"status": "pass",
"detail": "warehouse"
}
]
}
```
@ -46,6 +53,6 @@ ktx status --json
| Error | Cause | Recovery |
|-------|-------|----------|
| No KTX project found | Current directory has no `ktx.yaml` and `KTX_PROJECT_DIR` is unset | Run from a KTX project or set `KTX_PROJECT_DIR` |
| Project ready is false | One or more setup steps are incomplete | Run `ktx setup` to resume setup |
| Agent integration ready is false | No agent target has been installed | Run `ktx setup --agents --target <target>` |
| No KTX project found | Current directory has no `ktx.yaml` and `KTX_PROJECT_DIR` is unset | `ktx status` runs setup checks; run from a KTX project or set `KTX_PROJECT_DIR` for project checks |
| Project config check fails | The project directory is missing or has an invalid `ktx.yaml` | Run `ktx setup` to resume setup |
| Semantic search check warns | Embeddings are not configured or the provider probe failed | Run `ktx setup` or inspect the check's `fix` field in JSON output |

View file

@ -8,7 +8,6 @@
"ktx-ingest",
"ktx-sl",
"ktx-wiki",
"ktx-serve",
"ktx-status",
"ktx-agent",
"ktx-dev"

View file

@ -61,7 +61,7 @@ A typical branch shows a semantic diff: "this ingest added 3 new sources from db
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.
Once merged, agents querying through KTX's MCP server or 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.
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.
This workflow gives you the same review guarantees you have for dbt models. No semantic source reaches production without a human approving it. But unlike maintaining context manually, the heavy lifting — discovering new tables, drafting source definitions, extracting business rules from documentation — is done by the ingestion agent. You review and approve. You don't write from scratch.

View file

@ -97,7 +97,7 @@ Together, these four pillars give agents enough context to produce analytics art
## How KTX compares
KTX is a context layer with an agent-native semantic layer at its core. MetricFlow, Cube, and Malloy model metrics, dimensions, joins, and generated SQL. KTX covers that semantic-layer work, then adds the context agents need to use and maintain it: knowledge pages, schema scans, provenance, ingestion, validation, and MCP tools.
KTX is a context layer with an agent-native semantic layer at its core. MetricFlow, Cube, and Malloy model metrics, dimensions, joins, and generated SQL. KTX covers that semantic-layer work, then adds the context agents need to use and maintain it: knowledge pages, schema scans, provenance, ingestion, validation, and agent-facing CLI commands.
The workflow is the difference. Traditional semantic layers are powerful, but they are usually built and maintained through manual modeling work, product-specific runtimes, or language-specific workflows. They are not agent-native by default, which makes them harder for agents to inspect, edit, validate, and review in a tight loop. KTX is designed for agents that need to read context, change semantic files, inspect generated SQL, and leave a reviewable git diff.
@ -105,10 +105,10 @@ The workflow is the difference. Traditional semantic layers are powerful, but th
|---|---|---|---|---|
| **Model surface** | Plain YAML sources plus Markdown knowledge pages | YAML semantic models and metrics in a dbt project | YAML or JavaScript cubes, views, access policies, and pre-aggregations | `.malloy` models, query pipelines, notebooks, and annotations |
| **What it models** | Sources, columns, measures, segments, joins, grain, filters, default time dimensions, and context references | Semantic models, entities, dimensions, measures, metrics, time grains, and metric types | Cubes, views, measures, dimensions, segments, joins, hierarchies, policies, and rollups | Sources, joins, dimensions, measures, calculations, nested results, and query pipelines |
| **Agent edit loop** | First-class. Agents can patch small files, save imperfect drafts, run validation, query through MCP, inspect SQL, and refine in the same workflow | Possible, but the interface is a dbt/metric workflow rather than an agent context workflow | Possible through code-first models and platform APIs, but changes are tied to runtime deployment and governance concerns | Possible, but agents must operate in Malloy's language and compiler model |
| **Agent edit loop** | First-class. Agents can patch small files, save imperfect drafts, run validation, query through the CLI, inspect SQL, and refine in the same workflow | Possible, but the interface is a dbt/metric workflow rather than an agent context workflow | Possible through code-first models and platform APIs, but changes are tied to runtime deployment and governance concerns | Possible, but agents must operate in Malloy's language and compiler model |
| **Fan-out safety** | Explicit `grain` plus relationship metadata. KTX detects `one_to_many` fan-out, identifies chasm traps, pre-aggregates independent fact measures into CTEs, and rejects unsafe filters | Dataflow query planning for metric requests, multi-hop joins, metric time, and metric types | Runtime planner, modeled joins, primary keys, views, multi-fact views, and pre-aggregations | Symmetric aggregates and path-based aggregation in the language |
| **SQL generation** | Structured semantic query to canonical SQL, then dialect transpilation with sqlglot | Metric request to optimized query plan, then engine-specific SQL | REST, GraphQL, Postgres-compatible SQL, Semantic SQL, and cached/pre-aggregated execution | Malloy source/query to dialect-specific SQL and result metadata |
| **Context around semantics** | Built in: wiki pages, scan artifacts, relationship inference, ingest transcripts, replay, and agent-facing MCP tools | Primarily metric and dbt project context | Descriptions and `meta.ai_context` inside the semantic model, plus platform agent features | Annotations/tags can carry metadata; surrounding context depends on the application |
| **Context around semantics** | Built in: wiki pages, scan artifacts, relationship inference, ingest transcripts, replay, and agent-facing CLI commands | Primarily metric and dbt project context | Descriptions and `meta.ai_context` inside the semantic model, plus platform agent features | Annotations/tags can carry metadata; surrounding context depends on the application |
| **Best fit** | Agents maintaining analytics code, metrics, joins, SQL, docs, and semantic definitions | Teams standardizing metrics inside dbt workflows | Production semantic APIs, BI integrations, access control, caching, and concurrency | Expressive modeling and exploratory analysis above SQL |
If you do not have a semantic layer, KTX can build an agent-native one from your database schema and enrich it with generated descriptions and knowledge pages. If you already use MetricFlow or LookML, KTX ingests from those tools and merges their context into KTX's files. You can keep your existing BI or metric-serving system while using KTX as the semantic and contextual surface agents work against.

View file

@ -75,8 +75,8 @@ KTX uses embeddings for semantic search over sources, wiki content, schema metad
**Local embeddings** use `all-MiniLM-L6-v2` (384 dimensions) via the KTX managed Python runtime. No API key is needed. KTX can install and start the runtime during setup; to prepare it ahead of time, run:
```bash
ktx runtime install --feature local-embeddings --yes
ktx runtime start --feature local-embeddings
ktx dev runtime install --feature local-embeddings --yes
ktx dev runtime start --feature local-embeddings
```
## Step 3: Connect a database
@ -183,8 +183,6 @@ The final step connects KTX to your coding agent. Choose how agents should acces
```
◆ How should agents use this KTX project?
│ ○ CLI tools and skills
│ ○ MCP server config
│ ○ Both
```
Then select which agents to install for:
@ -200,9 +198,7 @@ Then select which agents to install for:
**CLI mode** writes a skill file (e.g., `.claude/skills/ktx/SKILL.md`) that teaches the agent to call KTX commands directly.
**MCP mode** writes an MCP server configuration (e.g., `.mcp.json`) that lets the agent call KTX tools like `sl_query`, `knowledge_search`, and `sl_write_source` over the Model Context Protocol.
**Custom agent** uses the universal `.agents` target for agents that can read project-local skills or MCP configuration.
**Custom agent** uses the universal `.agents` target for agents that can read project-local skills.
## Generated files
@ -215,7 +211,6 @@ KTX writes project state as plain files so agents can inspect and edit changes i
| `semantic-layer/<connection-id>/*.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/<user-id>/*.md` | `ktx wiki write --scope user` | User-scoped notes for one agent/user context |
| `.mcp.json`, `.cursor/mcp.json`, `.agents/mcp/ktx.json`, `.opencode/mcp.json` | agent integration setup | MCP server configuration for supported agent clients |
| `.claude/skills/ktx/SKILL.md`, `.agents/skills/ktx/SKILL.md` | CLI-mode agent integration setup | Agent instructions for calling `ktx agent` commands |
## Verify it worked
@ -244,10 +239,10 @@ 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 runtime doctor`, then run `ktx 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 doctor`, 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 context build` or rerun `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 --agent-install-mode both --project` using the target you need |
| 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 |
## Next steps

View file

@ -3,103 +3,14 @@ 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 two channels: an **MCP server** for persistent integration with tools like Claude Code and Cursor, and **CLI commands** for direct terminal access.
## MCP Server
The MCP (Model Context Protocol) server gives agents structured access to your entire context layer — semantic sources, knowledge pages, scans, and ingestion — through a standard tool-calling interface.
### Starting the server
```bash
ktx serve --mcp stdio
```
This starts an MCP server on stdio, which is how Claude Code, Cursor, and other MCP-compatible tools communicate with KTX. You typically don't run this manually — your agent's configuration handles it.
### Configuration options
| Flag | Description | Default |
|------|-------------|---------|
| `--mcp <mode>` | MCP transport mode (currently `stdio`) | Required |
| `--user-id <id>` | User identifier for knowledge scoping | `local` |
| `--semantic-compute` | Enable semantic layer planning and query execution | `false` |
| `--semantic-compute-url <url>` | URL for the semantic compute daemon | &mdash; |
| `--database-introspection-url <url>` | Daemon URL for live database access | &mdash; |
| `--execute-queries` | Allow agents to execute SQL queries | `false` |
| `--memory-capture` | Enable memory capture from conversations | `false` |
| `--memory-model <model>` | LLM model for memory capture | &mdash; |
### Available tools
When an agent connects via MCP, it can call these tools:
**Connections**
| Tool | Description |
|------|-------------|
| `connection_list` | List configured data connections |
| `connection_test` | Test a connection through the scan connector |
**Semantic Layer**
| Tool | Description |
|------|-------------|
| `sl_list_sources` | List sources, optionally filtered by connection or search query |
| `sl_read_source` | Read a source YAML by connection and name |
| `sl_write_source` | Create, replace, or delete a source |
| `sl_validate` | Validate sources against the database schema |
| `sl_query` | Execute a semantic query — returns rows, SQL, and query plan |
**Knowledge**
| Tool | Description |
|------|-------------|
| `knowledge_search` | Search knowledge pages by query, returns ranked summaries |
| `knowledge_read` | Read a knowledge page by key |
| `knowledge_write` | Create or replace a knowledge page |
**Scanning**
| Tool | Description |
|------|-------------|
| `scan_trigger` | Run a structural, enriched, or relationship scan |
| `scan_status` | Check the status of a running scan |
| `scan_report` | Read a completed scan report |
| `scan_list_artifacts` | List files produced by a scan run |
| `scan_read_artifact` | Read a scan artifact by path |
**Ingestion**
| Tool | Description |
|------|-------------|
| `ingest_trigger` | Trigger an ingest run for an adapter and connection |
| `ingest_status` | Check ingest progress, including diff and work-unit summaries |
| `ingest_report` | Read a stored ingest report |
| `ingest_replay` | Read the memory-flow replay for a past ingest |
**Memory**
| Tool | Description |
|------|-------------|
| `memory_capture` | Capture knowledge and semantic updates from a conversation |
| `memory_capture_status` | Check the status of a memory capture run |
### How agents use these tools
A typical agent interaction flows like this:
1. Agent calls `connection_list` to see available databases
2. Agent calls `sl_list_sources` to discover what semantic sources exist
3. Agent calls `knowledge_search` to find business context relevant to the user's question
4. Agent calls `sl_query` with measures, dimensions, and filters to get data
5. Agent presents results with the business context it found
Agents should use the semantic layer for analytics questions because it enforces correct joins, grain-aware aggregation, and consistent metric definitions. If SQL execution is enabled, KTX only allows read-only SQL with row limits.
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.
## CLI Commands
For agents that work through the terminal rather than MCP, KTX provides a set of machine-readable commands under `ktx agent`. These return JSON output designed for programmatic consumption.
KTX provides a set of machine-readable commands under `ktx agent`. These return
JSON output designed for programmatic consumption.
### Available commands
@ -149,17 +60,6 @@ ktx agent sql execute --json \
--max-rows 500
```
### When to use CLI vs MCP
| | MCP | CLI |
|---|-----|-----|
| **Best for** | Persistent agent integrations | Terminal-based workflows, scripting |
| **Protocol** | Structured tool calls over stdio | Shell commands with JSON output |
| **Used by** | Claude Code, Cursor, Codex | Shell scripts, custom agents, debugging |
| **State** | Server runs continuously | Stateless per invocation |
Most users should set up MCP — it gives agents richer context and a more natural interaction model. The CLI commands are useful for scripting, debugging, and agents that operate through terminal tools.
## Setting Up Your Agent
The fastest way to connect an agent is through the setup wizard:
@ -168,40 +68,9 @@ The fastest way to connect an agent is through the setup wizard:
ktx setup
```
The agents step auto-detects installed tools and generates the right configuration. For manual setup or per-tool details, see the [Agent Clients](/docs/integrations/agent-clients) integration page.
The agents step auto-detects installed tools and generates the right
configuration. For manual setup or per-tool details, see the
[Agent Clients](/docs/integrations/agent-clients) integration page.
### Quick manual setup
**Claude Code** — add to `.claude/settings.json`:
```json
{
"mcpServers": {
"ktx": {
"command": "ktx",
"args": ["serve", "--mcp", "stdio", "--semantic-compute", "--execute-queries"],
"env": {
"KTX_PROJECT_DIR": "/path/to/your/ktx/project"
}
}
}
}
```
**Cursor** — add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"ktx": {
"command": "ktx",
"args": ["serve", "--mcp", "stdio", "--semantic-compute", "--execute-queries"],
"env": {
"KTX_PROJECT_DIR": "/path/to/your/ktx/project"
}
}
}
}
```
After configuration, the agent can immediately start calling KTX tools — listing sources, searching knowledge, and querying your semantic layer.
After configuration, the agent can immediately call KTX commands to list
sources, search knowledge, and query your semantic layer.

View file

@ -3,10 +3,7 @@ title: Agent Clients
description: Set up KTX with Claude Code, Cursor, Codex, and OpenCode.
---
KTX integrates with coding agents through two channels that can be used independently or together:
- **MCP server** — A persistent Model Context Protocol server that exposes KTX tools (semantic queries, knowledge search, SQL execution) directly to the agent
- **CLI skills** — Command definitions that teach the agent how to invoke KTX via the terminal
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.
Run `ktx setup` and select your agent targets, or configure manually using the snippets below.
@ -19,34 +16,9 @@ During setup, select **Claude Code** from the agent targets. KTX writes:
| Mode | File |
|------|------|
| CLI skills | `.claude/skills/ktx/SKILL.md` |
| MCP server | `.mcp.json` (under `mcpServers.ktx`) |
Both project-scoped and global installations are supported. Global installs write to `~/.claude/skills/ktx/SKILL.md`.
### Manual MCP configuration
Add KTX to `.mcp.json` at your project root:
```json title=".mcp.json"
{
"mcpServers": {
"ktx": {
"command": "ktx",
"args": [
"--project-dir", "/path/to/ktx-project",
"serve",
"--mcp", "stdio",
"--semantic-compute",
"--execute-queries"
],
"env": {}
}
}
}
```
Replace `/path/to/ktx-project` with your KTX project directory. For a pinned local checkout, use the absolute path to the built CLI as the command and arguments generated by `ktx setup`.
### Manual CLI skills configuration
Create `.claude/skills/ktx/SKILL.md`:
@ -69,10 +41,9 @@ Available commands:
### Workflow tips
- Claude Code discovers skills automatically from `.claude/skills/` — no restart needed after setup
- The MCP server starts on-demand when Claude Code first calls a KTX tool
- Use `--semantic-compute` to enable query planning and execution
- Global installation (`~/.claude/skills/ktx/SKILL.md`) makes KTX available in all projects without per-project setup
- Claude Code discovers skills automatically from `.claude/skills/`.
- Global installation makes KTX available in all projects without per-project setup.
- Keep generated skills committed only when your team wants project-local agent instructions in git.
---
@ -85,42 +56,17 @@ During setup, select **Cursor** from the agent targets. KTX writes:
| Mode | File |
|------|------|
| CLI rules | `.cursor/rules/ktx.mdc` |
| MCP server | `.cursor/mcp.json` (under `mcpServers.ktx`) |
Cursor supports project-scoped installation only.
### Manual MCP configuration
Create or edit `.cursor/mcp.json`:
```json title=".cursor/mcp.json"
{
"mcpServers": {
"ktx": {
"command": "ktx",
"args": [
"--project-dir", "/path/to/ktx-project",
"serve",
"--mcp", "stdio",
"--semantic-compute",
"--execute-queries"
],
"env": {}
}
}
}
```
### Manual CLI rules configuration
Create `.cursor/rules/ktx.mdc` with the same content structure as the Claude Code SKILL.md file — Cursor rules use the `.mdc` extension but support the same markdown format with command definitions.
Create `.cursor/rules/ktx.mdc` with the same content structure as the Claude Code `SKILL.md` file. Cursor rules use the `.mdc` extension but support the same markdown command definitions.
### Workflow tips
- After adding MCP config, restart Cursor or reload the window for the server to connect
- Cursor rules in `.cursor/rules/` are automatically loaded into agent context
- MCP tools appear in Cursor's tool list once the server is running
- Project-scoped only — no global installation option
- Cursor rules in `.cursor/rules/` are automatically loaded into agent context.
- Project-scoped installs keep KTX command guidance close to the analytics context repository.
---
@ -133,41 +79,18 @@ During setup, select **Codex** from the agent targets. KTX writes:
| Mode | File |
|------|------|
| CLI skills | `.agents/skills/ktx/SKILL.md` |
| MCP server | `.agents/mcp/ktx.json` (under `mcpServers.ktx`) |
Both project-scoped and global installations are supported. Global installs write to `$CODEX_HOME/skills/ktx/SKILL.md` (defaults to `~/.codex/skills/ktx/SKILL.md`).
### Manual MCP configuration
Create or edit `.agents/mcp/ktx.json`:
```json title=".agents/mcp/ktx.json"
{
"mcpServers": {
"ktx": {
"command": "ktx",
"args": [
"--project-dir", "/path/to/ktx-project",
"serve",
"--mcp", "stdio",
"--semantic-compute",
"--execute-queries"
],
"env": {}
}
}
}
```
### Manual CLI skills configuration
Create `.agents/skills/ktx/SKILL.md` with the same content structure as Claude Code's SKILL.md.
Create `.agents/skills/ktx/SKILL.md` with the same content structure as Claude Code's `SKILL.md`.
### Workflow tips
- Set `CODEX_HOME` environment variable to customize the global installation directory
- Codex shares the `.agents/` directory structure with the universal format — one install covers both
- Global installation makes KTX available across all Codex sessions
- Set `CODEX_HOME` to customize the global installation directory.
- Codex shares the `.agents/` directory structure with the universal format.
- Global installation makes KTX available across all Codex sessions.
---
@ -180,91 +103,44 @@ During setup, select **OpenCode** from the agent targets. KTX writes:
| Mode | File |
|------|------|
| CLI commands | `.opencode/commands/ktx.md` |
| MCP server | `.opencode/mcp.json` (under `mcpServers.ktx`) |
OpenCode supports project-scoped installation only.
### Manual MCP configuration
Create or edit `.opencode/mcp.json`:
```json title=".opencode/mcp.json"
{
"mcpServers": {
"ktx": {
"command": "ktx",
"args": [
"--project-dir", "/path/to/ktx-project",
"serve",
"--mcp", "stdio",
"--semantic-compute",
"--execute-queries"
],
"env": {}
}
}
}
```
### Manual CLI commands configuration
Create `.opencode/commands/ktx.md` with the same command definitions as Claude Code's SKILL.md.
Create `.opencode/commands/ktx.md` with the same command definitions as Claude Code's `SKILL.md`.
### Workflow tips
- OpenCode reads commands from `.opencode/commands/` on startup
- Project-scoped only — no global installation option
- Commands file uses standard markdown format (`.md` extension)
- OpenCode reads commands from `.opencode/commands/` on startup.
- Project-scoped only; use a shared repository template if multiple projects need identical command files.
---
## MCP server reference
## Command reference
All agent clients connect to the same KTX MCP server. The server exposes these tools:
All supported agent clients call the same KTX CLI commands:
| Tool | Description |
|------|-------------|
| `connection_list` | List configured database connections |
| `connection_test` | Test a database connection |
| `knowledge_search` | Semantic + full-text search across knowledge pages |
| `knowledge_read` | Read a specific knowledge page |
| `knowledge_write` | Write or update a knowledge page |
| `sl_list_sources` | List semantic layer sources |
| `sl_read_source` | Read a semantic source definition |
| `sl_write_source` | Write or update a semantic source |
| `sl_validate` | Validate a source against the database schema |
| `sl_query` | Execute a semantic layer query |
| `ingest_trigger` | Trigger an ingestion run |
| `ingest_status` | Check ingestion status |
| `ingest_report` | View an ingestion report |
| `ingest_replay` | Replay a past ingestion session |
| `scan_trigger` | Trigger a structural, enriched, or relationship scan |
| `scan_status` | Check scan status |
| `scan_report` | View a completed scan report |
| `scan_list_artifacts` | List artifacts produced by a scan |
| `scan_read_artifact` | Read a scan artifact |
| `memory_capture` | Capture reusable context from an agent conversation when memory capture is enabled |
| `memory_capture_status` | Check a memory capture run |
### Server flags
| Flag | Description | Default |
|------|-------------|---------|
| `--project-dir` | KTX project directory; otherwise KTX uses `KTX_PROJECT_DIR`, the nearest `ktx.yaml`, or the current directory | Auto-detected |
| `--mcp stdio` | Transport mode (stdio only) | Required |
| `--semantic-compute` | Enable semantic layer queries | `false` |
| `--execute-queries` | Allow read-only SQL execution | `false` |
| `--semantic-compute-url` | Remote compute endpoint URL | — |
| `--database-introspection-url` | Live schema introspection endpoint | — |
| `--memory-capture` | Record agent interactions | `false` |
| `--memory-model` | LLM model for memory processing | — |
| 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 <query> --json` | Search knowledge pages |
| `ktx agent wiki read <key> --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 <source> --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 |
### Security constraints
- SQL execution is always read-only
- Agent CLI SQL execution requires an explicit `--max-rows` limit from 1 to 1000; MCP semantic queries default to a 1000-row cap
- Secrets and credentials are never exposed in tool responses
- The server runs as a child process of the agent client (no network exposure)
- 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`.
---
@ -272,8 +148,6 @@ All agent clients connect to the same KTX MCP server. The server exposes these t
| | Claude Code | Cursor | Codex | OpenCode |
|---|---|---|---|---|
| MCP support | Yes | Yes | Yes | Yes |
| CLI skills | Yes | Yes (.mdc) | Yes | Yes |
| Global install | Yes | No | Yes | No |
| Config location | `.mcp.json` | `.cursor/mcp.json` | `.agents/mcp/ktx.json` | `.opencode/mcp.json` |
| Skills location | `.claude/skills/` | `.cursor/rules/` | `.agents/skills/` | `.opencode/commands/` |
| Config location | `.claude/skills/ktx/SKILL.md` | `.cursor/rules/ktx.mdc` | `.agents/skills/ktx/SKILL.md` | `.opencode/commands/ktx.md` |

View file

@ -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 execution is disabled in the server | Run `ktx connection test <id>` and check `ktx serve` flags |
| SQL execution fails through agents | Connection is missing, unreachable, or query execution is disabled | Run `ktx connection test <id>` and check the agent command flags |

View file

@ -2,9 +2,9 @@
## local-warehouse
`local-warehouse/` is a runnable standalone KTX project for local CLI and MCP
smoke testing. It uses the fake ingest adapter and does not require a database
or external app server.
`local-warehouse/` is a runnable standalone KTX project for local CLI smoke
testing. It uses the fake ingest adapter and does not require a database or
external app server.
Copy it before running commands:

View file

@ -1,8 +1,8 @@
# Local Warehouse Example
This example is a standalone KTX project that can be copied to a temp directory
and used with the local CLI and stdio MCP server. It uses the `fake` ingest
adapter so it does not require a database or external app server.
and used with the local CLI. It uses the `fake` ingest adapter so it does not
require a database or external app server.
Run the example from the repository root after building the CLI:

View file

@ -12,11 +12,11 @@ imports the package entry point, and runs installed `ktx` commands against a
generated local project.
The managed Python runtime smoke requires `uv` on `PATH`, isolates
`KTX_RUNTIME_ROOT`, verifies `ktx runtime status`, runs `ktx sl query --yes` to
install the core runtime from the bundled wheel, checks `ktx runtime doctor`,
`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 runtime prune --dry-run`, verifies confirmation is required, and removes
the stale runtime with `ktx runtime prune --yes`.
`ktx dev runtime prune --dry-run`, verifies confirmation is required, and removes
the stale runtime with `ktx dev runtime prune --yes`.
The artifact manifest contains the public `@kaelio/ktx` npm tarball and the
bundled `kaelio-ktx` runtime wheel. The smoke does not install standalone

View file

@ -78,13 +78,13 @@ node packages/cli/dist/bin.js --project-dir /tmp/ktx-postgres-historic setup \
### Readiness check
```bash
pnpm run ktx -- dev doctor --project-dir /tmp/ktx-postgres-historic --no-input
pnpm run ktx -- status --project-dir /tmp/ktx-postgres-historic --no-input
```
The installed CLI form is:
```bash
ktx dev doctor --project-dir /tmp/ktx-postgres-historic --no-input
ktx status --project-dir /tmp/ktx-postgres-historic --no-input
```
Expected output includes `PASS Postgres Historic SQL (warehouse)` when
@ -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 -- runtime doctor` from the KTX
- SQL-analysis failures: run `pnpm run ktx -- dev runtime doctor` from the KTX
repository root and confirm `uv`, the bundled Python wheel, and the managed
runtime all pass.

View file

@ -15,7 +15,7 @@ unset KTX_SQL_ANALYSIS_URL
cleanup() {
if [[ -f "$KTX_BIN" ]]; then
node "$KTX_BIN" runtime stop >/dev/null 2>&1 || true
node "$KTX_BIN" dev runtime stop >/dev/null 2>&1 || true
fi
if [[ "${KTX_POSTGRES_HISTORIC_KEEP_DOCKER:-0}" != "1" ]]; then
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
@ -220,8 +220,8 @@ node "$KTX_BIN" --project-dir "$PROJECT_DIR" setup \
--yes \
--no-input
node "$KTX_BIN" runtime install --yes
node "$KTX_BIN" runtime start
node "$KTX_BIN" dev runtime install --yes
node "$KTX_BIN" dev runtime start
FIRST_RECORD="$PROJECT_PARENT/first-record.json"
run_historic_stage_only "historic-first-$$" "$FIRST_RECORD"

View file

@ -44,7 +44,6 @@
"@ktx/connector-sqlserver": "workspace:*",
"@ktx/context": "workspace:*",
"@ktx/llm": "workspace:*",
"@modelcontextprotocol/sdk": "^1.27.1",
"commander": "14.0.3",
"ink": "^7.0.1",
"react": "^19.2.5",

View file

@ -4,8 +4,6 @@ import { registerAgentCommands } from './commands/agent-commands.js';
import { registerConnectionCommands } from './commands/connection-commands.js';
import { registerWikiCommands } from './commands/knowledge-commands.js';
import { registerPublicIngestCommands } from './commands/public-ingest-commands.js';
import { registerRuntimeCommands } from './commands/runtime-commands.js';
import { registerServeCommands } from './commands/serve-commands.js';
import { registerSetupCommands } from './commands/setup-commands.js';
import { registerSlCommands } from './commands/sl-commands.js';
import { registerStatusCommands } from './commands/status-commands.js';
@ -145,13 +143,23 @@ function isProjectAwareCommand(path: string[]): boolean {
const rootCommand = path[1];
if (rootCommand === 'dev') {
return path[2] !== undefined && path[2] !== 'completion';
return path[2] !== undefined && path[2] !== 'completion' && path[2] !== 'runtime';
}
return rootCommand !== undefined && PROJECT_AWARE_ROOT_COMMANDS.has(rootCommand);
}
function shouldSuppressProjectDirLine(path: string[], options: Record<string, unknown>): boolean {
if (path.join(' ') === 'ktx dev init') {
const commandPathKey = path.join(' ');
if (commandPathKey === 'ktx dev init') {
return true;
}
if (
commandPathKey === 'ktx status' &&
typeof options.projectDir !== 'string' &&
process.env.KTX_PROJECT_DIR === undefined &&
!findNearestKtxProjectDir(process.cwd())
) {
return true;
}
@ -159,7 +167,6 @@ function shouldSuppressProjectDirLine(path: string[], options: Record<string, un
return true;
}
const commandPathKey = path.join(' ');
if (commandPathKey === 'ktx ingest watch') {
return options.json !== true;
}
@ -263,7 +270,6 @@ async function runBareInteractiveCommand(
mode: 'auto',
agents: false,
agentScope: 'project',
agentInstallMode: 'cli',
skipAgents: false,
inputMode: 'auto',
yes: false,
@ -324,12 +330,6 @@ export async function runCommanderKtxCli(
registerSlCommands(program, context);
profileMark('commander:register-sl');
registerRuntimeCommands(program, context);
profileMark('commander:register-runtime');
registerServeCommands(program, context);
profileMark('commander:register-serve');
registerStatusCommands(program, context);
profileMark('commander:register-status');

View file

@ -11,7 +11,6 @@ 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 { KtxServeArgs } from './serve.js';
import type { KtxSetupArgs } from './setup.js';
import type { KtxSlArgs } from './sl.js';
import { profileMark, profileSpan } from './startup-profile.js';
@ -32,7 +31,6 @@ export interface KtxCliIo {
}
export interface KtxCliDeps {
serveStdio?: (args: KtxServeArgs) => Promise<number>;
setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise<number>;
agent?: (args: KtxAgentArgs, io: KtxCliIo) => Promise<number>;
connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise<number>;

View file

@ -1,52 +0,0 @@
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 { KtxServeArgs } from '../serve.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/serve-commands');
function parseMcp(value: string): 'stdio' {
if (value === 'stdio') {
return 'stdio';
}
throw new InvalidArgumentError('Only stdio is supported in this phase');
}
export function registerServeCommands(program: Command, context: KtxCliCommandContext): void {
program
.command('serve')
.description('Run standalone KTX services such as MCP stdio')
.requiredOption('--mcp <mode>', 'MCP transport mode', parseMcp)
.option('--user-id <id>', 'Local user id', 'local')
.option('--semantic-compute', 'Enable semantic-layer compute', false)
.option('--semantic-compute-url <url>', 'HTTP semantic-layer compute URL')
.option('--yes', 'Install the managed Python runtime without prompting when required', false)
.option('--no-input', 'Disable interactive managed runtime installation')
.option('--database-introspection-url <url>', 'Daemon URL for live-database introspection')
.option('--execute-queries', 'Allow semantic-layer query execution', false)
.option('--memory-capture', 'Enable memory capture', false)
.option('--memory-model <model>', 'Memory capture model')
.showHelpAfterError()
.action(async (options, command): Promise<void> => {
const semanticCompute = options.semanticCompute === true || Boolean(options.semanticComputeUrl);
if (options.executeQueries === true && !semanticCompute) {
throw new Error('--execute-queries requires --semantic-compute');
}
const args: KtxServeArgs = {
mcp: options.mcp,
projectDir: resolveCommandProjectDir(command),
userId: options.userId,
semanticCompute,
semanticComputeUrl: options.semanticComputeUrl,
databaseIntrospectionUrl: options.databaseIntrospectionUrl,
executeQueries: options.executeQueries === true,
memoryCapture: options.memoryCapture === true,
memoryModel: options.memoryModel,
cliVersion: context.packageInfo.version,
runtimeInstallPolicy: runtimeInstallPolicyFromFlags(options),
};
const runner = context.deps.serveStdio ?? (await import('../serve.js')).runKtxServeStdio;
context.setExitCode(await runner(args));
});
}

View file

@ -64,13 +64,6 @@ function agentScope(value: string): 'project' | 'global' {
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function agentInstallMode(value: string): 'cli' | 'mcp' | 'both' {
if (value === 'cli' || value === 'mcp' || value === 'both') {
return value;
}
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function positiveNumber(value: string): number {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
@ -232,9 +225,6 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
.addOption(new Option('--agent-scope <scope>', 'Agent install scope').argParser(agentScope).default('project'))
.option('--project', 'Install agent integration into the project scope', false)
.option('--global', 'Install agent integration into the global target scope', false)
.addOption(
new Option('--agent-install-mode <mode>', 'Agent install mode').argParser(agentInstallMode).default('cli'),
)
.option('--skip-agents', 'Leave agent integration incomplete for now', false)
.option('--yes', 'Accept safe defaults in non-interactive setup', false)
.option('--no-input', 'Disable interactive terminal input')
@ -371,7 +361,6 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
agents: options.agents === true,
...(options.target ? { target: options.target } : {}),
agentScope: resolvedAgentScope,
agentInstallMode: options.agentInstallMode,
skipAgents: options.skipAgents === true,
inputMode: options.input === false ? 'disabled' : 'auto',
yes: options.yes === true,

View file

@ -1,20 +1,46 @@
import type { Command } from '@commander-js/extra-typings';
import type { KtxCliCommandContext } from '../cli-program.js';
import { resolveCommandProjectDir } from '../cli-program.js';
import { resolveCommandProjectDir, resolveCommandProjectDirOverride } from '../cli-program.js';
import { findNearestKtxProjectDir } from '../project-resolver.js';
function outputMode(options: { json?: boolean }): 'plain' | 'json' {
return options.json === true ? 'json' : 'plain';
}
function inputMode(options: { input?: boolean }): { inputMode?: 'disabled' } {
return options.input === false ? { inputMode: 'disabled' } : {};
}
export function registerStatusCommands(program: Command, context: KtxCliCommandContext): void {
program
.command('status')
.description('Show current KTX project setup status')
.description('Check current KTX setup and project readiness')
.option('--json', 'Print JSON output', false)
.action(async (options: { json?: boolean }, command) => {
const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
.option('--no-input', 'Disable interactive terminal input')
.action(async (options: { json?: boolean; input?: boolean }, command) => {
const runner = context.deps.doctor ?? (await import('../doctor.js')).runKtxDoctor;
const explicitOrEnvProjectDir = resolveCommandProjectDirOverride(command);
const nearestProjectDir = explicitOrEnvProjectDir ? undefined : findNearestKtxProjectDir(process.cwd());
if (!explicitOrEnvProjectDir && !nearestProjectDir) {
context.setExitCode(
await runner(
{
command: 'setup',
outputMode: outputMode(options),
...inputMode(options),
},
context.io,
),
);
return;
}
context.setExitCode(
await runner(
{
command: 'status',
command: 'project',
projectDir: resolveCommandProjectDir(command),
json: options.json === true,
outputMode: outputMode(options),
...inputMode(options),
},
context.io,
),

View file

@ -29,10 +29,11 @@ describe('dev Commander tree', () => {
await expect(runKtxCli(['dev', '--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
for (const command of ['init', 'doctor', 'scan', 'ingest', 'mapping']) {
for (const command of ['init', 'runtime', 'scan', 'ingest', 'mapping']) {
expect(testIo.stdout()).toContain(command);
}
for (const removed of [
'doctor',
'knowledge',
'model',
'replay',
@ -100,6 +101,7 @@ describe('dev Commander tree', () => {
it('rejects removed dev command groups', async () => {
for (const argv of [
['dev', 'doctor', 'setup'],
['dev', 'knowledge', 'list'],
['dev', 'model', 'list'],
['dev', 'artifacts'],
@ -114,8 +116,8 @@ describe('dev Commander tree', () => {
it.each([
{
argv: ['dev', 'doctor', '--help'],
expected: ['Usage: ktx dev doctor', '--json', '--no-input'],
argv: ['dev', 'runtime', '--help'],
expected: ['Usage: ktx dev runtime', 'install', 'start', 'stop', 'status', 'doctor', 'prune'],
},
{
argv: ['dev', 'scan', '--help'],

View file

@ -3,8 +3,8 @@ import type { Command } from '@commander-js/extra-typings';
import { type CommandWithGlobalOptions, type KtxCliCommandContext, resolveCommandProjectDir } from './cli-program.js';
import { registerCompletionCommands } from './commands/completion-commands.js';
import { registerConnectionMappingCommands } from './commands/connection-commands.js';
import { registerDoctorCommands } from './commands/doctor-commands.js';
import { registerIngestCommands } from './commands/ingest-commands.js';
import { registerRuntimeCommands } from './commands/runtime-commands.js';
import { registerScanCommands } from './commands/scan-commands.js';
import { profileMark } from './startup-profile.js';
@ -50,7 +50,7 @@ export function registerDevCommands(program: Command, context: KtxCliCommandCont
},
);
registerDoctorCommands(dev, context);
registerRuntimeCommands(dev, context);
registerScanCommands(dev, context);
registerIngestCommands(dev, context, {
runIngestWithProgress: async (ingestArgs, ingestIo, ingestDeps, defaultRunIngest) =>

View file

@ -3,8 +3,6 @@ import { cp, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { promisify } from 'node:util';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
const execFileAsync = promisify(execFile);
@ -50,12 +48,6 @@ async function runBuiltCli(args: string[]): Promise<CliResult> {
}
}
function structuredContent<T extends object>(result: unknown): T {
const content = (result as { structuredContent?: unknown }).structuredContent;
expect(content).toBeDefined();
return content as T;
}
function parseJsonOutput<T>(stdout: string): T {
return JSON.parse(stdout) as T;
}
@ -132,121 +124,4 @@ describe('standalone local warehouse example', () => {
);
}, 30_000);
it('serves local wiki and semantic-layer MCP tools against the copied example project', async () => {
const projectDir = await copyExampleProject(tempDir);
const client = new Client({ name: 'ktx-example-client', version: '0.0.0' });
const transport = new StdioClientTransport({
command: process.execPath,
args: [CLI_BIN, 'serve', '--mcp', 'stdio', '--project-dir', projectDir, '--user-id', 'example-user'],
stderr: 'pipe',
});
try {
await client.connect(transport);
const knowledgeSearch = structuredContent<{
results: Array<{ key: string; summary: string; score: number }>;
totalFound: number;
}>(
await client.callTool({
name: 'knowledge_search',
arguments: { query: 'refund', limit: 5 },
}),
);
expect(knowledgeSearch.totalFound).toBe(1);
expect(knowledgeSearch.results[0]).toMatchObject({
key: 'revenue',
summary: 'Paid order value after refunds',
});
const knowledgeRead = structuredContent<{ key: string; summary: string; content: string; scope: string }>(
await client.callTool({ name: 'knowledge_read', arguments: { key: 'revenue' } }),
);
expect(knowledgeRead).toMatchObject({
key: 'revenue',
summary: 'Paid order value after refunds',
scope: 'GLOBAL',
});
expect(knowledgeRead.content).toContain('Revenue is paid order amount after refund adjustments.');
const knowledgeWrite = structuredContent<{ success: boolean; key: string; action: string }>(
await client.callTool({
name: 'knowledge_write',
arguments: {
key: 'gross_margin',
summary: 'Revenue after direct costs',
content: 'Gross margin subtracts direct order costs from revenue.',
tags: ['finance'],
sl_refs: ['warehouse.orders'],
},
}),
);
expect(knowledgeWrite).toEqual({ success: true, key: 'gross_margin', action: 'created' });
const slList = structuredContent<{
sources: Array<{
connectionId: string;
name: string;
description?: string;
columnCount: number;
measureCount: number;
joinCount: number;
}>;
totalSources: number;
}>(await client.callTool({ name: 'sl_list_sources', arguments: { connectionId: 'warehouse' } }));
expect(slList.totalSources).toBe(1);
expect(slList.sources[0]).toMatchObject({
connectionId: 'warehouse',
name: 'orders',
description: 'Orders placed through the storefront.',
columnCount: 3,
measureCount: 2,
joinCount: 0,
});
const slRead = structuredContent<{ sourceName: string; yaml: string }>(
await client.callTool({
name: 'sl_read_source',
arguments: { connectionId: 'warehouse', sourceName: 'orders' },
}),
);
expect(slRead.sourceName).toBe('orders');
expect(slRead.yaml).toContain('name: orders');
expect(slRead.yaml).toContain('total_revenue');
const slWrite = structuredContent<{ success: boolean; sourceName: string }>(
await client.callTool({
name: 'sl_write_source',
arguments: {
connectionId: 'warehouse',
sourceName: 'customers',
source: {
name: 'customers',
table: 'public.customers',
grain: ['id'],
columns: [{ name: 'id', type: 'number' }],
joins: [],
measures: [],
},
},
}),
);
expect(slWrite).toMatchObject({ success: true, sourceName: 'customers' });
const slValidate = structuredContent<{ success: boolean; errors: string[]; warnings: string[] }>(
await client.callTool({
name: 'sl_validate',
arguments: { connectionId: 'warehouse', names: ['orders', 'customers'] },
}),
);
expect(slValidate.success).toBe(true);
expect(slValidate.errors).toEqual([]);
expect(slValidate.warnings).toContain(
'Local stdio validation checks YAML shape only; Python semantic validation is not configured.',
);
} finally {
await client.close();
}
}, 30_000);
});

View file

@ -143,7 +143,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
status: 'warn',
detail:
'pg_stat_statements ready (PostgreSQL 16.4) with warnings: pg_stat_statements.track is none; set it to top or all in the Postgres parameter group or config; info: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
fix: 'Update the Postgres parameter group or config, then rerun `ktx dev doctor --project-dir /tmp/ktx-project`',
fix: 'Update the Postgres parameter group or config, then rerun `ktx status --project-dir /tmp/ktx-project`',
},
]);
});

View file

@ -63,7 +63,7 @@ function capabilityFailureFix(error: unknown, connectionId: string, projectDir:
if (error instanceof Error && error.name === 'HistoricSqlVersionUnsupportedError') {
return 'Use PostgreSQL 14 or newer, or disable historicSql for this connection';
}
return `Fix connections.${connectionId} Postgres settings, then rerun \`ktx dev doctor --project-dir ${projectDir}\``;
return `Fix connections.${connectionId} Postgres settings, then rerun \`ktx status --project-dir ${projectDir}\``;
}
function failureDetail(error: unknown): string {
@ -143,7 +143,7 @@ export async function runPostgresHistoricSqlDoctorChecks(
checkId(connectionId),
label,
readinessDetail(result),
`Update the Postgres parameter group or config, then rerun \`ktx dev doctor --project-dir ${project.projectDir}\``,
`Update the Postgres parameter group or config, then rerun \`ktx status --project-dir ${project.projectDir}\``,
),
);
} else {

View file

@ -117,16 +117,16 @@ describe('runKtxCli', () => {
expect(testIo.stderr()).toBe('');
});
it('prints the May 6 public command surface in root help', async () => {
it('prints the public command surface in root help', async () => {
const testIo = makeIo();
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'runtime', 'serve', 'status']) {
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']) {
expect(testIo.stdout()).toContain(`${command}`);
}
for (const removed of ['demo', 'init', 'connect', 'scan', 'ask', 'knowledge', 'agent', 'completion']) {
for (const removed of ['demo', 'init', 'connect', 'scan', 'ask', 'knowledge', 'agent', 'completion', 'runtime', 'serve']) {
expect(testIo.stdout()).not.toContain(`${removed} [`);
expect(testIo.stdout()).not.toContain(`${removed} `);
}
@ -150,18 +150,18 @@ describe('runKtxCli', () => {
const pruneIo = makeIo();
await expect(
runKtxCli(['runtime', 'install', '--feature', 'local-embeddings', '--force', '--yes'], installIo.io, {
runKtxCli(['dev', 'runtime', 'install', '--feature', 'local-embeddings', '--force', '--yes'], installIo.io, {
runtime,
}),
).resolves.toBe(0);
await expect(
runKtxCli(['runtime', 'start', '--feature', 'local-embeddings', '--force'], startIo.io, { runtime }),
runKtxCli(['dev', 'runtime', 'start', '--feature', 'local-embeddings', '--force'], startIo.io, { runtime }),
).resolves.toBe(0);
await expect(runKtxCli(['runtime', 'stop'], stopIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['runtime', 'stop', '--all'], stopAllIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['runtime', 'status', '--json'], statusIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['runtime', 'doctor'], doctorIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['runtime', 'prune', '--dry-run'], pruneIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'runtime', 'stop'], stopIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'runtime', 'stop', '--all'], stopAllIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'runtime', 'status', '--json'], statusIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'runtime', 'doctor'], doctorIo.io, { runtime })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'runtime', 'prune', '--dry-run'], pruneIo.io, { runtime })).resolves.toBe(0);
expect(runtime).toHaveBeenNthCalledWith(
1,
@ -229,6 +229,9 @@ describe('runKtxCli', () => {
},
pruneIo.io,
);
for (const io of [installIo, startIo, stopIo, stopAllIo, statusIo, doctorIo, pruneIo]) {
expect(io.stderr()).toBe('');
}
});
it('prints the resolved project directory for ordinary project commands', async () => {
@ -266,7 +269,7 @@ describe('runKtxCli', () => {
it('documents runtime stop all in command help', async () => {
const testIo = makeIo();
await expect(runKtxCli(['runtime', 'stop', '--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'runtime', 'stop', '--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('--all');
expect(testIo.stdout()).toContain('Stop all KTX daemon processes recorded or discoverable');
@ -407,7 +410,6 @@ describe('runKtxCli', () => {
mode: 'auto',
agents: false,
agentScope: 'project',
agentInstallMode: 'cli',
skipAgents: false,
inputMode: 'auto',
yes: false,
@ -653,28 +655,13 @@ describe('runKtxCli', () => {
expect(choiceIo.stderr()).toBe('');
});
it('dispatches serve stdio commands', async () => {
it('rejects removed serve commands', async () => {
const testIo = makeIo();
const serveStdio = vi.fn().mockResolvedValue(0);
await expect(
runKtxCli(['--project-dir', tempDir, 'serve', '--mcp', 'stdio', '--user-id', 'agent'], testIo.io, {
serveStdio,
}),
).resolves.toBe(0);
await expect(runKtxCli(['--project-dir', tempDir, 'serve', '--mcp', 'stdio', '--user-id', 'agent'], testIo.io))
.resolves.toBe(1);
expect(serveStdio).toHaveBeenCalledWith({
mcp: 'stdio',
projectDir: tempDir,
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'prompt',
});
expect(testIo.stderr()).toMatch(/unknown command|error:/);
});
it('routes public ingest through the public ingest parser', async () => {
@ -948,16 +935,14 @@ describe('runKtxCli', () => {
);
});
it('dispatches dev doctor and ingest parser cases through Commander', async () => {
it('rejects removed dev doctor while keeping ingest parser cases under dev', async () => {
const doctor = vi.fn(async () => 0);
const ingest = vi.fn(async () => 0);
const doctorIo = makeIo();
const ingestRunIo = makeIo();
const ingestReplayHelpIo = makeIo();
await expect(runKtxCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(
0,
);
await expect(runKtxCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(1);
await expect(
runKtxCli(
[
@ -983,7 +968,7 @@ describe('runKtxCli', () => {
).resolves.toBe(0);
await expect(runKtxCli(['dev', 'ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0);
expect(doctor).toHaveBeenCalledWith({ command: 'setup', outputMode: 'json', inputMode: 'disabled' }, doctorIo.io);
expect(doctor).not.toHaveBeenCalled();
expect(ingest).toHaveBeenCalledWith(
{
command: 'run',
@ -1002,7 +987,7 @@ describe('runKtxCli', () => {
);
expect(ingestReplayHelpIo.stdout()).toContain('Usage: ktx dev ingest replay [options] <runId>');
expect(ingestReplayHelpIo.stdout()).toContain('<runId>');
expect(doctorIo.stderr()).toBe('');
expect(doctorIo.stderr()).toMatch(/unknown command|error:/);
expect(ingestRunIo.stderr()).toBe('');
expect(ingestReplayHelpIo.stderr()).toBe('');
});
@ -1082,18 +1067,58 @@ describe('runKtxCli', () => {
expect(testIo.stderr()).toBe(`Project: ${tempDir}\n`);
});
it('dispatches setup status and top-level status through the setup runner', async () => {
it('keeps setup status on the setup runner and routes top-level status through doctor', async () => {
const setup = vi.fn(async () => 0);
const doctor = vi.fn(async () => 0);
const setupIo = makeIo();
const statusIo = makeIo();
await expect(
runKtxCli(['--project-dir', tempDir, 'setup', 'status', '--json'], setupIo.io, { setup }),
).resolves.toBe(0);
await expect(runKtxCli(['--project-dir', tempDir, 'status', '--json'], statusIo.io, { setup })).resolves.toBe(0);
await expect(
runKtxCli(['--project-dir', tempDir, 'status', '--json', '--no-input'], statusIo.io, { setup, doctor }),
).resolves.toBe(0);
expect(setup).toHaveBeenNthCalledWith(1, { command: 'status', projectDir: tempDir, json: true }, setupIo.io);
expect(setup).toHaveBeenNthCalledWith(2, { command: 'status', projectDir: tempDir, json: true }, statusIo.io);
expect(setup).toHaveBeenCalledTimes(1);
expect(doctor).toHaveBeenCalledWith(
{ command: 'project', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
statusIo.io,
);
expect(statusIo.stderr()).toBe('');
});
it('routes top-level status without a project to setup doctor checks', async () => {
const { mkdtemp, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const originalCwd = process.cwd();
const previousProjectDir = process.env.KTX_PROJECT_DIR;
const tempCwd = await mkdtemp(join(tmpdir(), 'ktx-status-no-project-'));
const doctor = vi.fn(async () => 0);
const statusIo = makeIo();
try {
delete process.env.KTX_PROJECT_DIR;
process.chdir(tempCwd);
await expect(runKtxCli(['status', '--json', '--no-input'], statusIo.io, { doctor })).resolves.toBe(0);
expect(doctor).toHaveBeenCalledWith(
{ command: 'setup', outputMode: 'json', inputMode: 'disabled' },
statusIo.io,
);
expect(statusIo.stderr()).toBe('');
} finally {
process.chdir(originalCwd);
if (previousProjectDir === undefined) {
delete process.env.KTX_PROJECT_DIR;
} else {
process.env.KTX_PROJECT_DIR = previousProjectDir;
}
await rm(tempCwd, { recursive: true, force: true });
}
});
it('dispatches setup context recovery commands through the setup runner', async () => {
@ -1356,8 +1381,6 @@ describe('runKtxCli', () => {
'--target',
'codex',
'--project',
'--agent-install-mode',
'both',
'--no-input',
'--yes',
],
@ -1376,7 +1399,6 @@ describe('runKtxCli', () => {
agents: true,
target: 'codex',
agentScope: 'project',
agentInstallMode: 'both',
inputMode: 'disabled',
yes: true,
}),
@ -2241,9 +2263,8 @@ describe('runKtxCli', () => {
expect(conflictIo.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
});
it('dispatches serve public command options through Commander', async () => {
it('rejects removed public serve command options before dispatch', async () => {
const serveIo = makeIo();
const serveStdio = vi.fn(async () => 0);
await expect(
runKtxCli(
@ -2261,77 +2282,10 @@ describe('runKtxCli', () => {
'openai/gpt-5.2',
],
serveIo.io,
{ serveStdio },
),
).resolves.toBe(0);
expect(serveStdio).toHaveBeenCalledWith({
mcp: 'stdio',
projectDir: tempDir,
userId: 'local',
semanticCompute: true,
semanticComputeUrl: 'http://127.0.0.1:18080',
databaseIntrospectionUrl: undefined,
executeQueries: true,
memoryCapture: true,
memoryModel: 'openai/gpt-5.2',
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'prompt',
});
expect(serveIo.stderr()).toBe('');
});
it('routes serve managed runtime install policies', async () => {
const autoIo = makeIo();
const neverIo = makeIo();
const conflictIo = makeIo();
const serveStdio = vi.fn(async () => 0);
await expect(
runKtxCli(['serve', '--mcp', 'stdio', '--project-dir', tempDir, '--semantic-compute', '--yes'], autoIo.io, {
serveStdio,
}),
).resolves.toBe(0);
await expect(
runKtxCli(['serve', '--mcp', 'stdio', '--project-dir', tempDir, '--semantic-compute', '--no-input'], neverIo.io, {
serveStdio,
}),
).resolves.toBe(0);
await expect(
runKtxCli(
['serve', '--mcp', 'stdio', '--project-dir', tempDir, '--semantic-compute', '--yes', '--no-input'],
conflictIo.io,
{ serveStdio },
),
).resolves.toBe(1);
expect(serveStdio).toHaveBeenNthCalledWith(1, {
mcp: 'stdio',
projectDir: tempDir,
userId: 'local',
semanticCompute: true,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'auto',
});
expect(serveStdio).toHaveBeenNthCalledWith(2, {
mcp: 'stdio',
projectDir: tempDir,
userId: 'local',
semanticCompute: true,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'never',
});
expect(conflictIo.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
expect(serveIo.stderr()).toMatch(/unknown command|error:/);
});
it('prints dev help for bare dev commands', async () => {

View file

@ -118,7 +118,6 @@ describe('runKtxIngest', () => {
mode: 'new',
agents: false,
agentScope: 'project',
agentInstallMode: 'cli',
skipAgents: true,
inputMode: 'disabled',
yes: true,

View file

@ -108,9 +108,9 @@ function installResult(features: KtxRuntimeFeature[] = ['core']): ManagedPythonR
describe('managedRuntimeInstallCommand', () => {
it('prints the exact command for each managed runtime feature', () => {
expect(managedRuntimeInstallCommand('core')).toBe('ktx runtime install --yes');
expect(managedRuntimeInstallCommand('core')).toBe('ktx dev runtime install --yes');
expect(managedRuntimeInstallCommand('local-embeddings')).toBe(
'ktx runtime install --feature local-embeddings --yes',
'ktx dev runtime install --feature local-embeddings --yes',
);
});
});
@ -166,7 +166,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
readStatus: vi.fn(async () => missingStatus()),
installRuntime,
}),
).rejects.toThrow('KTX Python runtime is required for this command. Run: ktx runtime install --yes');
).rejects.toThrow('KTX Python runtime is required for this command. Run: ktx dev runtime install --yes');
expect(installRuntime).not.toHaveBeenCalled();
});

View file

@ -52,8 +52,8 @@ export interface ManagedPythonSemanticLayerComputeOptions extends ManagedPythonC
export function managedRuntimeInstallCommand(feature: KtxRuntimeFeature): string {
return feature === 'local-embeddings'
? 'ktx runtime install --feature local-embeddings --yes'
: 'ktx runtime install --yes';
? 'ktx dev runtime install --feature local-embeddings --yes'
: 'ktx dev runtime install --yes';
}
function installPrompt(feature: KtxRuntimeFeature): string {

View file

@ -446,7 +446,7 @@ describe('doctorManagedPythonRuntime', () => {
['asset', 'pass'],
['runtime', 'fail'],
]);
expect(checks[2]?.fix).toBe('Run: ktx runtime install --yes');
expect(checks[2]?.fix).toBe('Run: ktx dev runtime install --yes');
});
it('reports uv as a hard prerequisite when uv is missing', async () => {
@ -467,7 +467,7 @@ describe('doctorManagedPythonRuntime', () => {
label: 'uv',
status: 'fail',
detail: MISSING_UV_RUNTIME_INSTALL_MESSAGE,
fix: 'Install uv, make sure it is on PATH, and run: ktx runtime install --yes',
fix: 'Install uv, make sure it is on PATH, and run: ktx dev runtime install --yes',
});
});
});

View file

@ -115,7 +115,7 @@ export interface ManagedPythonRuntimePruneResult {
}
export const MISSING_UV_RUNTIME_INSTALL_MESSAGE =
'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx runtime install --yes';
'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx dev runtime install --yes';
function defaultAssetDir(): string {
return fileURLToPath(new URL('../assets/python/', import.meta.url));
@ -411,7 +411,7 @@ export async function doctorManagedPythonRuntime(
id: 'uv',
label: 'uv',
detail: error instanceof Error ? error.message : String(error),
fix: 'Install uv, make sure it is on PATH, and run: ktx runtime install --yes',
fix: 'Install uv, make sure it is on PATH, and run: ktx dev runtime install --yes',
}),
);
}
@ -436,7 +436,7 @@ export async function doctorManagedPythonRuntime(
id: 'runtime',
label: 'Managed Python runtime',
detail: status.detail,
...(status.kind === 'ready' ? {} : { fix: 'Run: ktx runtime install --yes' }),
...(status.kind === 'ready' ? {} : { fix: 'Run: ktx dev runtime install --yes' }),
}),
);
return checks;

View file

@ -44,22 +44,15 @@ describe('KTX demo next steps', () => {
command: 'ktx wiki list',
description: 'Inspect generated wiki pages',
},
{
command: 'ktx serve --mcp stdio --user-id local',
description: 'Optional MCP server route for clients that require MCP',
},
]);
});
it('prefers the direct CLI route before MCP serving', () => {
it('uses only the direct CLI route for agent verification', () => {
const commands = KTX_NEXT_STEP_COMMANDS.map((step) => step.command);
expect(commands.indexOf('ktx agent context --json')).toBeLessThan(
commands.indexOf('ktx serve --mcp stdio --user-id local'),
);
expect(commands.indexOf('ktx agent tools --json')).toBeLessThan(
commands.indexOf('ktx serve --mcp stdio --user-id local'),
);
expect(commands).toContain('ktx agent context --json');
expect(commands).toContain('ktx agent tools --json');
expect(commands).not.toContain('ktx serve --mcp stdio --user-id local');
});
it('explains what the next-step commands are for', () => {

View file

@ -32,14 +32,7 @@ export const KTX_NEXT_STEP_DIRECT_COMMANDS = [
},
] as const;
export const KTX_NEXT_STEP_MCP_COMMANDS = [
{
command: 'ktx serve --mcp stdio --user-id local',
description: 'Optional MCP server route for clients that require MCP',
},
] as const;
export const KTX_NEXT_STEP_COMMANDS = [...KTX_NEXT_STEP_DIRECT_COMMANDS, ...KTX_NEXT_STEP_MCP_COMMANDS] as const;
export const KTX_NEXT_STEP_COMMANDS = [...KTX_NEXT_STEP_DIRECT_COMMANDS] as const;
export const KTX_NEXT_STEP_COMMAND_WIDTH = Math.max(
...[...KTX_CONTEXT_BUILD_COMMANDS, ...KTX_NEXT_STEP_COMMANDS].map((step) => step.command.length),

View file

@ -36,72 +36,56 @@ describe('project directory defaults', () => {
const ingest = vi.fn(async () => 0);
const publicIngest = vi.fn(async () => 0);
const scan = vi.fn(async () => 0);
const serveStdio = vi.fn(async () => 0);
const setup = vi.fn(async () => 0);
const agent = vi.fn(async () => 0);
const deps: KtxCliDeps = { agent, connection, demo, doctor, ingest, publicIngest, scan, serveStdio, setup };
const deps: KtxCliDeps = { agent, connection, demo, doctor, ingest, publicIngest, scan, setup };
const cases: Array<{
argv: string[];
spy: ReturnType<typeof vi.fn>;
expected: Record<string, unknown>;
runnerType: 'cli' | 'serve';
expectedStderr: string;
}> = [
{
argv: ['connection', 'list'],
spy: connection,
expected: { command: 'list', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
expectedStderr: 'Project: /tmp/ktx-env-project\n',
},
{
argv: ['setup', 'demo', 'scan', '--no-input'],
spy: demo,
expected: { command: 'scan', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
expectedStderr: 'Project: /tmp/ktx-env-project\n',
},
{
argv: ['dev', 'doctor', '--no-input'],
argv: ['status', '--no-input'],
spy: doctor,
expected: { command: 'project', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
expectedStderr: 'Project: /tmp/ktx-env-project\n',
},
{
argv: ['ingest', 'status', 'run-1'],
spy: publicIngest,
expected: { command: 'status', projectDir: '/tmp/ktx-env-project', runId: 'run-1' },
runnerType: 'cli',
expectedStderr: 'Project: /tmp/ktx-env-project\n',
},
{
argv: ['setup', 'status'],
spy: setup,
expected: { command: 'status', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
expectedStderr: 'Project: /tmp/ktx-env-project\n',
},
{
argv: ['dev', 'scan', 'warehouse'],
spy: scan,
expected: { command: 'run', projectDir: '/tmp/ktx-env-project', connectionId: 'warehouse' },
runnerType: 'cli',
expectedStderr: 'Project: /tmp/ktx-env-project\n',
},
{
argv: ['serve', '--mcp', 'stdio'],
spy: serveStdio,
expected: { mcp: 'stdio', projectDir: '/tmp/ktx-env-project' },
runnerType: 'serve',
expectedStderr: '',
},
{
argv: ['agent', 'tools', '--json'],
spy: agent,
expected: { command: 'tools', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
expectedStderr: '',
},
];
@ -109,11 +93,7 @@ describe('project directory defaults', () => {
for (const item of cases) {
const testIo = makeIo();
await expect(runKtxCli(item.argv, testIo.io, deps)).resolves.toBe(0);
if (item.runnerType === 'serve') {
expect(item.spy).toHaveBeenLastCalledWith(expect.objectContaining(item.expected));
} else {
expect(item.spy).toHaveBeenLastCalledWith(expect.objectContaining(item.expected), testIo.io);
}
expect(item.spy).toHaveBeenLastCalledWith(expect.objectContaining(item.expected), testIo.io);
expect(testIo.stderr()).toBe(item.expectedStderr);
}
});

View file

@ -300,7 +300,7 @@ describe('runKtxRuntime', () => {
label: 'Managed Python runtime',
status: 'fail',
detail: 'No runtime manifest',
fix: 'Run: ktx runtime install --yes',
fix: 'Run: ktx dev runtime install --yes',
},
]),
};
@ -309,7 +309,7 @@ describe('runKtxRuntime', () => {
expect(io.stdout()).toContain('PASS uv: uv 0.9.5');
expect(io.stdout()).toContain('FAIL Managed Python runtime: No runtime manifest');
expect(io.stdout()).toContain('Fix: Run: ktx runtime install --yes');
expect(io.stdout()).toContain('Fix: Run: ktx dev runtime install --yes');
});
it('requires --yes before pruning stale runtime directories', async () => {

View file

@ -229,7 +229,7 @@ export async function runKtxRuntime(
return checks.some((check) => check.status === 'fail') ? 1 : 0;
}
if (!args.dryRun && !args.yes) {
io.stderr.write('Refusing to prune without --yes. Preview with: ktx runtime prune --dry-run\n');
io.stderr.write('Refusing to prune without --yes. Preview with: ktx dev runtime prune --dry-run\n');
return 1;
}
const status = await (deps.readStatus ?? readManagedPythonRuntimeStatus)({ cliVersion: args.cliVersion });

View file

@ -1,551 +0,0 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { SourceAdapter } from '@ktx/context/ingest';
import { initKtxProject } from '@ktx/context/project';
import { describe, expect, it, vi } from 'vitest';
import { runKtxServeStdio } from './serve.js';
function makeManagedRuntimeIo() {
let stdout = '';
let stderr = '';
return {
io: {
stdout: { write: (chunk: string) => (stdout += chunk) },
stderr: { write: (chunk: string) => (stderr += chunk) },
},
stdout: () => stdout,
stderr: () => stderr,
};
}
describe('runKtxServeStdio', () => {
it('loads the project, creates local ports, and connects the server to stdio', async () => {
const connect = vi.fn().mockResolvedValue(undefined);
const project = {
projectDir: '/tmp/ktx-project',
config: {
connections: {},
llm: {
provider: { backend: 'gateway' },
models: { default: 'anthropic/claude-sonnet' },
},
},
} as never;
const loadProject = vi.fn().mockResolvedValue(project);
const contextTools = { connections: { list: vi.fn() } };
const createContextTools = vi.fn().mockReturnValue(contextTools);
const createServer = vi.fn().mockReturnValue({ connect });
const createTransport = vi.fn().mockReturnValue({ kind: 'stdio' });
let stderr = '';
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
},
{
loadProject,
createContextTools,
createServer,
createTransport,
stderr: { write: (chunk: string) => (stderr += chunk) },
},
),
).resolves.toBe(0);
expect(loadProject).toHaveBeenCalledWith({ projectDir: '/tmp/ktx-project' });
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
localIngest: expect.objectContaining({
adapters: expect.any(Array),
}),
localScan: expect.objectContaining({
adapters: expect.any(Array),
}),
}),
);
expect(createServer).toHaveBeenCalledWith({
name: 'ktx',
version: '0.0.0-private',
userContext: { userId: 'agent' },
contextTools,
memoryCapture: undefined,
});
expect(connect).toHaveBeenCalledWith({ kind: 'stdio' });
expect(stderr).toContain('ktx MCP server running on stdio for /tmp/ktx-project');
});
it('enables local ingest ports by default when serving stdio', async () => {
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const connect = vi.fn().mockResolvedValue(undefined);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
},
{
loadProject: async () => project,
createContextTools,
createServer: vi.fn(() => ({ connect }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
localIngest: expect.objectContaining({
adapters: expect.any(Array),
}),
localScan: expect.objectContaining({
adapters: expect.any(Array),
}),
}),
);
});
it('passes daemon database introspection URL to MCP local ingest adapters', async () => {
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const connect = vi.fn().mockResolvedValue(undefined);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
const createdAdapters: SourceAdapter[] = [];
const createIngestAdapters = vi.fn(() => createdAdapters);
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: 'http://127.0.0.1:8765',
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
},
{
loadProject: async () => project,
createContextTools,
createIngestAdapters,
createServer: vi.fn(() => ({ connect }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
localIngest: expect.objectContaining({
adapters: expect.any(Array),
pullConfigOptions: {
databaseIntrospectionUrl: 'http://127.0.0.1:8765',
},
}),
localScan: expect.objectContaining({
adapters: createdAdapters,
databaseIntrospectionUrl: 'http://127.0.0.1:8765',
}),
}),
);
expect(createIngestAdapters).toHaveBeenCalledWith(project, {
databaseIntrospectionUrl: 'http://127.0.0.1:8765',
});
});
it('passes managed daemon options to MCP local ingest adapters and pull-config options', async () => {
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const adapters: SourceAdapter[] = [
{ source: 'looker', skillNames: [], detect: async () => true, chunk: async () => ({ workUnits: [] }) },
];
const createIngestAdapters = vi.fn(() => adapters);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
const managedRuntimeIo = makeManagedRuntimeIo();
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
cliVersion: '0.2.0',
runtimeInstallPolicy: 'auto',
},
{
loadProject: async () => project,
createContextTools,
createIngestAdapters,
managedRuntimeIo: managedRuntimeIo.io,
createServer: vi.fn(() => ({ connect: vi.fn(async () => undefined) }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
const expectedManagedDaemon = {
cliVersion: '0.2.0',
installPolicy: 'auto',
io: managedRuntimeIo.io,
};
expect(createIngestAdapters).toHaveBeenCalledWith(project, {
managedDaemon: expectedManagedDaemon,
});
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
localIngest: expect.objectContaining({
adapters,
pullConfigOptions: {
managedDaemon: expectedManagedDaemon,
},
}),
}),
);
});
it('uses CLI-native local ingest adapters for standalone scan tools', async () => {
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const createContextTools = vi.fn(() => ({}) as never);
await runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'local',
semanticCompute: false,
executeQueries: false,
memoryCapture: false,
},
{
loadProject: vi.fn(async () => project),
createContextTools,
createServer: vi.fn(() => ({ connect: vi.fn(async () => undefined) }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
);
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
localIngest: expect.objectContaining({ adapters: expect.any(Array) }),
localScan: expect.objectContaining({ adapters: expect.any(Array) }),
}),
);
});
it('passes semantic compute to local project ports when enabled', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-serve-'));
try {
const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: project.projectDir,
userId: 'local',
semanticCompute: true,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
},
{
loadProject: async () => project,
createContextTools,
createSemanticLayerCompute: () => semanticLayerCompute,
createServer: vi.fn(() => ({ connect: vi.fn(async () => undefined) }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
semanticLayerCompute,
localIngest: expect.objectContaining({
adapters: expect.any(Array),
semanticLayerCompute,
}),
localScan: expect.objectContaining({
adapters: expect.any(Array),
}),
}),
);
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});
it('uses managed semantic compute when MCP semantic compute has no explicit HTTP URL', async () => {
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
const createManagedSemanticLayerCompute = vi.fn(async () => semanticLayerCompute);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
const managedRuntimeIo = makeManagedRuntimeIo();
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: true,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
cliVersion: '0.2.0',
runtimeInstallPolicy: 'auto',
},
{
loadProject: async () => project,
createContextTools,
createManagedSemanticLayerCompute,
managedRuntimeIo: managedRuntimeIo.io,
createServer: vi.fn(() => ({ connect: vi.fn(async () => undefined) }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
expect(createManagedSemanticLayerCompute).toHaveBeenCalledWith({
cliVersion: '0.2.0',
installPolicy: 'auto',
io: managedRuntimeIo.io,
});
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
semanticLayerCompute,
}),
);
});
it('uses the HTTP semantic compute port when a daemon URL is provided', async () => {
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
const createHttpSemanticLayerCompute = vi.fn(() => semanticLayerCompute);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: true,
semanticComputeUrl: 'http://127.0.0.1:8765',
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: false,
memoryModel: undefined,
},
{
loadProject: async () => project,
createContextTools,
createHttpSemanticLayerCompute,
createServer: vi.fn(() => ({ connect: vi.fn(async () => undefined) }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
expect(createHttpSemanticLayerCompute).toHaveBeenCalledWith('http://127.0.0.1:8765');
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
semanticLayerCompute,
}),
);
});
it('passes a query executor to local project ports only when query execution is enabled', async () => {
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const connect = vi.fn().mockResolvedValue(undefined);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
const queryExecutor = { execute: vi.fn() };
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: true,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: true,
memoryCapture: false,
memoryModel: undefined,
},
{
loadProject: async () => project,
createContextTools,
createSemanticLayerCompute: () => semanticLayerCompute,
createQueryExecutor: () => queryExecutor,
createServer: vi.fn(() => ({ connect }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
semanticLayerCompute,
queryExecutor,
localIngest: expect.objectContaining({
adapters: expect.any(Array),
semanticLayerCompute,
queryExecutor,
}),
localScan: expect.objectContaining({
adapters: expect.any(Array),
}),
}),
);
});
it('creates a local memory capture port when memory capture is enabled', async () => {
const project = {
projectDir: '/tmp/ktx-project',
config: {
connections: {},
llm: {
provider: { backend: 'gateway' },
models: { default: 'anthropic/claude-sonnet' },
},
},
} as never;
const connect = vi.fn().mockResolvedValue(undefined);
const contextTools = { connections: { list: vi.fn() } };
const memoryCapture = { capture: vi.fn(), status: vi.fn() };
const createContextTools = vi.fn().mockReturnValue(contextTools);
const createMemoryCapture = vi.fn().mockReturnValue(memoryCapture);
const createServer = vi.fn().mockReturnValue({ connect });
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: true,
memoryModel: 'anthropic/claude-sonnet',
},
{
loadProject: async () => project,
createContextTools,
createMemoryCapture,
createServer,
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
expect(createMemoryCapture).toHaveBeenCalledWith(project, {
llmProvider: expect.objectContaining({ getModel: expect.any(Function) }),
semanticLayerCompute: undefined,
});
expect(createServer).toHaveBeenCalledWith({
name: 'ktx',
version: '0.0.0-private',
userContext: { userId: 'agent' },
contextTools,
memoryCapture,
});
});
it('reuses semantic compute for local memory capture when enabled', async () => {
const project = {
projectDir: '/tmp/ktx-project',
config: {
connections: {},
llm: {
provider: { backend: 'gateway' },
models: { default: 'openai/gpt' },
},
},
} as never;
const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
const createMemoryCapture = vi.fn().mockReturnValue({ capture: vi.fn(), status: vi.fn() });
await expect(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: true,
semanticComputeUrl: undefined,
databaseIntrospectionUrl: undefined,
executeQueries: false,
memoryCapture: true,
memoryModel: 'openai/gpt',
},
{
loadProject: async () => project,
createContextTools: vi.fn(() => ({ connections: { list: async () => [] } })),
createSemanticLayerCompute: () => semanticLayerCompute,
createMemoryCapture,
createServer: vi.fn(() => ({ connect: vi.fn(async () => undefined) }) as never),
createTransport: vi.fn(() => ({}) as never),
stderr: { write: vi.fn() },
},
),
).resolves.toBe(0);
expect(createMemoryCapture).toHaveBeenCalledWith(project, {
llmProvider: expect.objectContaining({ getModel: expect.any(Function) }),
semanticLayerCompute,
});
});
});

View file

@ -1,171 +0,0 @@
import { createLocalKtxLlmProviderFromConfig } from '@ktx/context';
import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
import {
createHttpSemanticLayerComputePort,
type KtxSemanticLayerComputePort,
} from '@ktx/context/daemon';
import { createDefaultLocalIngestAdapters, type LocalIngestMcpOptions } from '@ktx/context/ingest';
import {
createDefaultKtxMcpServer,
createLocalProjectMcpContextPorts,
type KtxMcpContextPorts,
} from '@ktx/context/mcp';
import { createLocalProjectMemoryCapture, type MemoryCaptureService } from '@ktx/context/memory';
import { type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
import type { LocalScanMcpOptions } from '@ktx/context/scan';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import type { KtxCliIo } from './cli-runtime.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js';
import {
createManagedPythonSemanticLayerComputePort,
type KtxManagedPythonInstallPolicy,
} from './managed-python-command.js';
import type { ManagedPythonCoreDaemonOptions } from './managed-python-http.js';
import { profileMark } from './startup-profile.js';
profileMark('module:serve');
export interface KtxServeArgs {
mcp: 'stdio';
projectDir: string;
userId: string;
semanticCompute: boolean;
semanticComputeUrl?: string;
databaseIntrospectionUrl?: string;
executeQueries: boolean;
memoryCapture: boolean;
memoryModel?: string;
cliVersion?: string;
runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
}
interface KtxServeIo {
stderr: { write(chunk: string): void };
}
interface LocalProjectContextToolOptions {
semanticLayerCompute?: KtxSemanticLayerComputePort;
queryExecutor?: KtxSqlQueryExecutorPort;
localIngest?: LocalIngestMcpOptions;
localScan?: LocalScanMcpOptions;
}
interface KtxServeDeps {
loadProject?: typeof loadKtxProject;
createContextTools?: (project: KtxLocalProject, options?: LocalProjectContextToolOptions) => KtxMcpContextPorts;
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
createManagedSemanticLayerCompute?: typeof createManagedPythonSemanticLayerComputePort;
managedRuntimeIo?: KtxCliIo;
createHttpSemanticLayerCompute?: (baseUrl: string) => KtxSemanticLayerComputePort;
createIngestAdapters?: typeof createKtxCliLocalIngestAdapters;
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
createMemoryCapture?: typeof createLocalProjectMemoryCapture;
createServer?: typeof createDefaultKtxMcpServer;
createTransport?: () => StdioServerTransport;
stderr?: KtxServeIo['stderr'];
}
function requiredManagedRuntimeCliVersion(args: KtxServeArgs): string {
if (!args.cliVersion) {
throw new Error('Managed Python semantic compute requires a CLI version.');
}
return args.cliVersion;
}
function managedDaemonOptionsForServe(
args: KtxServeArgs,
deps: KtxServeDeps,
): ManagedPythonCoreDaemonOptions | undefined {
if (args.databaseIntrospectionUrl || !args.cliVersion) {
return undefined;
}
return {
cliVersion: args.cliVersion,
installPolicy: args.runtimeInstallPolicy ?? 'prompt',
io: deps.managedRuntimeIo ?? process,
};
}
async function createServeSemanticLayerCompute(
args: KtxServeArgs,
deps: KtxServeDeps,
): Promise<KtxSemanticLayerComputePort | undefined> {
if (!args.semanticCompute) {
return undefined;
}
if (args.semanticComputeUrl) {
return (deps.createHttpSemanticLayerCompute ?? ((baseUrl) => createHttpSemanticLayerComputePort({ baseUrl })))(
args.semanticComputeUrl,
);
}
if (deps.createSemanticLayerCompute) {
return deps.createSemanticLayerCompute();
}
const createManagedSemanticLayerCompute =
deps.createManagedSemanticLayerCompute ?? createManagedPythonSemanticLayerComputePort;
return createManagedSemanticLayerCompute({
cliVersion: requiredManagedRuntimeCliVersion(args),
installPolicy: args.runtimeInstallPolicy ?? 'prompt',
io: deps.managedRuntimeIo ?? process,
});
}
export async function runKtxServeStdio(args: KtxServeArgs, deps: KtxServeDeps = {}): Promise<number> {
const loadProjectFn = deps.loadProject ?? loadKtxProject;
const createContextToolsFn = deps.createContextTools ?? createLocalProjectMcpContextPorts;
const createServerFn = deps.createServer ?? createDefaultKtxMcpServer;
const createTransportFn = deps.createTransport ?? (() => new StdioServerTransport());
const stderr = deps.stderr ?? process.stderr;
const project = await loadProjectFn({ projectDir: args.projectDir });
const semanticLayerCompute = await createServeSemanticLayerCompute(args, deps);
const queryExecutor = args.executeQueries
? (deps.createQueryExecutor ?? createDefaultLocalQueryExecutor)()
: undefined;
const createIngestAdapters = deps.createIngestAdapters ?? createKtxCliLocalIngestAdapters;
const managedDaemon = managedDaemonOptionsForServe(args, deps);
const localAdapterOptions = {
...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}),
...(managedDaemon ? { managedDaemon } : {}),
};
const localAdapters = createIngestAdapters(project, localAdapterOptions);
const llmProvider = args.memoryCapture
? (createLocalKtxLlmProviderFromConfig(project.config.llm) ?? undefined)
: undefined;
const memoryCapture: MemoryCaptureService | undefined = args.memoryCapture
? (deps.createMemoryCapture ?? createLocalProjectMemoryCapture)(project, {
llmProvider,
semanticLayerCompute,
})
: undefined;
const localIngest: LocalIngestMcpOptions = {
adapters: localAdapters,
pullConfigOptions: localAdapterOptions,
...(semanticLayerCompute ? { semanticLayerCompute } : {}),
...(queryExecutor ? { queryExecutor } : {}),
};
const localScan: LocalScanMcpOptions = {
adapters: localAdapters,
databaseIntrospectionUrl: args.databaseIntrospectionUrl,
createConnector: (connectionId) => createKtxCliScanConnector(project, connectionId),
};
const contextToolOptions: LocalProjectContextToolOptions = {
localIngest,
localScan,
...(semanticLayerCompute ? { semanticLayerCompute } : {}),
...(queryExecutor ? { queryExecutor } : {}),
};
const contextTools = createContextToolsFn(project, contextToolOptions);
const server = createServerFn({
name: 'ktx',
version: '0.0.0-private',
userContext: { userId: args.userId },
contextTools,
memoryCapture,
});
const transport = createTransportFn();
await server.connect(transport);
stderr.write(`ktx MCP server running on stdio for ${project.projectDir}\n`);
return 0;
}

View file

@ -36,25 +36,23 @@ describe('setup agents', () => {
await rm(tempDir, { recursive: true, force: true });
});
it('plans project-scoped CLI and MCP files for every target', () => {
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-code', scope: 'project', mode: 'both' })).toEqual([
it('plans project-scoped CLI files for every target', () => {
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-code', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.claude/skills/ktx/SKILL.md'), role: 'skill' },
{ kind: 'file', path: join(tempDir, '.claude/rules/ktx.md'), role: 'rule' },
{ kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
]);
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'codex', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.agents/skills/ktx/SKILL.md'), role: 'skill' },
{ kind: 'file', path: join(tempDir, '.codex/instructions/ktx.md'), role: 'rule' },
]);
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'cursor', scope: 'project', mode: 'mcp' })).toEqual([
{ kind: 'json-key', path: join(tempDir, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'cursor', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.cursor/rules/ktx.mdc') },
]);
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'opencode', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.opencode/commands/ktx.md') },
]);
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'universal', scope: 'project', mode: 'both' })).toEqual([
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'universal', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.agents/skills/ktx/SKILL.md') },
{ kind: 'json-key', path: join(tempDir, '.agents/mcp/ktx.json'), jsonPath: ['mcpServers', 'ktx'] },
]);
});
@ -70,7 +68,7 @@ describe('setup agents', () => {
agents: true,
target: 'universal',
scope: 'project',
mode: 'both',
mode: 'cli',
skipAgents: false,
},
io.io,
@ -78,11 +76,10 @@ describe('setup agents', () => {
).resolves.toEqual({
status: 'ready',
projectDir: tempDir,
installs: [{ target: 'universal', scope: 'project', mode: 'both' }],
installs: [{ target: 'universal', scope: 'project', mode: 'cli' }],
});
await expect(stat(join(tempDir, '.agents/skills/ktx/SKILL.md'))).resolves.toBeDefined();
await expect(stat(join(tempDir, '.agents/mcp/ktx.json'))).resolves.toBeDefined();
const skill = await readFile(join(tempDir, '.agents/skills/ktx/SKILL.md'), 'utf-8');
expect(skill).toContain(`--project-dir ${tempDir}`);
expect(skill).toContain('must not print secrets');
@ -90,13 +87,13 @@ describe('setup agents', () => {
expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
version: 1,
projectDir: tempDir,
installs: [{ target: 'universal', scope: 'project', mode: 'both' }],
installs: [{ target: 'universal', scope: 'project', mode: 'cli' }],
});
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain('agents');
expect(io.stderr()).toBe('');
});
it('writes PATH-independent launcher commands for skills and MCP configs', async () => {
it('writes PATH-independent launcher commands for skills', async () => {
const io = makeIo();
await expect(
@ -108,7 +105,7 @@ describe('setup agents', () => {
agents: true,
target: 'universal',
scope: 'project',
mode: 'both',
mode: 'cli',
skipAgents: false,
},
io.io,
@ -119,37 +116,21 @@ describe('setup agents', () => {
expect(skill).not.toContain('`ktx agent');
expect(skill).toContain('agent context --json');
expect(skill).toContain('agent sql execute');
const mcp = JSON.parse(await readFile(join(tempDir, '.agents/mcp/ktx.json'), 'utf-8')) as {
mcpServers?: { ktx?: { command?: string; args?: string[] } };
};
expect(mcp.mcpServers?.ktx?.command).toBe(process.execPath);
expect(mcp.mcpServers?.ktx?.args?.[0]).toMatch(/packages\/cli\/(src|dist)\/bin\.(ts|js)$/);
expect(mcp.mcpServers?.ktx?.args).toEqual([
expect.stringMatching(/packages\/cli\/(src|dist)\/bin\.(ts|js)$/),
'--project-dir',
tempDir,
'serve',
'--mcp',
'stdio',
'--semantic-compute',
'--execute-queries',
]);
});
it('removes only manifest-listed files and JSON keys', async () => {
it('removes only manifest-listed files', async () => {
const io = makeIo();
await runKtxSetupAgentsStep(
{
projectDir: tempDir,
inputMode: 'disabled',
yes: true,
agents: true,
target: 'claude-code',
scope: 'project',
mode: 'both',
skipAgents: false,
},
agents: true,
target: 'claude-code',
scope: 'project',
mode: 'cli',
skipAgents: false,
},
io.io,
);
await writeFile(join(tempDir, '.claude/skills/ktx/keep.txt'), 'user file', 'utf-8');
@ -230,7 +211,7 @@ describe('setup agents', () => {
agents: true,
target: 'claude-code',
scope: 'project',
mode: 'both',
mode: 'cli',
skipAgents: false,
},
io.io,
@ -243,25 +224,18 @@ describe('setup agents', () => {
expect(output).toContain('.claude/skills/ktx/SKILL.md');
expect(output).toContain('+ Rule installed — tells your agent when to use KTX');
expect(output).toContain('.claude/rules/ktx.md');
expect(output).toContain('+ MCP config added — lets your agent talk to KTX over MCP');
expect(output).toContain('.mcp.json');
});
it('formats summary with relative paths for project scope', () => {
const summary = formatInstallSummary(
[{ target: 'cursor', scope: 'project', mode: 'both' }],
[
{ kind: 'file', path: join(tempDir, '.cursor/rules/ktx.mdc') },
{ kind: 'json-key', path: join(tempDir, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
],
[{ target: 'cursor', scope: 'project', mode: 'cli' }],
[{ kind: 'file', path: join(tempDir, '.cursor/rules/ktx.mdc') }],
tempDir,
);
expect(summary).toContain('Cursor');
expect(summary).toContain('+ Rule installed — tells your agent when to use KTX');
expect(summary).toContain('.cursor/rules/ktx.mdc');
expect(summary).toContain('+ MCP config added — lets your agent talk to KTX over MCP');
expect(summary).toContain('.cursor/mcp.json');
expect(summary).not.toContain(tempDir);
});
@ -269,12 +243,13 @@ describe('setup agents', () => {
const summary = formatInstallSummary(
[
{ target: 'claude-code', scope: 'project', mode: 'cli' },
{ target: 'codex', scope: 'project', mode: 'mcp' },
{ target: 'codex', scope: 'project', mode: 'cli' },
],
[
{ kind: 'file', path: join(tempDir, '.claude/skills/ktx/SKILL.md'), role: 'skill' },
{ kind: 'file', path: join(tempDir, '.claude/rules/ktx.md'), role: 'rule' },
{ kind: 'json-key', path: join(tempDir, '.agents/mcp/ktx.json'), jsonPath: ['mcpServers', 'ktx'] },
{ kind: 'file', path: join(tempDir, '.agents/skills/ktx/SKILL.md'), role: 'skill' },
{ kind: 'file', path: join(tempDir, '.codex/instructions/ktx.md'), role: 'rule' },
],
tempDir,
);
@ -283,6 +258,6 @@ describe('setup agents', () => {
expect(summary).toContain('+ Skill installed — teaches your agent which KTX commands to run');
expect(summary).toContain('+ Rule installed — tells your agent when to use KTX');
expect(summary).toContain('Codex');
expect(summary).toContain('+ MCP config added — lets your agent talk to KTX over MCP');
expect(summary).toContain('.agents/skills/ktx/SKILL.md');
});
});

View file

@ -9,7 +9,7 @@ import { withSetupInterruptConfirmation } from './setup-interrupt.js';
export type KtxAgentTarget = 'claude-code' | 'codex' | 'cursor' | 'opencode' | 'universal';
export type KtxAgentScope = 'project' | 'global';
export type KtxAgentInstallMode = 'cli' | 'mcp' | 'both';
export type KtxAgentInstallMode = 'cli';
export interface KtxSetupAgentsArgs {
projectDir: string;
@ -91,17 +91,9 @@ export function plannedKtxAgentFiles(input: {
'claude-code': { kind: 'file', path: join(root, '.claude/rules/ktx.md'), role: 'rule' },
codex: { kind: 'file', path: join(root, '.codex/instructions/ktx.md'), role: 'rule' },
};
const mcpEntries: Record<KtxAgentTarget, InstallEntry> = {
'claude-code': { kind: 'json-key', path: join(root, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
codex: { kind: 'json-key', path: join(root, '.agents/mcp/ktx.json'), jsonPath: ['mcpServers', 'ktx'] },
cursor: { kind: 'json-key', path: join(root, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
opencode: { kind: 'json-key', path: join(root, '.opencode/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
universal: { kind: 'json-key', path: join(root, '.agents/mcp/ktx.json'), jsonPath: ['mcpServers', 'ktx'] },
};
return [
...(input.mode === 'cli' || input.mode === 'both' ? [cliEntries[input.target], ruleEntries[input.target]] : []),
...(input.mode === 'mcp' || input.mode === 'both' ? [mcpEntries[input.target]] : []),
].filter((entry): entry is InstallEntry => entry !== undefined);
return [cliEntries[input.target], ruleEntries[input.target]].filter(
(entry): entry is InstallEntry => entry !== undefined,
);
}
function ktxCliLauncher(): KtxCliLauncher {
@ -187,32 +179,6 @@ function ruleInstructionContent(input: { projectDir: string }): string {
].join('\n');
}
function mcpConfig(projectDir: string, launcher: KtxCliLauncher): Record<string, unknown> {
return {
command: launcher.command,
args: [...launcher.args, '--project-dir', projectDir, 'serve', '--mcp', 'stdio', '--semantic-compute', '--execute-queries'],
env: {},
};
}
async function writeJsonKey(path: string, jsonPath: string[], value: Record<string, unknown>): Promise<void> {
let root: Record<string, unknown> = {};
try {
root = JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>;
} catch {
root = {};
}
let cursor = root;
for (const segment of jsonPath.slice(0, -1)) {
const next = cursor[segment];
if (!next || typeof next !== 'object' || Array.isArray(next)) cursor[segment] = {};
cursor = cursor[segment] as Record<string, unknown>;
}
cursor[jsonPath.at(-1) as string] = value;
await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(root, null, 2)}\n`, 'utf-8');
}
async function removeJsonKey(path: string, jsonPath: string[]): Promise<void> {
const root = JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>;
let cursor: Record<string, unknown> = root;
@ -351,7 +317,6 @@ export function formatInstallSummary(
const fileHints: Record<string, string> = {
skill: 'teaches your agent which KTX commands to run',
rule: 'tells your agent when to use KTX',
mcp: 'lets your agent talk to KTX over MCP',
};
const lines: string[] = [];
@ -367,9 +332,6 @@ export function formatInstallSummary(
const hint = fileHints[isRule ? 'rule' : (entry.role ?? 'skill')] ?? '';
lines.push(` + ${label}${hint}`);
lines.push(` ${displayPath}`);
} else {
lines.push(` + MCP config added — ${fileHints.mcp}`);
lines.push(` ${displayPath}`);
}
}
}
@ -385,16 +347,13 @@ async function installTarget(input: {
const entries = plannedKtxAgentFiles(input);
const launcher = ktxCliLauncher();
for (const entry of entries) {
if (entry.kind === 'file') {
const content =
entry.role === 'rule'
? ruleInstructionContent({ projectDir: input.projectDir })
: cliInstructionContent({ projectDir: input.projectDir, launcher });
await mkdir(dirname(entry.path), { recursive: true });
await writeFile(entry.path, content, 'utf-8');
} else {
await writeJsonKey(entry.path, entry.jsonPath, mcpConfig(input.projectDir, launcher));
}
if (entry.kind !== 'file') continue;
const content =
entry.role === 'rule'
? ruleInstructionContent({ projectDir: input.projectDir })
: cliInstructionContent({ projectDir: input.projectDir, launcher });
await mkdir(dirname(entry.path), { recursive: true });
await writeFile(entry.path, content, 'utf-8');
}
return entries;
}
@ -425,8 +384,6 @@ export async function runKtxSetupAgentsStep(
message: 'How should agents use this KTX project?',
options: [
{ value: 'cli', label: 'CLI tools and skills' },
{ value: 'mcp', label: 'MCP server config' },
{ value: 'both', label: 'Both' },
{ value: 'skip', label: 'Skip' },
{ value: 'back', label: 'Back' },
],

View file

@ -173,7 +173,7 @@ describe('runDemoTour', () => {
const mockAgents = vi.fn().mockResolvedValue({
status: 'ready',
projectDir: '/tmp/test',
installs: [{ target: 'claude-code', scope: 'project', mode: 'both' }],
installs: [{ target: 'claude-code', scope: 'project', mode: 'cli' }],
} satisfies KtxSetupAgentsResult);
const navigation = vi.fn().mockResolvedValue('forward');

View file

@ -365,7 +365,7 @@ export async function runDemoTour(
yes: false,
agents: true,
scope: 'project',
mode: 'both',
mode: 'cli',
skipAgents: false,
},
io,

View file

@ -245,7 +245,7 @@ describe('setup embeddings step', () => {
const io = makeIo();
const ensureLocalEmbeddings = vi.fn(async () => {
throw new Error(
'KTX Python runtime is required for this command. Run: ktx runtime install --feature local-embeddings --yes',
'KTX Python runtime is required for this command. Run: ktx dev runtime install --feature local-embeddings --yes',
);
});
@ -263,7 +263,7 @@ describe('setup embeddings step', () => {
expect(result.status).toBe('failed');
expect(io.stderr()).toContain(
'KTX Python runtime is required for this command. Run: ktx runtime install --feature local-embeddings --yes',
'KTX Python runtime is required for this command. Run: ktx dev runtime install --feature local-embeddings --yes',
);
});
@ -290,7 +290,7 @@ describe('setup embeddings step', () => {
expect(config.setup?.completed_steps ?? []).not.toContain('embeddings');
expect(config.ingest.embeddings.backend).toBe('deterministic');
expect(io.stderr()).toContain('Local embedding health check failed: 401 invalid api key [redacted]');
expect(io.stderr()).toContain('Prepare the runtime with: ktx runtime start --feature local-embeddings');
expect(io.stderr()).toContain('Prepare the runtime with: ktx dev runtime start --feature local-embeddings');
expect(io.stderr()).not.toContain('skip for now');
});

View file

@ -314,7 +314,7 @@ function localEmbeddingSetupMessage(message: string): string {
return [
`Local embedding health check failed: ${message}`,
'Local embeddings use the KTX-managed Python runtime.',
'Prepare the runtime with: ktx runtime start --feature local-embeddings',
'Prepare the runtime with: ktx dev runtime start --feature local-embeddings',
'Use --yes with setup to install and start the runtime without prompting.',
'The first run may download Python packages and the all-MiniLM-L6-v2 model.',
].join('\n');

View file

@ -1007,7 +1007,6 @@ describe('setup status', () => {
mode: 'new',
agents: false,
agentScope: 'project',
agentInstallMode: 'cli',
skipAgents: true,
inputMode: 'disabled',
yes: false,
@ -1525,7 +1524,6 @@ describe('setup status', () => {
agents: true,
target: 'codex',
agentScope: 'project',
agentInstallMode: 'cli',
inputMode: 'disabled',
yes: true,
cliVersion: '0.2.0',
@ -1579,7 +1577,6 @@ describe('setup status', () => {
agents: true,
target: 'codex',
agentScope: 'project',
agentInstallMode: 'cli',
inputMode: 'disabled',
yes: true,
cliVersion: '0.2.0',
@ -1996,7 +1993,7 @@ describe('setup status', () => {
const agents = vi.fn(async () => ({
status: 'ready' as const,
projectDir: tempDir,
installs: [{ target: 'universal' as const, scope: 'project' as const, mode: 'both' as const }],
installs: [{ target: 'universal' as const, scope: 'project' as const, mode: 'cli' as const }],
}));
await expect(
@ -2008,7 +2005,6 @@ describe('setup status', () => {
agents: true,
target: 'universal',
agentScope: 'project',
agentInstallMode: 'both',
inputMode: 'disabled',
yes: true,
cliVersion: '0.2.0',

View file

@ -7,7 +7,6 @@ import type { KtxCliIo } from './cli-runtime.js';
import { formatSetupNextStepLines } from './next-steps.js';
import { isKtxSetupExitError, withSetupInterruptConfirmation } from './setup-interrupt.js';
import {
type KtxAgentInstallMode,
type KtxAgentScope,
type KtxAgentTarget,
type KtxSetupAgentsDeps,
@ -60,7 +59,6 @@ export type KtxSetupArgs =
agents: boolean;
target?: KtxAgentTarget;
agentScope?: KtxAgentScope;
agentInstallMode?: KtxAgentInstallMode;
skipAgents?: boolean;
inputMode: 'auto' | 'disabled';
yes: boolean;
@ -736,7 +734,7 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
agents: true,
...(args.target ? { target: args.target } : {}),
scope: args.agentScope ?? 'project',
mode: args.agentInstallMode ?? 'cli',
mode: 'cli',
skipAgents: false,
},
io,

View file

@ -4,8 +4,6 @@ import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { promisify } from 'node:util';
import { parseKtxProjectConfig } from '@ktx/context/project';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import Database from 'better-sqlite3';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
@ -60,12 +58,6 @@ function getRunId(stdout: string): string {
return match[1];
}
function structuredContent<T extends object>(result: unknown): T {
const content = (result as { structuredContent?: unknown }).structuredContent;
expect(content).toBeDefined();
return content as T;
}
async function writeWarehouseConfig(projectDir: string): Promise<void> {
await writeFile(
join(projectDir, 'ktx.yaml'),
@ -344,81 +336,13 @@ describe('standalone built ktx CLI smoke', () => {
expect(inspect.stdout).not.toContain('ktx serve --mcp stdio');
});
it('serves seeded demo wiki and semantic-layer context over stdio MCP', async () => {
const projectDir = join(tempDir, 'seeded-mcp-project');
const seeded = await runBuiltCli(
['setup', 'demo', '--mode', 'seeded', '--project-dir', projectDir, '--plain', '--no-input'],
{
env: { ...process.env, ANTHROPIC_API_KEY: '' },
},
);
expectProjectStderr(seeded, projectDir);
expect(seeded.stdout).toContain('Mode: seeded');
const client = new Client({ name: 'ktx-seeded-demo-smoke-client', version: '0.0.0' });
const transport = new StdioClientTransport({
command: process.execPath,
args: [CLI_BIN, 'serve', '--mcp', 'stdio', '--project-dir', projectDir, '--user-id', 'smoke-user'],
stderr: 'pipe',
});
try {
await client.connect(transport);
const toolNames = (await client.listTools()).tools.map((tool) => tool.name).sort();
expect(toolNames).toEqual(
expect.arrayContaining(['knowledge_read', 'knowledge_search', 'sl_read_source', 'sl_validate']),
);
const knowledgeSearch = structuredContent<{
results: Array<{ key: string; summary: string; score: number }>;
totalFound: number;
}>(await client.callTool({ name: 'knowledge_search', arguments: { query: 'ARR contract-first definition', limit: 10 } }));
expect(knowledgeSearch.totalFound).toBeGreaterThan(0);
expect(knowledgeSearch.results.map((result) => result.key)).toContain('orbit-arr-contract-first-definition');
const knowledgeRead = structuredContent<{
key: string;
summary: string;
content: string;
tags: string[];
slRefs: string[];
}>(await client.callTool({ name: 'knowledge_read', arguments: { key: 'orbit-arr-contract-first-definition' } }));
expect(knowledgeRead.key).toBe('orbit-arr-contract-first-definition');
expect(knowledgeRead.summary).toContain('ARR');
expect(knowledgeRead.content).toContain('contract');
expect(knowledgeRead.slRefs).toContain('mart_arr_daily');
const slRead = structuredContent<{ sourceName: string; yaml: string }>(
await client.callTool({
name: 'sl_read_source',
arguments: { connectionId: 'dbt-main', sourceName: 'mart_arr_daily' },
}),
);
expect(slRead.sourceName).toBe('mart_arr_daily');
expect(slRead.yaml).toContain('name: mart_arr_daily');
expect(slRead.yaml).toContain('measures:');
const slValidate = structuredContent<{ success: boolean; errors: string[]; warnings: string[] }>(
await client.callTool({
name: 'sl_validate',
arguments: { connectionId: 'dbt-main', names: ['mart_arr_daily', 'stg_contracts'] },
}),
);
expect(slValidate.success).toBe(true);
expect(slValidate.errors).toEqual([]);
} finally {
await client.close();
}
});
it('runs doctor setup through the built binary', async () => {
const result = await runBuiltCli(['dev', 'doctor', 'setup', '--no-input']);
const result = await runBuiltCli(['status', '--no-input']);
expect(result.stdout).toContain('KTX setup doctor');
expect(result.stdout).toContain('Node 22+');
expect(result.stdout).toContain('Workspace-local CLI');
expect(result.stderr).toBe(`Project: ${process.cwd()}\n`);
expect(result.stderr).toBe('');
expect([0, 1]).toContain(result.code);
});
@ -747,185 +671,4 @@ describe('standalone built ktx CLI smoke', () => {
});
});
it('serves local ingest MCP tools over stdio from the built binary', async () => {
const projectDir = join(tempDir, 'project');
const init = await runSetupNewProject(projectDir);
expectProjectStderr(init, projectDir);
await writeWarehouseConfig(projectDir);
const client = new Client({ name: 'ktx-smoke-client', version: '0.0.0' });
const transport = new StdioClientTransport({
command: process.execPath,
args: [CLI_BIN, 'serve', '--mcp', 'stdio', '--project-dir', projectDir, '--user-id', 'smoke-user'],
stderr: 'pipe',
});
try {
await client.connect(transport);
const tools = await client.listTools();
const toolNames = tools.tools.map((tool) => tool.name).sort();
expect(toolNames).toEqual(
expect.arrayContaining([
'connection_list',
'connection_test',
'ingest_report',
'ingest_replay',
'ingest_status',
'ingest_trigger',
'knowledge_read',
'knowledge_search',
'knowledge_write',
'scan_list_artifacts',
'scan_read_artifact',
'scan_report',
'scan_status',
'scan_trigger',
'sl_list_sources',
'sl_read_source',
'sl_validate',
'sl_write_source',
]),
);
const connections = structuredContent<{
connections: Array<{ id: string; name: string; connectionType: string }>;
}>(await client.callTool({ name: 'connection_list', arguments: {} }));
expect(connections).toEqual({
connections: [{ id: 'warehouse', name: 'warehouse', connectionType: 'POSTGRESQL' }],
});
await expect(client.callTool({ name: 'ingest_status', arguments: { runId: 'missing-run' } })).resolves.toEqual({
content: [{ type: 'text', text: 'Ingest run "missing-run" was not found.' }],
isError: true,
});
await expect(client.callTool({ name: 'ingest_report', arguments: { runId: 'missing-run' } })).resolves.toEqual({
content: [{ type: 'text', text: 'Ingest report "missing-run" was not found.' }],
isError: true,
});
await expect(client.callTool({ name: 'ingest_replay', arguments: { runId: 'missing-run' } })).resolves.toEqual({
content: [{ type: 'text', text: 'Ingest replay "missing-run" was not found.' }],
isError: true,
});
} finally {
await client.close();
}
});
it('serves scan execution and artifact inspection tools over stdio from the built binary', async () => {
const projectDir = join(tempDir, 'scan-mcp-project');
const init = await runSetupNewProject(projectDir);
expectProjectStderr(init, projectDir);
const dbPath = join(projectDir, 'warehouse.db');
createSqliteWarehouse(dbPath);
await writeSqliteScanConfig(projectDir, dbPath);
const client = new Client({ name: 'ktx-scan-smoke-client', version: '0.0.0' });
const transport = new StdioClientTransport({
command: process.execPath,
args: [CLI_BIN, 'serve', '--mcp', 'stdio', '--project-dir', projectDir, '--user-id', 'smoke-user'],
stderr: 'pipe',
});
try {
await client.connect(transport);
const connectionTest = structuredContent<{
id: string;
connectionType: string;
ok: boolean;
tableCount: number | null;
}>(await client.callTool({ name: 'connection_test', arguments: { connectionId: 'warehouse' } }));
expect(connectionTest).toMatchObject({
id: 'warehouse',
connectionType: 'SQLITE',
ok: true,
tableCount: 2,
});
const trigger = structuredContent<{
runId: string;
status: 'done';
done: true;
connectionId: string;
mode: string;
dryRun: boolean;
report: {
artifactPaths: { manifestShards: string[] };
manifestShardsWritten: number;
};
}>(
await client.callTool({
name: 'scan_trigger',
arguments: {
connectionId: 'warehouse',
mode: 'structural',
detectRelationships: false,
dryRun: false,
},
}),
);
expect(trigger).toMatchObject({
status: 'done',
done: true,
connectionId: 'warehouse',
mode: 'structural',
dryRun: false,
});
expect(trigger.report.artifactPaths.manifestShards).toEqual(['semantic-layer/warehouse/_schema/public.yaml']);
expect(trigger.report.manifestShardsWritten).toBe(1);
const status = structuredContent<{
runId: string;
status: string;
done: boolean;
reportPath: string | null;
}>(await client.callTool({ name: 'scan_status', arguments: { runId: trigger.runId } }));
expect(status).toMatchObject({
runId: trigger.runId,
status: 'done',
done: true,
});
expect(status.reportPath).toContain('scan-report.json');
const artifacts = structuredContent<{
runId: string;
artifacts: Array<{ path: string; type: string }>;
}>(await client.callTool({ name: 'scan_list_artifacts', arguments: { runId: trigger.runId } }));
expect(artifacts.artifacts).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: 'semantic-layer/warehouse/_schema/public.yaml', type: 'manifest_shard' }),
expect.objectContaining({ type: 'report' }),
expect.objectContaining({ type: 'raw_source' }),
]),
);
const manifestArtifact = structuredContent<{
runId: string;
path: string;
type: string;
content: string;
}>(
await client.callTool({
name: 'scan_read_artifact',
arguments: {
runId: trigger.runId,
path: 'semantic-layer/warehouse/_schema/public.yaml',
},
}),
);
expect(manifestArtifact).toMatchObject({
runId: trigger.runId,
path: 'semantic-layer/warehouse/_schema/public.yaml',
type: 'manifest_shard',
});
expect(manifestArtifact.content).toContain('orders:');
expect(manifestArtifact.content).toContain('source: formal');
} finally {
await client.close();
}
});
});

3
pnpm-lock.yaml generated
View file

@ -97,9 +97,6 @@ importers:
'@ktx/llm':
specifier: workspace:*
version: link:../llm
'@modelcontextprotocol/sdk':
specifier: ^1.27.1
version: 1.29.0(zod@4.4.3)
commander:
specifier: 14.0.3
version: 14.0.3

View file

@ -28,7 +28,7 @@ describe('Conductor workspace scripts', () => {
assert.match(setupScript, /pnpm install --frozen-lockfile --prefer-offline/);
assert.match(setupScript, /pnpm run native:rebuild/);
assert.match(setupScript, /pnpm run build/);
assert.match(setupScript, /packages\/cli\/dist\/bin\.js dev doctor setup --no-input/);
assert.match(setupScript, /packages\/cli\/dist\/bin\.js status --no-input/);
assert.doesNotMatch(setupScript, /scripts\/conductor\//);
});

View file

@ -137,6 +137,6 @@ echo "Building KTX packages..."
pnpm run build
echo "Running KTX setup doctor..."
node packages/cli/dist/bin.js dev doctor setup --no-input
node packages/cli/dist/bin.js status --no-input
echo "=== Setup complete ==="

View file

@ -71,7 +71,7 @@ describe('standalone example docs', () => {
assert.match(examples, /unified Historic SQL artifacts/);
assert.match(readme, /--enable-historic-sql/);
assert.match(readme, /--historic-sql-min-executions 2/);
assert.match(readme, /ktx dev doctor --project-dir/);
assert.match(readme, /ktx status --project-dir/);
assert.match(readme, /Postgres Historic SQL/);
assert.match(readme, /manifest\.json/);
assert.match(readme, /tables\/\*\.json/);
@ -152,33 +152,20 @@ describe('standalone example docs', () => {
assert.match(contributing, /ktx-daemon\/\s+# Daemon/);
});
it('documents every standalone MCP tool that the CLI server exposes', async () => {
it('documents agent-facing CLI commands', async () => {
const servingAgents = await readText('docs-site/content/docs/guides/serving-agents.mdx');
for (const tool of [
'connection_list',
'connection_test',
'knowledge_search',
'knowledge_read',
'knowledge_write',
'sl_list_sources',
'sl_read_source',
'sl_write_source',
'sl_validate',
'sl_query',
'scan_trigger',
'scan_status',
'scan_report',
'scan_list_artifacts',
'scan_read_artifact',
'ingest_trigger',
'ingest_status',
'ingest_report',
'ingest_replay',
'memory_capture',
'memory_capture_status',
for (const command of [
'ktx agent tools --json',
'ktx agent context --json',
'ktx agent sl list --json',
'ktx agent sl read orders --json',
'ktx agent sl query --json',
'ktx agent wiki search "revenue recognition" --json',
'ktx agent wiki read order-status-definitions --json',
'ktx agent sql execute --json',
]) {
assert.match(servingAgents, new RegExp(`\`${tool}\``));
assert.match(servingAgents, new RegExp(command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
});
@ -199,14 +186,14 @@ describe('standalone example docs', () => {
assert.match(rootReadme, publicPackagePattern('npm install -g {package}'));
assert.match(quickstart, publicPackagePattern('npm install -g {package}'));
assert.match(quickstart, /ktx runtime install --feature local-embeddings --yes/);
assert.match(quickstart, /ktx runtime start --feature local-embeddings/);
assert.match(quickstart, /Install `uv`, run `ktx runtime doctor`/);
assert.match(quickstart, /ktx dev runtime install --feature local-embeddings --yes/);
assert.match(quickstart, /ktx dev runtime start --feature local-embeddings/);
assert.match(quickstart, /Install `uv`, run `ktx dev runtime doctor`/);
assert.match(packageArtifacts, /requires `uv` on `PATH`/);
assert.match(packageArtifacts, /ktx runtime status/);
assert.match(packageArtifacts, /ktx runtime doctor/);
assert.match(packageArtifacts, /ktx runtime prune --dry-run/);
assert.match(packageArtifacts, /ktx runtime prune --yes/);
assert.match(packageArtifacts, /ktx dev runtime status/);
assert.match(packageArtifacts, /ktx dev runtime doctor/);
assert.match(packageArtifacts, /ktx dev runtime prune --dry-run/);
assert.match(packageArtifacts, /ktx dev runtime prune --yes/);
assert.match(
packageArtifacts,
new RegExp(
@ -215,7 +202,7 @@ describe('standalone example docs', () => {
)}\` runtime wheel`,
),
);
assert.match(rootReadme, /ktx serve --mcp stdio/);
assert.doesNotMatch(rootReadme, /ktx serve --mcp stdio/);
assert.doesNotMatch(rootReadme, /uv run ktx-daemon serve-http/);
assert.doesNotMatch(rootReadme, /--semantic-compute-url http:\/\/127\.0\.0\.1:8765/);
});
@ -237,10 +224,10 @@ describe('standalone example docs', () => {
assert.doesNotMatch(readme, /standalone Python distributions/);
assert.doesNotMatch(readme, /installs the Python artifacts directly/);
assert.match(readme, /requires `uv` on `PATH`/);
assert.match(readme, /ktx runtime status/);
assert.match(readme, /ktx runtime doctor/);
assert.match(readme, /ktx runtime prune --dry-run/);
assert.match(readme, /ktx runtime prune --yes/);
assert.match(readme, /ktx dev runtime status/);
assert.match(readme, /ktx dev runtime doctor/);
assert.match(readme, /ktx dev runtime prune --dry-run/);
assert.match(readme, /ktx dev runtime prune --yes/);
assert.doesNotMatch(readme, /@ktx\/context/);
assert.doesNotMatch(readme, /@ktx\/cli/);
assert.doesNotMatch(readme, /python -m ktx_daemon semantic-validate/);

View file

@ -250,17 +250,17 @@ function parseDaemonBaseUrl(stdout) {
}
async function startDaemon(cleanInstallDir) {
const result = await run('pnpm', ['exec', 'ktx', 'runtime', 'start'], {
const result = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'start'], {
cwd: cleanInstallDir,
env: managedRuntimeEnv(cleanInstallDir),
timeout: 120_000,
});
requireSuccess('ktx runtime start', result);
requireSuccess('ktx dev runtime start', result);
return parseDaemonBaseUrl(result.stdout);
}
async function stopDaemon(cleanInstallDir) {
await run('pnpm', ['exec', 'ktx', 'runtime', 'stop'], {
await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'stop'], {
cwd: cleanInstallDir,
env: managedRuntimeEnv(cleanInstallDir),
timeout: 30_000,
@ -283,7 +283,7 @@ async function prepareCleanInstall(layout, cleanInstallDir) {
await run('pnpm', ['install'], { cwd: cleanInstallDir, timeout: 120_000 }).then((result) =>
requireSuccess('pnpm install clean artifact project', result),
);
await run('pnpm', ['exec', 'ktx', 'runtime', 'install', '--yes'], {
await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'install', '--yes'], {
cwd: cleanInstallDir,
env: managedRuntimeEnv(cleanInstallDir),
timeout: 120_000,

View file

@ -73,27 +73,27 @@ export function localEmbeddingsSmokeCommands(input) {
timeoutMs: 60_000,
},
{
label: 'ktx runtime status missing',
label: 'ktx dev runtime status missing',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'status', '--json'],
args: ['exec', 'ktx', 'dev', 'runtime', 'status', '--json'],
timeoutMs: 60_000,
},
{
label: 'ktx runtime install local embeddings',
label: 'ktx dev runtime install local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
args: ['exec', 'ktx', 'dev', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
timeoutMs: 1_200_000,
},
{
label: 'ktx runtime status local embeddings ready',
label: 'ktx dev runtime status local embeddings ready',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'status', '--json'],
args: ['exec', 'ktx', 'dev', 'runtime', 'status', '--json'],
timeoutMs: 60_000,
},
{
label: 'ktx runtime start local embeddings',
label: 'ktx dev runtime start local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'start', '--feature', 'local-embeddings'],
args: ['exec', 'ktx', 'dev', 'runtime', 'start', '--feature', 'local-embeddings'],
timeoutMs: 300_000,
},
{
@ -118,9 +118,9 @@ export function localEmbeddingsSmokeCommands(input) {
timeoutMs: 900_000,
},
{
label: 'ktx runtime stop local embeddings',
label: 'ktx dev runtime stop local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'stop'],
args: ['exec', 'ktx', 'dev', 'runtime', 'stop'],
timeoutMs: 60_000,
},
];
@ -361,7 +361,7 @@ export async function runLocalEmbeddingsRuntimeSmoke(options = {}) {
process.stdout.write('KTX local embeddings runtime smoke verified\n');
} finally {
if (daemonStarted) {
await run('pnpm', ['exec', 'ktx', 'runtime', 'stop'], {
await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'stop'], {
cwd: installDir,
env: smokeEnv,
timeoutMs: 60_000,

View file

@ -89,23 +89,23 @@ describe('localEmbeddingsSmokeCommands', () => {
assert.deepEqual(commands.map((command) => command.label), [
'ktx public package version',
'ktx runtime status missing',
'ktx runtime install local embeddings',
'ktx runtime status local embeddings ready',
'ktx runtime start local embeddings',
'ktx dev runtime status missing',
'ktx dev runtime install local embeddings',
'ktx dev runtime status local embeddings ready',
'ktx dev runtime start local embeddings',
'ktx setup local embeddings',
'ktx runtime stop local embeddings',
'ktx dev runtime stop local embeddings',
]);
assert.deepEqual(commands[2], {
label: 'ktx runtime install local embeddings',
label: 'ktx dev runtime install local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
args: ['exec', 'ktx', 'dev', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
timeoutMs: 1_200_000,
});
assert.deepEqual(commands[4], {
label: 'ktx runtime start local embeddings',
label: 'ktx dev runtime start local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'start', '--feature', 'local-embeddings'],
args: ['exec', 'ktx', 'dev', 'runtime', 'start', '--feature', 'local-embeddings'],
timeoutMs: 300_000,
});
assert.deepEqual(commands[5].args, [

View file

@ -149,7 +149,7 @@ export async function findPythonArtifacts(pythonDir) {
files,
RUNTIME_WHEEL_DISTRIBUTION_NAME,
'.whl',
'kaelio-ktx runtime wheel',
'kaelio-ktx dev runtime wheel',
pythonDir,
RUNTIME_WHEEL_PACKAGE_VERSION,
),
@ -594,8 +594,8 @@ try {
requireOutput('ktx public package version', version, /@kaelio\\/ktx 0\\.1\\.0/);
const runtimeStatusBefore = parseJsonResult(
'ktx runtime status missing',
await run('pnpm', ['exec', 'ktx', 'runtime', 'status', '--json']),
'ktx dev runtime status missing',
await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'status', '--json']),
);
assert.equal(runtimeStatusBefore.kind, 'missing');
assert.equal(runtimeStatusBefore.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
@ -835,8 +835,8 @@ try {
requireOutput('ktx sl query first managed runtime install', slQuery, /orders/);
const runtimeStatusAfter = parseJsonResult(
'ktx runtime status ready',
await run('pnpm', ['exec', 'ktx', 'runtime', 'status', '--json']),
'ktx dev runtime status ready',
await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'status', '--json']),
);
assert.equal(runtimeStatusAfter.kind, 'ready');
assert.deepEqual(runtimeStatusAfter.manifest.features, ['core']);
@ -864,51 +864,51 @@ try {
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"rows": \\[\\s*\\[\\s*3\\s*\\]\\s*\\]/);
process.stdout.write('ktx sl query sqlite execute verified\\n');
const runtimeDoctor = await run('pnpm', ['exec', 'ktx', 'runtime', 'doctor']);
requireSuccess('ktx runtime doctor', runtimeDoctor);
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS uv/);
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS Bundled Python wheel/);
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS Managed Python runtime/);
process.stdout.write('ktx runtime doctor verified\\n');
const runtimeDoctor = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'doctor']);
requireSuccess('ktx dev runtime doctor', runtimeDoctor);
requireOutput('ktx dev runtime doctor', runtimeDoctor, /PASS uv/);
requireOutput('ktx dev runtime doctor', runtimeDoctor, /PASS Bundled Python wheel/);
requireOutput('ktx dev runtime doctor', runtimeDoctor, /PASS Managed Python runtime/);
process.stdout.write('ktx dev runtime doctor verified\\n');
const runtimeStart = await run('pnpm', ['exec', 'ktx', 'runtime', 'start']);
requireSuccess('ktx runtime start', runtimeStart);
const runtimeStart = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'start']);
requireSuccess('ktx dev runtime start', runtimeStart);
daemonStarted = true;
requireOutput('ktx runtime start', runtimeStart, /Started KTX Python daemon/);
requireOutput('ktx runtime start', runtimeStart, /url: http:\\/\\/127\\.0\\.0\\.1:\\d+/);
requireOutput('ktx runtime start', runtimeStart, /features: core/);
requireOutput('ktx dev runtime start', runtimeStart, /Started KTX Python daemon/);
requireOutput('ktx dev runtime start', runtimeStart, /url: http:\\/\\/127\\.0\\.0\\.1:\\d+/);
requireOutput('ktx dev runtime start', runtimeStart, /features: core/);
const runtimeStartReuse = await run('pnpm', ['exec', 'ktx', 'runtime', 'start']);
requireSuccess('ktx runtime start reuse', runtimeStartReuse);
requireOutput('ktx runtime start reuse', runtimeStartReuse, /Using existing KTX Python daemon/);
requireOutput('ktx runtime start reuse', runtimeStartReuse, /features: core/);
const runtimeStartReuse = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'start']);
requireSuccess('ktx dev runtime start reuse', runtimeStartReuse);
requireOutput('ktx dev runtime start reuse', runtimeStartReuse, /Using existing KTX Python daemon/);
requireOutput('ktx dev runtime start reuse', runtimeStartReuse, /features: core/);
const runtimeStop = await run('pnpm', ['exec', 'ktx', 'runtime', 'stop']);
requireSuccess('ktx runtime stop', runtimeStop);
const runtimeStop = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'stop']);
requireSuccess('ktx dev runtime stop', runtimeStop);
daemonStarted = false;
requireOutput('ktx runtime stop', runtimeStop, /Stopped KTX Python daemon/);
process.stdout.write('ktx runtime daemon lifecycle verified\\n');
requireOutput('ktx dev runtime stop', runtimeStop, /Stopped KTX Python daemon/);
process.stdout.write('ktx dev runtime daemon lifecycle verified\\n');
const staleRuntimeDir = join(process.env.KTX_RUNTIME_ROOT, '0.0.0');
await mkdir(staleRuntimeDir, { recursive: true });
const runtimePruneDryRun = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune', '--dry-run']);
requireSuccess('ktx runtime prune dry run', runtimePruneDryRun);
requireOutput('ktx runtime prune dry run', runtimePruneDryRun, /Stale KTX Python runtimes/);
requireOutput('ktx runtime prune dry run', runtimePruneDryRun, /0\\.0\\.0/);
const runtimePruneDryRun = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune', '--dry-run']);
requireSuccess('ktx dev runtime prune dry run', runtimePruneDryRun);
requireOutput('ktx dev runtime prune dry run', runtimePruneDryRun, /Stale KTX Python runtimes/);
requireOutput('ktx dev runtime prune dry run', runtimePruneDryRun, /0\\.0\\.0/);
await access(staleRuntimeDir);
const runtimePruneNeedsConfirmation = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune']);
assert.equal(runtimePruneNeedsConfirmation.code, 1, 'ktx runtime prune needs confirmation');
assert.equal(runtimePruneNeedsConfirmation.stdout, '', 'ktx runtime prune needs confirmation wrote stdout');
const runtimePruneNeedsConfirmation = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune']);
assert.equal(runtimePruneNeedsConfirmation.code, 1, 'ktx dev runtime prune needs confirmation');
assert.equal(runtimePruneNeedsConfirmation.stdout, '', 'ktx dev runtime prune needs confirmation wrote stdout');
assert.match(runtimePruneNeedsConfirmation.stderr, /Refusing to prune without --yes/);
const runtimePruneConfirmed = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune', '--yes']);
requireSuccess('ktx runtime prune confirmed', runtimePruneConfirmed);
requireOutput('ktx runtime prune confirmed', runtimePruneConfirmed, /Removed stale KTX Python runtimes/);
requireOutput('ktx runtime prune confirmed', runtimePruneConfirmed, /0\\.0\\.0/);
const runtimePruneConfirmed = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune', '--yes']);
requireSuccess('ktx dev runtime prune confirmed', runtimePruneConfirmed);
requireOutput('ktx dev runtime prune confirmed', runtimePruneConfirmed, /Removed stale KTX Python runtimes/);
requireOutput('ktx dev runtime prune confirmed', runtimePruneConfirmed, /0\\.0\\.0/);
await assert.rejects(() => access(staleRuntimeDir));
process.stdout.write('ktx runtime prune verified\\n');
process.stdout.write('ktx dev runtime prune verified\\n');
const structuralScan = await run('pnpm', ['exec', 'ktx', 'dev', 'scan', 'warehouse',
'--project-dir',
@ -992,7 +992,7 @@ try {
process.stdout.write('ktx dev ingest provider guard verified\\n');
} finally {
if (daemonStarted) {
await run('pnpm', ['exec', 'ktx', 'runtime', 'stop']);
await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'stop']);
}
if (previousRuntimeRoot === undefined) {
delete process.env.KTX_RUNTIME_ROOT;
@ -1122,11 +1122,11 @@ try {
);
process.stdout.write('ktx seeded demo agent sl search verified\\n');
const doctor = await run('pnpm', ['exec', 'ktx', 'dev', 'doctor', 'setup', '--no-input']);
assert.ok([0, 1].includes(doctor.code), 'ktx dev doctor setup exit code must be 0 or 1');
requireStdout('ktx dev doctor setup', doctor, /KTX setup doctor/);
requireStdout('ktx dev doctor setup', doctor, /Node 22\\+/);
assert.equal(doctor.stderr, 'Project: ' + process.cwd() + '\\n', 'ktx dev doctor setup wrote unexpected stderr');
const doctor = await run('pnpm', ['exec', 'ktx', 'status', '--no-input']);
assert.ok([0, 1].includes(doctor.code), 'ktx status setup exit code must be 0 or 1');
requireStdout('ktx status setup', doctor, /KTX setup doctor/);
requireStdout('ktx status setup', doctor, /Node 22\\+/);
assert.equal(doctor.stderr, '', 'ktx status setup wrote unexpected stderr');
} finally {
await rm(root, { recursive: true, force: true });
}

View file

@ -151,7 +151,7 @@ describe('findPythonArtifacts', () => {
it('throws when a required Python artifact is missing', async () => {
const root = await mkdtemp(join(tmpdir(), 'ktx-artifacts-test-'));
try {
await assert.rejects(() => findPythonArtifacts(root), /Missing Python artifact: kaelio-ktx runtime wheel/);
await assert.rejects(() => findPythonArtifacts(root), /Missing Python artifact: kaelio-ktx dev runtime wheel/);
} finally {
await rm(root, { recursive: true, force: true });
}
@ -472,24 +472,24 @@ describe('verification snippets', () => {
assert.doesNotMatch(source, /run\('python'/);
assert.match(source, /KTX_RUNTIME_ROOT/);
assert.match(source, /managed-runtime/);
assert.match(source, /ktx runtime status missing/);
assert.match(source, /ktx dev runtime status missing/);
assert.match(source, /runtimeStatusBefore\.kind, 'missing'/);
assert.ok(source.includes(String.raw`Installing KTX Python runtime \(core\) with uv`));
assert.match(source, /KTX Python runtime ready:/);
assert.match(source, /ktx runtime status ready/);
assert.match(source, /ktx dev runtime status ready/);
assert.match(source, /runtimeStatusAfter\.kind, 'ready'/);
assert.match(source, /runtimeStatusAfter\.manifest\.features/);
assert.match(source, /ktx runtime doctor/);
assert.match(source, /ktx dev runtime doctor/);
assert.match(source, /PASS Managed Python runtime/);
assert.match(source, /ktx runtime start/);
assert.match(source, /ktx runtime start reuse/);
assert.match(source, /ktx dev runtime start/);
assert.match(source, /ktx dev runtime start reuse/);
assert.match(source, /Using existing KTX Python daemon/);
assert.match(source, /ktx runtime stop/);
assert.match(source, /ktx runtime prune dry run/);
assert.match(source, /ktx dev runtime stop/);
assert.match(source, /ktx dev runtime prune dry run/);
assert.match(source, /0\.0\.0/);
assert.match(source, /ktx runtime prune needs confirmation/);
assert.match(source, /ktx dev runtime prune needs confirmation/);
assert.match(source, /Refusing to prune without --yes/);
assert.match(source, /ktx runtime prune confirmed/);
assert.match(source, /ktx dev runtime prune confirmed/);
assert.match(source, /Removed stale KTX Python runtimes/);
assert.match(source, /assert\.rejects\(\(\) => access\(staleRuntimeDir\)\)/);
assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'dev',\s*'scan',\s*'warehouse'/);
@ -519,7 +519,7 @@ describe('verification snippets', () => {
assert.match(source, /LLM calls: none/);
assert.match(source, /ktx agent context --json/);
assert.doesNotMatch(source, new RegExp(["'demo'", "'--mode'", "'deterministic'"].join(', ')));
assert.match(source, /'dev', 'doctor', 'setup', '--no-input'/);
assert.match(source, /'status', '--no-input'/);
assert.match(source, /'--plain'/);
assert.match(source, /function requireProjectStderr/);
assert.match(source, /requireProjectStderr\('ktx setup demo seeded', seeded, projectDir\)/);

View file

@ -44,8 +44,8 @@ export async function runSetupDev(options = {}) {
{
name: 'doctor setup',
command: process.execPath,
args: ['packages/cli/dist/bin.js', 'dev', 'doctor', 'setup', '--no-input'],
retry: 'pnpm run ktx -- dev doctor setup --no-input',
args: ['packages/cli/dist/bin.js', 'status', '--no-input'],
retry: 'pnpm run ktx -- status --no-input',
},
];