diff --git a/docs-site/components/terminal-preview.tsx b/docs-site/components/terminal-preview.tsx
index a1f950c8..d430c4ac 100644
--- a/docs-site/components/terminal-preview.tsx
+++ b/docs-site/components/terminal-preview.tsx
@@ -47,7 +47,7 @@ export function TerminalPreview() {
${" "}
- ktx agent context --json
+ ktx status --json
diff --git a/docs-site/content/docs/ai-resources/agent-quickstart.mdx b/docs-site/content/docs/ai-resources/agent-quickstart.mdx
index 40983224..6fd6e5ac 100644
--- a/docs-site/content/docs/ai-resources/agent-quickstart.mdx
+++ b/docs-site/content/docs/ai-resources/agent-quickstart.mdx
@@ -22,7 +22,7 @@ Agents should start with the smallest source that answers the task:
| How to check project readiness | [ktx status](/docs/cli-reference/ktx-status) | [Quickstart](/docs/getting-started/quickstart) |
| How context gets built | [Building Context](/docs/guides/building-context) | [ktx ingest](/docs/cli-reference/ktx-ingest) |
| How semantic YAML works | [Writing Context](/docs/guides/writing-context) | [ktx sl](/docs/cli-reference/ktx-sl) |
-| How machine-readable CLI output is shaped | [ktx agent](/docs/cli-reference/ktx-agent) | [Markdown Access](/docs/ai-resources/markdown-access) |
+| How machine-readable CLI output is shaped | [ktx sl](/docs/cli-reference/ktx-sl) | [ktx wiki](/docs/cli-reference/ktx-wiki) |
## Operating workflow
diff --git a/docs-site/content/docs/ai-resources/markdown-access.mdx b/docs-site/content/docs/ai-resources/markdown-access.mdx
index c363a215..12bb7456 100644
--- a/docs-site/content/docs/ai-resources/markdown-access.mdx
+++ b/docs-site/content/docs/ai-resources/markdown-access.mdx
@@ -31,7 +31,8 @@ Every docs page has a Markdown route:
```text
https://docs.kaelio.com/ktx/docs/getting-started/quickstart.md
-https://docs.kaelio.com/ktx/docs/cli-reference/ktx-agent.md
+https://docs.kaelio.com/ktx/docs/cli-reference/ktx-sl.md
+https://docs.kaelio.com/ktx/docs/cli-reference/ktx-wiki.md
https://docs.kaelio.com/ktx/docs/guides/building-context.md
```
diff --git a/docs-site/content/docs/cli-reference/ktx-agent.mdx b/docs-site/content/docs/cli-reference/ktx-agent.mdx
deleted file mode 100644
index cdc4ceac..00000000
--- a/docs-site/content/docs/cli-reference/ktx-agent.mdx
+++ /dev/null
@@ -1,148 +0,0 @@
----
-title: "ktx agent"
-description: "Machine-readable commands for coding agents."
----
-
-Hidden commands that provide machine-readable JSON output for coding agents. These are the commands that agent integrations (Claude Code, Cursor, Codex, OpenCode) call under the hood — you typically won't use them directly.
-
-All `ktx agent` subcommands require `--json` and produce structured JSON output on stdout.
-
-## Command signature
-
-```bash
-ktx agent --json [options]
-```
-
-## Subcommands
-
-| Subcommand | Description |
-|-----------|-------------|
-| `tools` | Print available agent-facing KTX tools |
-| `context` | Print project context for agent planning |
-| `sl list` | List semantic-layer sources |
-| `sl read ` | Read one semantic-layer source |
-| `sl query` | Run a semantic-layer query from a JSON file |
-| `wiki search ` | Search KTX wiki pages |
-| `wiki read ` | Read one KTX wiki page |
-| `sql execute` | Execute read-only SQL with a row limit |
-
-## Options
-
-### `agent tools`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-
-### `agent context`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-
-### `agent sl list`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--connection-id ` | Filter by connection id | — |
-| `--query ` | Search source names and descriptions | — |
-
-### `agent sl read`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--connection-id ` | Connection id containing the source | — |
-
-### `agent sl query`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--connection-id ` | Connection id for execution (required) | — |
-| `--query-file ` | JSON semantic-layer query file (required) | — |
-| `--execute` | Execute the compiled query against the connection | `false` |
-| `--max-rows ` | Maximum rows to return when executing (1-1000) | — |
-
-### `agent wiki search`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--limit ` | Maximum search results | `10` |
-
-### `agent wiki read`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-
-### `agent sql execute`
-
-| Flag | Description | Default |
-|------|-------------|---------|
-| `--json` | Print JSON output (required) | — |
-| `--connection-id ` | Connection id for execution (required) | — |
-| `--sql-file ` | SQL file to execute (required) | — |
-| `--max-rows ` | Maximum rows to return, 1-1000 (required) | — |
-
-## Examples
-
-```bash
-# List available tools
-ktx agent tools --json
-
-# Get project context for planning
-ktx agent context --json
-
-# List semantic sources
-ktx agent sl list --json
-
-# Search semantic sources by name
-ktx agent sl list --json --query "revenue"
-
-# Read a semantic source
-ktx agent sl read orders --json --connection-id my-warehouse
-
-# Run a semantic-layer query from a file
-ktx agent sl query --json \
- --connection-id my-warehouse \
- --query-file /tmp/query.json \
- --execute \
- --max-rows 100
-
-# Search wiki pages
-ktx agent wiki search "churn definition" --json
-
-# Read a specific wiki page
-ktx agent wiki read page-abc123 --json
-
-# Execute read-only SQL
-ktx agent sql execute --json \
- --connection-id my-warehouse \
- --sql-file /tmp/query.sql \
- --max-rows 500
-```
-
-## Output
-
-Every `ktx agent` command writes JSON to stdout and diagnostic text to stderr. Agents should parse stdout as JSON and treat a non-zero exit code as a failed tool call.
-
-```json
-{
- "ok": true,
- "data": {
- "type": "agent-response"
- }
-}
-```
-
-## Common errors
-
-| Error | Cause | Recovery |
-|-------|-------|----------|
-| Missing JSON output | `--json` was omitted | Re-run the same subcommand with `--json` |
-| Unknown connection id | The requested connection is not configured in `ktx.yaml` | Call `ktx agent context --json` or `ktx connection list` to discover valid ids |
-| Query file cannot be read | `--query-file` points to a missing or invalid JSON file | Write the query payload to a real file and pass its absolute path |
-| SQL execution rejected | SQL is not read-only or `--max-rows` is missing | Use semantic-layer queries first; for direct SQL, pass read-only SQL and an explicit row limit |
diff --git a/docs-site/content/docs/cli-reference/ktx-sl.mdx b/docs-site/content/docs/cli-reference/ktx-sl.mdx
index 4ec7bdd1..f5a31b27 100644
--- a/docs-site/content/docs/cli-reference/ktx-sl.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-sl.mdx
@@ -28,6 +28,7 @@ ktx sl [options]
| Flag | Description | Default |
|------|-------------|---------|
| `--connection-id ` | Filter by KTX connection id | — |
+| `--query ` | Search source names and descriptions | — |
| `--output ` | Output mode: `pretty` (default in TTY), `plain` (TSV), or `json` | `pretty` |
| `--json` | Shortcut for `--output=json` (overrides `--output`) | `false` |
@@ -36,6 +37,7 @@ ktx sl [options]
| Flag | Description | Default |
|------|-------------|---------|
| `--connection-id ` | KTX connection id (required) | — |
+| `--json` | Print JSON output | `false` |
### `sl validate`
@@ -55,6 +57,7 @@ ktx sl [options]
| Flag | Description | Default |
|------|-------------|---------|
| `--connection-id ` | KTX connection id | — |
+| `--query-file ` | JSON semantic-layer query file | — |
| `--measure ` | Measure to query; repeatable (at least one required) | — |
| `--dimension ` | Dimension to include; repeatable | — |
| `--filter ` | Filter expression; repeatable | — |
@@ -78,9 +81,15 @@ ktx sl list --connection-id my-warehouse
# List sources as JSON
ktx sl list --json
+# Search sources as JSON
+ktx sl list --json --query "revenue"
+
# Read a source definition
ktx sl read orders --connection-id my-warehouse
+# Read a source definition as JSON
+ktx sl read orders --connection-id my-warehouse --json
+
# Validate a source against the live schema
ktx sl validate orders --connection-id my-warehouse
@@ -119,6 +128,13 @@ ktx sl query \
--dimension orders.created_date \
--execute \
--max-rows 1000
+
+# Execute a query from a JSON file
+ktx sl query \
+ --connection-id my-warehouse \
+ --query-file query.json \
+ --execute \
+ --max-rows 100
```
## Output
diff --git a/docs-site/content/docs/cli-reference/ktx-wiki.mdx b/docs-site/content/docs/cli-reference/ktx-wiki.mdx
index a709ac07..7e45420e 100644
--- a/docs-site/content/docs/cli-reference/ktx-wiki.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-wiki.mdx
@@ -26,19 +26,23 @@ ktx wiki [options]
| Flag | Description | Default |
|------|-------------|---------|
+| `--json` | Print JSON output | `false` |
| `--user-id ` | Local user id | `local` |
### `wiki read`
| Flag | Description | Default |
|------|-------------|---------|
+| `--json` | Print JSON output | `false` |
| `--user-id ` | Local user id | `local` |
### `wiki search`
| Flag | Description | Default |
|------|-------------|---------|
+| `--json` | Print JSON output | `false` |
| `--user-id ` | Local user id | `local` |
+| `--limit ` | Maximum search results | — |
### `wiki write`
@@ -58,12 +62,21 @@ ktx wiki [options]
# List all wiki pages
ktx wiki list
+# List all wiki pages as JSON
+ktx wiki list --json
+
# Read a specific wiki page
ktx wiki read revenue-definitions
+# Read a specific wiki page as JSON
+ktx wiki read revenue-definitions --json
+
# Search wiki pages
ktx wiki search "monthly recurring revenue"
+# Search wiki pages as JSON
+ktx wiki search "monthly recurring revenue" --json --limit 10
+
# Write a global knowledge page
ktx wiki write revenue-definitions \
--summary "Canonical revenue metric definitions" \
@@ -97,13 +110,16 @@ Wiki commands print local knowledge pages and search results. Agents should sear
```json
{
- "results": [
- {
- "key": "revenue-definitions",
- "summary": "Canonical revenue metric definitions",
- "score": 0.92
- }
- ]
+ "kind": "list",
+ "data": {
+ "items": [
+ {
+ "key": "revenue-definitions",
+ "summary": "Canonical revenue metric definitions",
+ "score": 0.92
+ }
+ ]
+ }
}
```
diff --git a/docs-site/content/docs/cli-reference/meta.json b/docs-site/content/docs/cli-reference/meta.json
index a5d7a95f..bed3f98c 100644
--- a/docs-site/content/docs/cli-reference/meta.json
+++ b/docs-site/content/docs/cli-reference/meta.json
@@ -9,7 +9,6 @@
"ktx-sl",
"ktx-wiki",
"ktx-status",
- "ktx-agent",
"ktx-dev"
]
}
diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx
index 13b973e3..6aef2b14 100644
--- a/docs-site/content/docs/getting-started/quickstart.mdx
+++ b/docs-site/content/docs/getting-started/quickstart.mdx
@@ -211,7 +211,7 @@ KTX writes project state as plain files so agents can inspect and edit changes i
| `semantic-layer//*.yaml` | context build, ingestion, or `ktx sl write` | Semantic source definitions agents use for SQL generation |
| `knowledge/global/*.md` | ingestion or `ktx wiki write --scope global` | Shared business context and metric definitions |
| `knowledge/user//*.md` | `ktx wiki write --scope user` | User-scoped notes for one agent/user context |
-| `.claude/skills/ktx/SKILL.md`, `.agents/skills/ktx/SKILL.md` | CLI-mode agent integration setup | Agent instructions for calling `ktx agent` commands |
+| `.claude/skills/ktx/SKILL.md`, `.agents/skills/ktx/SKILL.md` | CLI-mode agent integration setup | Agent instructions for calling public `ktx` commands |
## Verify it worked
diff --git a/docs-site/content/docs/guides/serving-agents.mdx b/docs-site/content/docs/guides/serving-agents.mdx
index 4285611b..b6f073b8 100644
--- a/docs-site/content/docs/guides/serving-agents.mdx
+++ b/docs-site/content/docs/guides/serving-agents.mdx
@@ -3,37 +3,36 @@ title: Serving Agents
description: Expose your context to Claude Code, Cursor, Codex, and other coding agents.
---
-Once you've built and refined your context, the final step is exposing it to
-coding agents. KTX provides machine-readable CLI commands for direct terminal
-access from Claude Code, Cursor, Codex, OpenCode, and custom agent workflows.
+Once you've built and refined your context, expose it to coding agents through
+the public KTX CLI. Claude Code, Cursor, Codex, OpenCode, and custom agent
+workflows can call the same commands you use at a terminal.
## CLI Commands
-KTX provides a set of machine-readable commands under `ktx agent`. These return
-JSON output designed for programmatic consumption.
+KTX public commands support JSON output for the context reads that agents use
+most often. Use `--project-dir` when the agent is not already running inside the
+KTX project directory.
### Available commands
```bash
-# List available tools and their descriptions
-ktx agent tools --json
-
-# Get project context for planning
-ktx agent context --json
+# Check setup and context readiness
+ktx status --json
```
**Semantic layer:**
```bash
# List sources
-ktx agent sl list --json
-ktx agent sl list --json --connection-id my-postgres
+ktx sl list --json
+ktx sl list --json --connection-id my-postgres
+ktx sl list --json --query "revenue"
# Read a source
-ktx agent sl read orders --json --connection-id my-postgres
+ktx sl read orders --json --connection-id my-postgres
# Run a query from a JSON file
-ktx agent sl query --json \
+ktx sl query --json \
--connection-id my-postgres \
--query-file query.json \
--execute \
@@ -44,20 +43,10 @@ ktx agent sl query --json \
```bash
# Search knowledge pages
-ktx agent wiki search "revenue recognition" --json --limit 10
+ktx wiki search "revenue recognition" --json --limit 10
# Read a specific page
-ktx agent wiki read order-status-definitions --json
-```
-
-**SQL execution:**
-
-```bash
-# Execute read-only SQL with a row limit
-ktx agent sql execute --json \
- --connection-id my-postgres \
- --sql-file query.sql \
- --max-rows 500
+ktx wiki read order-status-definitions --json
```
## Setting Up Your Agent
diff --git a/docs-site/content/docs/integrations/agent-clients.mdx b/docs-site/content/docs/integrations/agent-clients.mdx
index 1c105e1f..8a055fda 100644
--- a/docs-site/content/docs/integrations/agent-clients.mdx
+++ b/docs-site/content/docs/integrations/agent-clients.mdx
@@ -3,7 +3,9 @@ title: Agent Clients
description: Set up KTX with Claude Code, Cursor, Codex, and OpenCode.
---
-KTX integrates with coding agents through CLI skills and command files. These files teach agents to call `ktx agent ...` commands directly from the terminal for semantic-layer context, wiki knowledge, and safe SQL execution.
+KTX integrates with coding agents through CLI skills and command files. These
+files teach agents to call public `ktx` commands directly from the terminal for
+semantic-layer context and wiki knowledge.
Run `ktx setup` and select your agent targets, or configure manually using the snippets below.
@@ -26,17 +28,17 @@ Create `.claude/skills/ktx/SKILL.md`:
```markdown title=".claude/skills/ktx/SKILL.md"
---
name: ktx
-description: Use local KTX semantic context, wiki knowledge, and safe SQL execution for this project.
+description: Use local KTX semantic context and wiki knowledge for this project.
---
Available commands:
-- `ktx agent context --json --project-dir /path/to/project`
-- `ktx agent sl list --json --project-dir /path/to/project`
-- `ktx agent sl read '' --json --project-dir /path/to/project`
-- `ktx agent sl query --json --project-dir /path/to/project --connection-id '' --query-file '' --execute --max-rows 100`
-- `ktx agent wiki search '' --json --project-dir /path/to/project`
-- `ktx agent wiki read '' --json --project-dir /path/to/project`
-- `ktx agent sql execute --json --project-dir /path/to/project --connection-id '' --sql-file '' --max-rows 100`
+- `ktx status --json --project-dir /path/to/project`
+- `ktx sl list --json --project-dir /path/to/project`
+- `ktx sl list --json --project-dir /path/to/project --query ''`
+- `ktx sl read '' --json --project-dir /path/to/project --connection-id ''`
+- `ktx sl query --json --project-dir /path/to/project --connection-id '' --query-file '' --execute --max-rows 100`
+- `ktx wiki search '' --json --project-dir /path/to/project --limit 10`
+- `ktx wiki read '' --json --project-dir /path/to/project`
```
### Workflow tips
@@ -123,22 +125,19 @@ All supported agent clients call the same KTX CLI commands:
| Command | Description |
|---------|-------------|
-| `ktx agent context --json` | Return a compact project context summary |
-| `ktx agent tools --json` | List available agent-facing commands |
-| `ktx agent wiki search --json` | Search knowledge pages |
-| `ktx agent wiki read --json` | Read a knowledge page |
-| `ktx agent wiki write --json` | Write or update a knowledge page |
-| `ktx agent sl list --json` | List semantic layer sources |
-| `ktx agent sl read --json` | Read a semantic source definition |
-| `ktx agent sl write --json` | Write or update a semantic source |
-| `ktx agent sl validate --json` | Validate semantic source definitions |
-| `ktx agent sl query --json` | Execute a semantic layer query when semantic compute is configured |
-| `ktx agent sql execute --json` | Execute read-only SQL with an explicit row limit |
+| `ktx status --json` | Return project setup and context readiness |
+| `ktx wiki search --json` | Search knowledge pages |
+| `ktx wiki read --json` | Read a knowledge page |
+| `ktx wiki write ` | Write or update a knowledge page |
+| `ktx sl list --json` | List semantic-layer sources |
+| `ktx sl list --query --json` | Search semantic-layer sources |
+| `ktx sl read --json --connection-id ` | Read a semantic source definition |
+| `ktx sl write --connection-id ` | Write or update a semantic source |
+| `ktx sl validate --connection-id ` | Validate semantic source definitions |
+| `ktx sl query --json` | Execute a semantic-layer query when semantic compute is configured |
### Security constraints
-- SQL execution is always read-only.
-- Agent SQL execution requires an explicit `--max-rows` limit from 1 to 1000.
- Secrets and credentials are never exposed in command output.
- Commands resolve the project from `--project-dir`, `KTX_PROJECT_DIR`, or the nearest `ktx.yaml`.
diff --git a/docs-site/content/docs/integrations/primary-sources.mdx b/docs-site/content/docs/integrations/primary-sources.mdx
index 49200d47..94dc4e44 100644
--- a/docs-site/content/docs/integrations/primary-sources.mdx
+++ b/docs-site/content/docs/integrations/primary-sources.mdx
@@ -511,4 +511,4 @@ No authentication required — SQLite is file-based. The file must be readable b
| Scan returns no tables | Schema/database/project filter is wrong or the user lacks metadata permissions | Verify the schema list and grant metadata read permissions |
| Historic SQL is empty | Query history extension or warehouse history view is unavailable | Enable the warehouse-specific history feature, then rerun scan or setup |
| Column statistics are missing | Connector cannot access stats tables or the warehouse does not expose them | Grant stats permissions where supported; otherwise rely on structural scan output |
-| SQL execution fails through agents | Connection is missing, unreachable, or query execution is disabled | Run `ktx connection test ` and check the agent command flags |
+| Semantic query execution fails | Connection is missing, unreachable, or query execution is disabled | Run `ktx connection test ` and check the `ktx sl query` flags |
diff --git a/docs-site/lib/llm-docs.ts b/docs-site/lib/llm-docs.ts
index 9d9b5c74..69aac698 100644
--- a/docs-site/lib/llm-docs.ts
+++ b/docs-site/lib/llm-docs.ts
@@ -67,12 +67,12 @@ ${link("/docs/guides/writing-context", "Writing Context", "Write semantic source
- [Full documentation](${absoluteUrl("/llms-full.txt")}): All docs pages in one plain-text markdown response
- [Markdown access guide](${absoluteUrl("/docs/ai-resources/markdown-access.md")}): How to fetch llms.txt, llms-full.txt, and per-page Markdown
- [Quickstart markdown](${absoluteUrl("/docs/getting-started/quickstart.md")}): Human setup walkthrough
-- [Agent CLI markdown](${absoluteUrl("/docs/cli-reference/ktx-agent.md")}): Machine-readable agent commands
+- [Semantic-layer CLI markdown](${absoluteUrl("/docs/cli-reference/ktx-sl.md")}): Semantic-layer commands and JSON output
+- [Wiki CLI markdown](${absoluteUrl("/docs/cli-reference/ktx-wiki.md")}): Knowledge page commands and JSON output
## CLI Reference
${link("/docs/cli-reference/ktx-setup", "ktx setup", "Interactive project setup")}
-${link("/docs/cli-reference/ktx-agent", "ktx agent", "Machine-readable commands for coding agents")}
${link("/docs/cli-reference/ktx-sl", "ktx sl", "Semantic-layer commands")}
${link("/docs/cli-reference/ktx-wiki", "ktx wiki", "Knowledge page commands")}
${link("/docs/cli-reference/ktx-connection", "ktx connection", "Connection management commands")}
diff --git a/packages/cli/src/agent-runtime.test.ts b/packages/cli/src/agent-runtime.test.ts
deleted file mode 100644
index 808ddac3..00000000
--- a/packages/cli/src/agent-runtime.test.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import {
- KTX_AGENT_MAX_ROWS_CAP,
- createKtxAgentRuntime,
- parseAgentMaxRows,
- readAgentJsonFile,
- writeAgentJson,
- writeAgentJsonError,
-} from './agent-runtime.js';
-
-function makeIo() {
- let stdout = '';
- let stderr = '';
- return {
- io: {
- stdout: { write: (chunk: string) => (stdout += chunk) },
- stderr: { write: (chunk: string) => (stderr += chunk) },
- },
- stdout: () => stdout,
- stderr: () => stderr,
- };
-}
-
-describe('agent runtime helpers', () => {
- let tempDir: string;
-
- beforeEach(async () => {
- tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-runtime-'));
- });
-
- afterEach(async () => {
- await rm(tempDir, { recursive: true, force: true });
- });
-
- it('writes JSON success and error envelopes without color or spinners', () => {
- const successIo = makeIo();
- const errorIo = makeIo();
-
- writeAgentJson(successIo.io, { ok: true });
- writeAgentJsonError(errorIo.io, 'missing source', { code: 'NOT_FOUND' });
-
- expect(JSON.parse(successIo.stdout())).toEqual({ ok: true });
- expect(successIo.stderr()).toBe('');
- expect(JSON.parse(errorIo.stderr())).toEqual({
- ok: false,
- error: { message: 'missing source', code: 'NOT_FOUND' },
- });
- expect(errorIo.stdout()).toBe('');
- });
-
- it('reads JSON query files as objects', async () => {
- const path = join(tempDir, 'query.json');
- await writeFile(path, '{"measures":["revenue"],"limit":50}', 'utf-8');
-
- await expect(readAgentJsonFile(path)).resolves.toEqual({ measures: ['revenue'], limit: 50 });
- });
-
- it('rejects non-object JSON query files', async () => {
- const path = join(tempDir, 'query.json');
- await writeFile(path, '["revenue"]', 'utf-8');
-
- await expect(readAgentJsonFile(path)).rejects.toThrow('must contain a JSON object');
- });
-
- it('requires positive row limits and enforces the agent cap', () => {
- expect(parseAgentMaxRows(100)).toBe(100);
- expect(() => parseAgentMaxRows(undefined)).toThrow('maxRows is required');
- expect(() => parseAgentMaxRows(0)).toThrow('positive integer');
- expect(() => parseAgentMaxRows(KTX_AGENT_MAX_ROWS_CAP + 1)).toThrow(String(KTX_AGENT_MAX_ROWS_CAP));
- });
-
- it('constructs local context ports with semantic compute and query executor', async () => {
- const project = {
- projectDir: tempDir,
- configPath: join(tempDir, 'ktx.yaml'),
- config: { project: 'revenue', connections: {} },
- coreConfig: {},
- git: {},
- fileStore: {},
- } as never;
- const ports = { knowledge: {}, semanticLayer: {} } as never;
- const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
- const queryExecutor = { execute: vi.fn() };
- const loadProject = vi.fn(async () => project);
- const createContextTools = vi.fn(() => ports);
-
- await expect(
- createKtxAgentRuntime(
- { projectDir: tempDir, enableSemanticCompute: true, enableQueryExecution: true },
- {
- loadProject,
- createContextTools,
- createSemanticLayerCompute: () => semanticLayerCompute,
- createQueryExecutor: () => queryExecutor,
- },
- ),
- ).resolves.toMatchObject({ project, ports, queryExecutor });
-
- expect(loadProject).toHaveBeenCalledWith({ projectDir: tempDir });
- expect(createContextTools).toHaveBeenCalledWith(project, {
- semanticLayerCompute,
- queryExecutor,
- });
- });
-
- it('creates managed semantic compute when no test override is injected', async () => {
- const project = {
- projectDir: tempDir,
- configPath: join(tempDir, 'ktx.yaml'),
- config: { project: 'revenue', connections: {} },
- coreConfig: {},
- git: {},
- fileStore: {},
- } as never;
- const ports = { semanticLayer: {} } as never;
- const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
- const loadProject = vi.fn(async () => project);
- const createContextTools = vi.fn(() => ports);
- const createManagedSemanticLayerCompute = vi.fn(async () => semanticLayerCompute);
- const { io } = makeIo();
-
- await expect(
- createKtxAgentRuntime(
- {
- projectDir: tempDir,
- enableSemanticCompute: true,
- enableQueryExecution: false,
- cliVersion: '0.2.0',
- runtimeInstallPolicy: 'auto',
- io,
- },
- {
- loadProject,
- createContextTools,
- createManagedSemanticLayerCompute,
- },
- ),
- ).resolves.toMatchObject({ project, ports, semanticLayerCompute });
-
- expect(createManagedSemanticLayerCompute).toHaveBeenCalledWith({
- cliVersion: '0.2.0',
- installPolicy: 'auto',
- io,
- });
- expect(createContextTools).toHaveBeenCalledWith(project, {
- semanticLayerCompute,
- });
- });
-});
diff --git a/packages/cli/src/agent-runtime.ts b/packages/cli/src/agent-runtime.ts
deleted file mode 100644
index feccae7c..00000000
--- a/packages/cli/src/agent-runtime.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { readFile } from 'node:fs/promises';
-import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
-import type { KtxSemanticLayerComputePort } from '@ktx/context/daemon';
-import { createLocalProjectMcpContextPorts, type KtxMcpContextPorts } from '@ktx/context/mcp';
-import { type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
-import type { KtxCliIo } from './cli-runtime.js';
-import {
- createManagedPythonSemanticLayerComputePort,
- type KtxManagedPythonInstallPolicy,
-} from './managed-python-command.js';
-
-export const KTX_AGENT_MAX_ROWS_CAP = 1000;
-
-export interface KtxAgentRuntimeOptions {
- projectDir: string;
- enableSemanticCompute: boolean;
- enableQueryExecution: boolean;
- cliVersion?: string;
- runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
- io?: KtxCliIo;
-}
-
-export interface KtxAgentRuntime {
- project: KtxLocalProject;
- ports: KtxMcpContextPorts;
- semanticLayerCompute?: KtxSemanticLayerComputePort;
- queryExecutor?: KtxSqlQueryExecutorPort;
-}
-
-export interface KtxAgentRuntimeDeps {
- loadProject?: typeof loadKtxProject;
- createContextTools?: typeof createLocalProjectMcpContextPorts;
- createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
- createManagedSemanticLayerCompute?: typeof createManagedPythonSemanticLayerComputePort;
- createQueryExecutor?: () => KtxSqlQueryExecutorPort;
-}
-
-export function writeAgentJson(io: KtxCliIo, value: unknown): void {
- io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
-}
-
-export function writeAgentJsonError(
- io: KtxCliIo,
- message: string,
- detail: Record = {},
-): void {
- io.stderr.write(`${JSON.stringify({ ok: false, error: { message, ...detail } }, null, 2)}\n`);
-}
-
-export async function readAgentJsonFile(path: string): Promise> {
- const parsed = JSON.parse(await readFile(path, 'utf-8')) as unknown;
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
- throw new Error(`${path} must contain a JSON object.`);
- }
- return parsed as Record;
-}
-
-export function parseAgentMaxRows(value: number | undefined): number {
- if (!Number.isInteger(value) || value === undefined || value <= 0) {
- throw new Error('maxRows is required and must be a positive integer.');
- }
- if (value > KTX_AGENT_MAX_ROWS_CAP) {
- throw new Error(`maxRows must be less than or equal to ${KTX_AGENT_MAX_ROWS_CAP}.`);
- }
- return value;
-}
-
-async function createAgentSemanticLayerCompute(
- options: KtxAgentRuntimeOptions,
- deps: KtxAgentRuntimeDeps,
-): Promise {
- if (!options.enableSemanticCompute) {
- return undefined;
- }
- if (deps.createSemanticLayerCompute) {
- return deps.createSemanticLayerCompute();
- }
- if (!options.cliVersion || !options.runtimeInstallPolicy || !options.io) {
- throw new Error('Managed Python semantic compute requires cliVersion, runtimeInstallPolicy, and io.');
- }
- const createManagedSemanticLayerCompute =
- deps.createManagedSemanticLayerCompute ?? createManagedPythonSemanticLayerComputePort;
- return createManagedSemanticLayerCompute({
- cliVersion: options.cliVersion,
- installPolicy: options.runtimeInstallPolicy,
- io: options.io,
- });
-}
-
-export async function createKtxAgentRuntime(
- options: KtxAgentRuntimeOptions,
- deps: KtxAgentRuntimeDeps = {},
-): Promise {
- const project = await (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
- const semanticLayerCompute = await createAgentSemanticLayerCompute(options, deps);
- const queryExecutor = options.enableQueryExecution
- ? (deps.createQueryExecutor ?? createDefaultLocalQueryExecutor)()
- : undefined;
- const ports = (deps.createContextTools ?? createLocalProjectMcpContextPorts)(project, {
- ...(semanticLayerCompute ? { semanticLayerCompute } : {}),
- ...(queryExecutor ? { queryExecutor } : {}),
- });
- return {
- project,
- ports,
- ...(semanticLayerCompute ? { semanticLayerCompute } : {}),
- ...(queryExecutor ? { queryExecutor } : {}),
- };
-}
diff --git a/packages/cli/src/agent-search-readiness.test.ts b/packages/cli/src/agent-search-readiness.test.ts
deleted file mode 100644
index 432afa90..00000000
--- a/packages/cli/src/agent-search-readiness.test.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import {
- isMissingProjectConfigError,
- missingConnectionSlSearchReadiness,
- missingProjectSlSearchReadiness,
- noConnectionsSlSearchReadiness,
- noIndexedSourcesSlSearchReadiness,
-} from './agent-search-readiness.js';
-
-describe('agent semantic-layer search readiness guidance', () => {
- it('formats missing project guidance with exact recovery commands', () => {
- expect(missingProjectSlSearchReadiness('/tmp/ktx-search', 'gross revenue')).toEqual({
- code: 'agent_sl_search_missing_project',
- message: 'Semantic-layer search needs an initialized KTX project at /tmp/ktx-search.',
- nextSteps: [
- 'ktx setup --project-dir /tmp/ktx-search',
- 'ktx status --project-dir /tmp/ktx-search',
- 'ktx ingest run --connection-id --adapter ',
- 'ktx agent sl list --json --query "gross revenue" --project-dir /tmp/ktx-search',
- ],
- });
- });
-
- it('formats no-connection and no-index guidance without hiding the project path', () => {
- expect(noConnectionsSlSearchReadiness('/tmp/ktx-search', 'revenue')).toMatchObject({
- code: 'agent_sl_search_no_connections',
- message: 'Semantic-layer search found no configured connections in /tmp/ktx-search.',
- });
- expect(noIndexedSourcesSlSearchReadiness('/tmp/ktx-search', 'orders')).toMatchObject({
- code: 'agent_sl_search_no_indexed_sources',
- message: 'Semantic-layer search found no indexed semantic-layer sources in /tmp/ktx-search.',
- });
- });
-
- it('formats unknown connection guidance', () => {
- expect(missingConnectionSlSearchReadiness('/tmp/ktx-search', 'warehouse', 'revenue')).toMatchObject({
- code: 'agent_sl_search_unknown_connection',
- message: 'Semantic-layer search connection "warehouse" is not configured in /tmp/ktx-search.',
- });
- });
-
- it('detects missing ktx.yaml read errors', () => {
- const error = Object.assign(new Error('ENOENT: no such file or directory'), {
- code: 'ENOENT',
- path: '/tmp/ktx-search/ktx.yaml',
- });
-
- expect(isMissingProjectConfigError(error)).toBe(true);
- expect(isMissingProjectConfigError(new Error('other'))).toBe(false);
- });
-});
diff --git a/packages/cli/src/agent-search-readiness.ts b/packages/cli/src/agent-search-readiness.ts
deleted file mode 100644
index e4de7318..00000000
--- a/packages/cli/src/agent-search-readiness.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-export type KtxAgentSlSearchReadinessCode =
- | 'agent_sl_search_missing_project'
- | 'agent_sl_search_no_connections'
- | 'agent_sl_search_unknown_connection'
- | 'agent_sl_search_no_indexed_sources';
-
-export interface KtxAgentSlSearchReadinessDetail {
- code: KtxAgentSlSearchReadinessCode;
- message: string;
- nextSteps: string[];
-}
-
-function queryForCommand(query: string | undefined): string {
- const trimmed = query?.trim();
- return trimmed && trimmed.length > 0 ? trimmed : 'revenue';
-}
-
-function projectSearchCommand(projectDir: string, query: string | undefined): string {
- return `ktx agent sl list --json --query ${JSON.stringify(queryForCommand(query))} --project-dir ${projectDir}`;
-}
-
-function baseNextSteps(projectDir: string, query: string | undefined): string[] {
- return [
- `ktx setup --project-dir ${projectDir}`,
- `ktx status --project-dir ${projectDir}`,
- 'ktx ingest run --connection-id --adapter ',
- projectSearchCommand(projectDir, query),
- ];
-}
-
-export function missingProjectSlSearchReadiness(
- projectDir: string,
- query: string | undefined,
-): KtxAgentSlSearchReadinessDetail {
- return {
- code: 'agent_sl_search_missing_project',
- message: `Semantic-layer search needs an initialized KTX project at ${projectDir}.`,
- nextSteps: baseNextSteps(projectDir, query),
- };
-}
-
-export function noConnectionsSlSearchReadiness(
- projectDir: string,
- query: string | undefined,
-): KtxAgentSlSearchReadinessDetail {
- return {
- code: 'agent_sl_search_no_connections',
- message: `Semantic-layer search found no configured connections in ${projectDir}.`,
- nextSteps: baseNextSteps(projectDir, query),
- };
-}
-
-export function missingConnectionSlSearchReadiness(
- projectDir: string,
- connectionId: string,
- query: string | undefined,
-): KtxAgentSlSearchReadinessDetail {
- return {
- code: 'agent_sl_search_unknown_connection',
- message: `Semantic-layer search connection "${connectionId}" is not configured in ${projectDir}.`,
- nextSteps: baseNextSteps(projectDir, query),
- };
-}
-
-export function noIndexedSourcesSlSearchReadiness(
- projectDir: string,
- query: string | undefined,
-): KtxAgentSlSearchReadinessDetail {
- return {
- code: 'agent_sl_search_no_indexed_sources',
- message: `Semantic-layer search found no indexed semantic-layer sources in ${projectDir}.`,
- nextSteps: baseNextSteps(projectDir, query),
- };
-}
-
-function errorCode(error: unknown): string | undefined {
- if (typeof error !== 'object' || error === null || !('code' in error)) {
- return undefined;
- }
- const code = (error as { code?: unknown }).code;
- return typeof code === 'string' ? code : undefined;
-}
-
-function errorPath(error: unknown): string | undefined {
- if (typeof error !== 'object' || error === null || !('path' in error)) {
- return undefined;
- }
- const path = (error as { path?: unknown }).path;
- return typeof path === 'string' ? path : undefined;
-}
-
-export function isMissingProjectConfigError(error: unknown): boolean {
- return errorCode(error) === 'ENOENT' && (errorPath(error)?.endsWith('ktx.yaml') ?? false);
-}
diff --git a/packages/cli/src/agent.test.ts b/packages/cli/src/agent.test.ts
deleted file mode 100644
index 566f5763..00000000
--- a/packages/cli/src/agent.test.ts
+++ /dev/null
@@ -1,428 +0,0 @@
-import { mkdtemp, rm, writeFile } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { buildDefaultKtxProjectConfig } from '@ktx/context/project';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { runKtxAgent } from './agent.js';
-import type { KtxAgentRuntime } from './agent-runtime.js';
-
-function makeIo() {
- let stdout = '';
- let stderr = '';
- return {
- io: {
- stdout: { write: (chunk: string) => (stdout += chunk) },
- stderr: { write: (chunk: string) => (stderr += chunk) },
- },
- stdout: () => stdout,
- stderr: () => stderr,
- };
-}
-
-function runtime(overrides: Record = {}): KtxAgentRuntime {
- const config = buildDefaultKtxProjectConfig('revenue');
- return {
- project: {
- projectDir: '/tmp/revenue',
- configPath: '/tmp/revenue/ktx.yaml',
- config: {
- ...config,
- connections: {
- warehouse: { driver: 'sqlite', path: 'warehouse.sqlite', readonly: true as const },
- },
- },
- coreConfig: {} as KtxAgentRuntime['project']['coreConfig'],
- git: {} as KtxAgentRuntime['project']['git'],
- fileStore: {} as KtxAgentRuntime['project']['fileStore'],
- },
- ports: {
- connections: { list: vi.fn(async () => [{ id: 'warehouse', name: 'warehouse', connectionType: 'sqlite' }]) },
- semanticLayer: {
- listSources: vi.fn(async () => ({
- sources: [
- {
- connectionId: 'warehouse',
- connectionName: 'warehouse',
- name: 'orders',
- columnCount: 2,
- measureCount: 1,
- joinCount: 0,
- },
- ],
- totalSources: 1,
- })),
- readSource: vi.fn(async () => ({ sourceName: 'orders', yaml: 'name: orders\n' })),
- writeSource: vi.fn(async () => ({ success: true, sourceName: 'orders' })),
- validate: vi.fn(async () => ({ success: true, errors: [], warnings: [] })),
- query: vi.fn(async () => ({ sql: 'select 1', headers: ['x'], rows: [[1]], totalRows: 1, plan: {} })),
- },
- knowledge: {
- search: vi.fn(async () => ({
- results: [
- {
- key: 'page-1',
- path: 'knowledge/global/page-1.md',
- scope: 'GLOBAL' as const,
- summary: 'Revenue logic',
- score: 0.9,
- matchReasons: ['lexical' as const],
- },
- ],
- totalFound: 1,
- })),
- read: vi.fn(async () => ({
- key: 'page-1',
- scope: 'GLOBAL' as const,
- summary: 'Revenue logic',
- content: 'Use net revenue.',
- })),
- write: vi.fn(async () => ({ success: true, key: 'page-1', action: 'created' as const })),
- },
- },
- queryExecutor: {
- execute: vi.fn(async () => ({ headers: ['x'], rows: [[1]], totalRows: 1, command: 'SELECT', rowCount: 1 })),
- },
- ...overrides,
- };
-}
-
-function runtimeWithoutConnections(): KtxAgentRuntime {
- const base = runtime();
- return {
- ...base,
- project: {
- ...base.project,
- config: {
- ...base.project.config,
- connections: {},
- },
- },
- ports: {
- ...base.ports,
- semanticLayer: {
- ...base.ports.semanticLayer!,
- listSources: vi.fn(async () => ({ sources: [], totalSources: 0 })),
- },
- },
- };
-}
-
-describe('runKtxAgent', () => {
- let tempDir: string;
-
- beforeEach(async () => {
- tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-'));
- });
-
- afterEach(async () => {
- await rm(tempDir, { recursive: true, force: true });
- });
-
- it('prints tool discovery with every stable command', async () => {
- const io = makeIo();
-
- await expect(runKtxAgent({ command: 'tools', projectDir: tempDir, json: true }, io.io)).resolves.toBe(0);
-
- const body = JSON.parse(io.stdout());
- expect(body.projectDir).toBe(tempDir);
- expect(body.tools.map((tool: { name: string }) => tool.name)).toEqual([
- 'context',
- 'sl.list',
- 'sl.read',
- 'sl.query',
- 'wiki.search',
- 'wiki.read',
- 'sql.execute',
- ]);
- expect(io.stderr()).toBe('');
- });
-
- it('prints project context from setup status, connections, and SL summaries', async () => {
- const io = makeIo();
- const createRuntime = vi.fn(async () => runtime());
- const readSetupStatus = vi.fn(async () => ({ project: { path: tempDir, ready: true }, agents: [] }));
-
- await expect(
- runKtxAgent({ command: 'context', projectDir: tempDir, json: true }, io.io, { createRuntime, readSetupStatus }),
- ).resolves.toBe(0);
-
- expect(JSON.parse(io.stdout())).toMatchObject({
- projectDir: tempDir,
- status: { project: { ready: true } },
- connections: [{ id: 'warehouse' }],
- semanticLayer: { totalSources: 1 },
- });
- });
-
- it('dispatches SL list, SL read, wiki search, and wiki read through local ports', async () => {
- for (const args of [
- { command: 'sl-list' as const, projectDir: tempDir, json: true as const, connectionId: 'warehouse' },
- {
- command: 'sl-read' as const,
- projectDir: tempDir,
- json: true as const,
- connectionId: 'warehouse',
- sourceName: 'orders',
- },
- { command: 'wiki-search' as const, projectDir: tempDir, json: true as const, query: 'revenue', limit: 10 },
- { command: 'wiki-read' as const, projectDir: tempDir, json: true as const, pageId: 'page-1' },
- ]) {
- const io = makeIo();
- await expect(runKtxAgent(args, io.io, { createRuntime: async () => runtime() })).resolves.toBe(0);
- expect(JSON.parse(io.stdout())).toBeTruthy();
- expect(io.stderr()).toBe('');
- }
- });
-
- it('prints wiki hybrid search metadata from the hidden agent wiki search command', async () => {
- const fakeRuntime = runtime();
- const knowledge = fakeRuntime.ports.knowledge;
- if (!knowledge) {
- throw new Error('Expected runtime knowledge port');
- }
- fakeRuntime.ports.knowledge = {
- ...knowledge,
- search: vi.fn(async () => ({
- results: [
- {
- key: 'metrics-revenue',
- path: 'knowledge/global/metrics-revenue.md',
- scope: 'GLOBAL' as const,
- summary: 'Revenue metric definition',
- score: 0.02459016393442623,
- matchReasons: ['lexical' as const, 'token' as const],
- },
- ],
- totalFound: 1,
- })),
- };
- const io = makeIo();
-
- await expect(
- runKtxAgent({ command: 'wiki-search', projectDir: tempDir, json: true, query: 'paid order', limit: 5 }, io.io, {
- createRuntime: async () => fakeRuntime,
- }),
- ).resolves.toBe(0);
-
- expect(JSON.parse(io.stdout())).toEqual({
- results: [
- expect.objectContaining({
- key: 'metrics-revenue',
- path: 'knowledge/global/metrics-revenue.md',
- matchReasons: ['lexical', 'token'],
- }),
- ],
- totalFound: 1,
- });
- });
-
- it('executes SL queries from a JSON query file', async () => {
- const queryFile = join(tempDir, 'sl-query.json');
- const io = makeIo();
- await writeFile(queryFile, '{"measures":["total_revenue"],"dimensions":[]}', 'utf-8');
-
- await expect(
- runKtxAgent(
- {
- command: 'sl-query',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- queryFile,
- execute: true,
- maxRows: 100,
- cliVersion: '0.2.0',
- runtimeInstallPolicy: 'never',
- },
- io.io,
- { createRuntime: async () => runtime() },
- ),
- ).resolves.toBe(0);
-
- expect(JSON.parse(io.stdout())).toMatchObject({ sql: 'select 1', rows: [[1]] });
- });
-
- it('passes managed runtime options into default SL query runtime creation', async () => {
- const queryFile = join(tempDir, 'sl-query.json');
- const io = makeIo();
- const createRuntime = vi.fn(async () => runtime());
- await writeFile(queryFile, '{"measures":["total_revenue"],"dimensions":[]}', 'utf-8');
-
- await expect(
- runKtxAgent(
- {
- command: 'sl-query',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- queryFile,
- execute: false,
- cliVersion: '0.2.0',
- runtimeInstallPolicy: 'auto',
- },
- io.io,
- { createRuntime },
- ),
- ).resolves.toBe(0);
-
- expect(createRuntime).toHaveBeenCalledWith({
- projectDir: tempDir,
- enableSemanticCompute: true,
- enableQueryExecution: false,
- cliVersion: '0.2.0',
- runtimeInstallPolicy: 'auto',
- io: io.io,
- });
- });
-
- it('executes read-only SQL from a SQL file with an explicit row limit', async () => {
- const sqlFile = join(tempDir, 'query.sql');
- const fakeRuntime = runtime();
- const io = makeIo();
- await writeFile(sqlFile, 'select 1', 'utf-8');
-
- await expect(
- runKtxAgent(
- {
- command: 'sql-execute',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- sqlFile,
- maxRows: 100,
- },
- io.io,
- { createRuntime: async () => fakeRuntime as never },
- ),
- ).resolves.toBe(0);
-
- expect(fakeRuntime.queryExecutor?.execute).toHaveBeenCalledWith({
- connectionId: 'warehouse',
- projectDir: '/tmp/revenue',
- connection: { driver: 'sqlite', path: 'warehouse.sqlite', readonly: true },
- sql: 'select 1',
- maxRows: 100,
- });
- });
-
- it('prints guided JSON when semantic-layer search runs outside a project', async () => {
- const io = makeIo();
- const missingProjectError = Object.assign(new Error('ENOENT: no such file or directory'), {
- code: 'ENOENT',
- path: join(tempDir, 'ktx.yaml'),
- });
-
- await expect(
- runKtxAgent(
- { command: 'sl-list', projectDir: tempDir, json: true, query: 'gross revenue' },
- io.io,
- { createRuntime: vi.fn(async () => Promise.reject(missingProjectError)) },
- ),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toEqual({
- ok: false,
- error: {
- code: 'agent_sl_search_missing_project',
- message: `Semantic-layer search needs an initialized KTX project at ${tempDir}.`,
- nextSteps: [
- `ktx setup --project-dir ${tempDir}`,
- `ktx status --project-dir ${tempDir}`,
- 'ktx ingest run --connection-id --adapter ',
- `ktx agent sl list --json --query "gross revenue" --project-dir ${tempDir}`,
- ],
- },
- });
- expect(io.stdout()).toBe('');
- });
-
- it('prints guided JSON when semantic-layer search has no configured connections', async () => {
- const io = makeIo();
-
- await expect(
- runKtxAgent(
- { command: 'sl-list', projectDir: tempDir, json: true, query: 'revenue' },
- io.io,
- { createRuntime: async () => runtimeWithoutConnections() },
- ),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toMatchObject({
- ok: false,
- error: {
- code: 'agent_sl_search_no_connections',
- message: `Semantic-layer search found no configured connections in ${tempDir}.`,
- nextSteps: [
- `ktx setup --project-dir ${tempDir}`,
- `ktx status --project-dir ${tempDir}`,
- 'ktx ingest run --connection-id --adapter ',
- `ktx agent sl list --json --query "revenue" --project-dir ${tempDir}`,
- ],
- },
- });
- });
-
- it('prints guided JSON when semantic-layer search asks for an unknown connection', async () => {
- const io = makeIo();
-
- await expect(
- runKtxAgent(
- { command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'missing', query: 'revenue' },
- io.io,
- { createRuntime: async () => runtime() },
- ),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toMatchObject({
- ok: false,
- error: {
- code: 'agent_sl_search_unknown_connection',
- message: `Semantic-layer search connection "missing" is not configured in ${tempDir}.`,
- },
- });
- });
-
- it('prints guided JSON when semantic-layer search has no indexed sources', async () => {
- const fakeRuntime = runtime();
- const semanticLayer = fakeRuntime.ports.semanticLayer!;
- fakeRuntime.ports.semanticLayer = {
- ...semanticLayer,
- listSources: vi.fn(async () => ({ sources: [], totalSources: 0 })),
- };
- const io = makeIo();
-
- await expect(
- runKtxAgent(
- { command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'warehouse', query: 'revenue' },
- io.io,
- { createRuntime: async () => fakeRuntime },
- ),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toMatchObject({
- ok: false,
- error: {
- code: 'agent_sl_search_no_indexed_sources',
- message: `Semantic-layer search found no indexed semantic-layer sources in ${tempDir}.`,
- },
- });
- });
-
- it('returns JSON errors when required ports or records are missing', async () => {
- const io = makeIo();
-
- await expect(
- runKtxAgent({ command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'missing' }, io.io, {
- createRuntime: async () =>
- runtime({
- ports: { knowledge: { read: vi.fn(async () => null) } },
- }) as never,
- }),
- ).resolves.toBe(1);
-
- expect(JSON.parse(io.stderr())).toMatchObject({
- ok: false,
- error: { message: expect.stringContaining('missing') },
- });
- });
-});
diff --git a/packages/cli/src/agent.ts b/packages/cli/src/agent.ts
deleted file mode 100644
index 61d85b8c..00000000
--- a/packages/cli/src/agent.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-import { readFile } from 'node:fs/promises';
-import type { KtxCliIo } from './cli-runtime.js';
-import {
- createKtxAgentRuntime,
- parseAgentMaxRows,
- readAgentJsonFile,
- writeAgentJson,
- writeAgentJsonError,
- type KtxAgentRuntime,
- type KtxAgentRuntimeDeps,
-} from './agent-runtime.js';
-import {
- isMissingProjectConfigError,
- missingConnectionSlSearchReadiness,
- missingProjectSlSearchReadiness,
- noConnectionsSlSearchReadiness,
- noIndexedSourcesSlSearchReadiness,
- type KtxAgentSlSearchReadinessDetail,
-} from './agent-search-readiness.js';
-import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
-import { readKtxSetupStatus, type KtxSetupStatus } from './setup.js';
-
-export type KtxAgentArgs =
- | { command: 'tools'; projectDir: string; json: true }
- | { command: 'context'; projectDir: string; json: true }
- | { command: 'sl-list'; projectDir: string; json: true; connectionId?: string; query?: string }
- | { command: 'sl-read'; projectDir: string; json: true; connectionId?: string; sourceName: string }
- | {
- command: 'sl-query';
- projectDir: string;
- json: true;
- connectionId: string;
- queryFile: string;
- execute: boolean;
- maxRows?: number;
- cliVersion: string;
- runtimeInstallPolicy: KtxManagedPythonInstallPolicy;
- }
- | { command: 'wiki-search'; projectDir: string; json: true; query: string; limit: number }
- | { command: 'wiki-read'; projectDir: string; json: true; pageId: string }
- | { command: 'sql-execute'; projectDir: string; json: true; connectionId: string; sqlFile: string; maxRows?: number };
-
-export interface KtxAgentDeps extends KtxAgentRuntimeDeps {
- createRuntime?: (options: {
- projectDir: string;
- enableSemanticCompute: boolean;
- enableQueryExecution: boolean;
- cliVersion?: string;
- runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
- io?: KtxCliIo;
- }) => Promise;
- readSetupStatus?: (
- projectDir: string,
- ) => Promise;
-}
-
-const AGENT_TOOLS = [
- { name: 'context', command: 'ktx agent context --json' },
- { name: 'sl.list', command: 'ktx agent sl list --json [--connection-id ] [--query ]' },
- { name: 'sl.read', command: 'ktx agent sl read --json [--connection-id ]' },
- {
- name: 'sl.query',
- command: 'ktx agent sl query --json --connection-id --query-file --execute --max-rows 100',
- },
- { name: 'wiki.search', command: 'ktx agent wiki search --json [--limit 10]' },
- { name: 'wiki.read', command: 'ktx agent wiki read --json' },
- {
- name: 'sql.execute',
- command: 'ktx agent sql execute --json --connection-id --sql-file --max-rows 100',
- },
-] as const;
-
-function writeAgentSlSearchReadinessError(io: KtxCliIo, detail: KtxAgentSlSearchReadinessDetail): void {
- writeAgentJsonError(io, detail.message, { code: detail.code, nextSteps: detail.nextSteps });
-}
-
-async function runtimeFor(args: KtxAgentArgs, deps: KtxAgentDeps, io: KtxCliIo): Promise {
- const needsSemanticCompute = args.command === 'sl-query';
- const needsQueryExecution = args.command === 'sql-execute' || (args.command === 'sl-query' && args.execute);
- const runtimeOptions = {
- projectDir: args.projectDir,
- enableSemanticCompute: needsSemanticCompute,
- enableQueryExecution: needsQueryExecution,
- ...(args.command === 'sl-query'
- ? {
- cliVersion: args.cliVersion,
- runtimeInstallPolicy: args.runtimeInstallPolicy,
- io,
- }
- : {}),
- };
- return deps.createRuntime ? deps.createRuntime(runtimeOptions) : createKtxAgentRuntime(runtimeOptions, deps);
-}
-
-function connectionIdForSource(runtime: KtxAgentRuntime, requested: string | undefined): string {
- if (requested) return requested;
- const ids = Object.keys(runtime.project.config.connections ?? {});
- if (ids.length === 1) return ids[0] as string;
- throw new Error('Use --connection-id when the project has zero or multiple connections.');
-}
-
-export async function runKtxAgent(args: KtxAgentArgs, io: KtxCliIo, deps: KtxAgentDeps = {}): Promise {
- try {
- if (args.command === 'tools') {
- writeAgentJson(io, { projectDir: args.projectDir, tools: AGENT_TOOLS });
- return 0;
- }
-
- const runtime = await runtimeFor(args, deps, io);
-
- if (args.command === 'context') {
- const [status, connections, semanticLayer] = await Promise.all([
- (deps.readSetupStatus ?? readKtxSetupStatus)(args.projectDir),
- runtime.ports.connections?.list() ?? [],
- runtime.ports.semanticLayer?.listSources({}) ?? { sources: [], totalSources: 0 },
- ]);
- writeAgentJson(io, { projectDir: args.projectDir, status, connections, semanticLayer, tools: AGENT_TOOLS });
- return 0;
- }
-
- if (args.command === 'sl-list') {
- const semanticLayer = runtime.ports.semanticLayer;
- if (!semanticLayer) throw new Error('Semantic-layer tools are not available for this project.');
- if (args.query) {
- const connectionIds = Object.keys(runtime.project.config.connections ?? {});
- if (args.connectionId && !runtime.project.config.connections[args.connectionId]) {
- writeAgentSlSearchReadinessError(
- io,
- missingConnectionSlSearchReadiness(args.projectDir, args.connectionId, args.query),
- );
- return 1;
- }
- if (connectionIds.length === 0) {
- writeAgentSlSearchReadinessError(io, noConnectionsSlSearchReadiness(args.projectDir, args.query));
- return 1;
- }
- }
-
- const listed = await semanticLayer.listSources({ connectionId: args.connectionId, query: args.query });
- if (args.query && listed.sources.length === 0) {
- const allSources = await semanticLayer.listSources({ connectionId: args.connectionId });
- if (allSources.totalSources === 0) {
- writeAgentSlSearchReadinessError(io, noIndexedSourcesSlSearchReadiness(args.projectDir, args.query));
- return 1;
- }
- }
-
- writeAgentJson(io, listed);
- return 0;
- }
-
- if (args.command === 'sl-read') {
- const semanticLayer = runtime.ports.semanticLayer;
- if (!semanticLayer) throw new Error('Semantic-layer tools are not available for this project.');
- const source = await semanticLayer.readSource({
- connectionId: connectionIdForSource(runtime, args.connectionId),
- sourceName: args.sourceName,
- });
- if (!source) throw new Error(`Semantic-layer source "${args.sourceName}" was not found.`);
- writeAgentJson(io, source);
- return 0;
- }
-
- if (args.command === 'sl-query') {
- const semanticLayer = runtime.ports.semanticLayer;
- if (!semanticLayer) throw new Error('Semantic-layer tools are not available for this project.');
- const query = await readAgentJsonFile(args.queryFile);
- const maxRows = args.execute ? parseAgentMaxRows(args.maxRows) : args.maxRows;
- writeAgentJson(
- io,
- await semanticLayer.query({
- connectionId: args.connectionId,
- query: { ...query, ...(maxRows !== undefined ? { limit: maxRows } : {}) } as never,
- }),
- );
- return 0;
- }
-
- if (args.command === 'wiki-search') {
- const knowledge = runtime.ports.knowledge;
- if (!knowledge) throw new Error('Wiki tools are not available for this project.');
- writeAgentJson(io, await knowledge.search({ userId: 'agent', query: args.query, limit: args.limit }));
- return 0;
- }
-
- if (args.command === 'wiki-read') {
- const knowledge = runtime.ports.knowledge;
- if (!knowledge) throw new Error('Wiki tools are not available for this project.');
- const page = await knowledge.read({ userId: 'agent', key: args.pageId });
- if (!page) throw new Error(`Wiki page "${args.pageId}" was not found.`);
- writeAgentJson(io, page);
- return 0;
- }
-
- const queryExecutor = runtime.queryExecutor;
- if (!queryExecutor) throw new Error('SQL execution is not available for this project.');
- const connection = runtime.project.config.connections[args.connectionId];
- if (!connection) throw new Error(`Connection "${args.connectionId}" was not found.`);
- const maxRows = parseAgentMaxRows(args.maxRows);
- writeAgentJson(
- io,
- await queryExecutor.execute({
- connectionId: args.connectionId,
- projectDir: runtime.project.projectDir,
- connection,
- sql: await readFile(args.sqlFile, 'utf-8'),
- maxRows,
- }),
- );
- return 0;
- } catch (error) {
- if (args.command === 'sl-list' && args.query && isMissingProjectConfigError(error)) {
- writeAgentSlSearchReadinessError(io, missingProjectSlSearchReadiness(args.projectDir, args.query));
- return 1;
- }
- writeAgentJsonError(io, error instanceof Error ? error.message : String(error));
- return 1;
- }
-}
diff --git a/packages/cli/src/cli-program.ts b/packages/cli/src/cli-program.ts
index 682c027a..7d6a98f3 100644
--- a/packages/cli/src/cli-program.ts
+++ b/packages/cli/src/cli-program.ts
@@ -1,6 +1,5 @@
import { Command, InvalidArgumentError } from '@commander-js/extra-typings';
import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js';
-import { registerAgentCommands } from './commands/agent-commands.js';
import { registerConnectionCommands } from './commands/connection-commands.js';
import { registerIngestCommands } from './commands/ingest-commands.js';
import { registerWikiCommands } from './commands/knowledge-commands.js';
@@ -321,7 +320,6 @@ export function buildKtxProgram(options: BuildKtxProgramOptions): Command {
registerWikiCommands(program, context);
registerSlCommands(program, context);
registerStatusCommands(program, context);
- registerAgentCommands(program, context);
registerDevCommands(program, context);
return program;
diff --git a/packages/cli/src/cli-runtime.ts b/packages/cli/src/cli-runtime.ts
index 8fc06589..f303309a 100644
--- a/packages/cli/src/cli-runtime.ts
+++ b/packages/cli/src/cli-runtime.ts
@@ -2,7 +2,6 @@ import { createRequire } from 'node:module';
import type { KtxConnectionMetabaseSetupArgs } from './commands/connection-metabase-setup.js';
import type { KtxConnectionNotionArgs } from './commands/connection-notion.js';
-import type { KtxAgentArgs } from './agent.js';
import type { KtxConnectionArgs } from './connection.js';
import type { KtxDoctorArgs } from './doctor.js';
import type { KtxIngestArgs } from './ingest.js';
@@ -30,7 +29,6 @@ export interface KtxCliIo {
export interface KtxCliDeps {
setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise;
- agent?: (args: KtxAgentArgs, io: KtxCliIo) => Promise;
connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise;
connectionNotion?: (args: KtxConnectionNotionArgs, io: KtxCliIo) => Promise;
connectionMetabaseSetup?: (args: KtxConnectionMetabaseSetupArgs, io: KtxCliIo) => Promise;
diff --git a/packages/cli/src/command-schemas.ts b/packages/cli/src/command-schemas.ts
index 9ffe6de3..cb11f2eb 100644
--- a/packages/cli/src/command-schemas.ts
+++ b/packages/cli/src/command-schemas.ts
@@ -53,15 +53,18 @@ export const slQueryCommandSchema = z.object({
command: z.literal('query'),
projectDir: projectDirSchema,
connectionId: z.string().min(1).optional(),
- query: z.object({
- measures: z.array(z.string().min(1)).min(1),
- dimensions: stringArraySchema,
- filters: stringArraySchema.optional(),
- segments: stringArraySchema.optional(),
- order_by: z.array(orderBySchema).optional(),
- limit: z.number().int().positive().optional(),
- include_empty: z.literal(true).optional(),
- }),
+ query: z
+ .object({
+ measures: z.array(z.string().min(1)).min(1),
+ dimensions: stringArraySchema,
+ filters: stringArraySchema.optional(),
+ segments: stringArraySchema.optional(),
+ order_by: z.array(orderBySchema).optional(),
+ limit: z.number().int().positive().optional(),
+ include_empty: z.literal(true).optional(),
+ })
+ .optional(),
+ queryFile: z.string().min(1).optional(),
format: z.enum(['json', 'sql']),
execute: z.boolean(),
cliVersion: z.string().min(1),
diff --git a/packages/cli/src/commands/agent-commands.ts b/packages/cli/src/commands/agent-commands.ts
deleted file mode 100644
index 2593991a..00000000
--- a/packages/cli/src/commands/agent-commands.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { Option, type Command } from '@commander-js/extra-typings';
-import type { KtxAgentArgs } from '../agent.js';
-import type { KtxCliCommandContext } from '../cli-program.js';
-import { parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
-import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';
-
-async function runAgent(context: KtxCliCommandContext, args: KtxAgentArgs): Promise {
- const runner = context.deps.agent ?? (await import('../agent.js')).runKtxAgent;
- context.setExitCode(await runner(args, context.io));
-}
-
-function jsonOption(): Option {
- return new Option('--json', 'Print JSON output').makeOptionMandatory();
-}
-
-export function registerAgentCommands(program: Command, context: KtxCliCommandContext): void {
- const agent = program
- .command('agent', { hidden: true })
- .description('Machine-readable KTX commands for coding agents')
- .showHelpAfterError();
-
- agent.hook('preAction', (_thisCommand, actionCommand) => {
- context.writeDebug?.('agent', actionCommand);
- });
-
- agent
- .command('tools')
- .description('Print available agent-facing KTX tools')
- .addOption(jsonOption())
- .action(async (_options, command) => {
- await runAgent(context, { command: 'tools', projectDir: resolveCommandProjectDir(command), json: true });
- });
-
- agent
- .command('context')
- .description('Print project context for agent planning')
- .addOption(jsonOption())
- .action(async (_options, command) => {
- await runAgent(context, { command: 'context', projectDir: resolveCommandProjectDir(command), json: true });
- });
-
- const sl = agent.command('sl').description('Semantic-layer agent commands');
- sl.command('list')
- .description('List semantic-layer sources')
- .addOption(jsonOption())
- .option('--connection-id ', 'Filter by connection id')
- .option('--query ', 'Search source names and descriptions')
- .action(async (options: { connectionId?: string; query?: string }, command) => {
- await runAgent(context, {
- command: 'sl-list',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- ...(options.connectionId ? { connectionId: options.connectionId } : {}),
- ...(options.query ? { query: options.query } : {}),
- });
- });
- sl.command('read')
- .description('Read one semantic-layer source')
- .argument('')
- .addOption(jsonOption())
- .option('--connection-id ', 'Connection id containing the source')
- .action(async (sourceName: string, options: { connectionId?: string }, command) => {
- await runAgent(context, {
- command: 'sl-read',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- sourceName,
- ...(options.connectionId ? { connectionId: options.connectionId } : {}),
- });
- });
- sl.command('query')
- .description('Run a semantic-layer query JSON file')
- .addOption(jsonOption())
- .requiredOption('--connection-id ', 'Connection id for execution')
- .requiredOption('--query-file ', 'JSON semantic-layer query file')
- .option('--execute', 'Execute the compiled query against the connection', false)
- .option('--yes', 'Install the managed Python runtime without prompting when required', false)
- .option('--no-input', 'Disable interactive managed runtime installation')
- .option('--max-rows ', 'Maximum rows to return when executing', parsePositiveIntegerOption)
- .action(
- async (
- options: {
- connectionId: string;
- queryFile: string;
- execute: boolean;
- maxRows?: number;
- yes?: boolean;
- input?: boolean;
- },
- command,
- ) => {
- await runAgent(context, {
- command: 'sl-query',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- connectionId: options.connectionId,
- queryFile: options.queryFile,
- execute: options.execute,
- cliVersion: context.packageInfo.version,
- runtimeInstallPolicy: runtimeInstallPolicyFromFlags(options),
- ...(options.maxRows !== undefined ? { maxRows: options.maxRows } : {}),
- });
- },
- );
-
- const wiki = agent.command('wiki').description('KTX wiki agent commands');
- wiki
- .command('search')
- .description('Search KTX wiki pages')
- .argument('')
- .addOption(jsonOption())
- .option('--limit ', 'Maximum search results', parsePositiveIntegerOption, 10)
- .action(async (query: string, options: { limit: number }, command) => {
- await runAgent(context, {
- command: 'wiki-search',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- query,
- limit: options.limit,
- });
- });
- wiki
- .command('read')
- .description('Read one KTX wiki page')
- .argument('')
- .addOption(jsonOption())
- .action(async (pageId: string, _options, command) => {
- await runAgent(context, { command: 'wiki-read', projectDir: resolveCommandProjectDir(command), json: true, pageId });
- });
-
- const sql = agent.command('sql').description('Safe SQL execution commands');
- sql
- .command('execute')
- .description('Execute read-only SQL with a row limit')
- .addOption(jsonOption())
- .requiredOption('--connection-id ', 'Connection id for execution')
- .requiredOption('--sql-file ', 'SQL file to execute')
- .requiredOption('--max-rows ', 'Maximum rows to return', parsePositiveIntegerOption)
- .action(async (options: { connectionId: string; sqlFile: string; maxRows: number }, command) => {
- await runAgent(context, {
- command: 'sql-execute',
- projectDir: resolveCommandProjectDir(command),
- json: true,
- connectionId: options.connectionId,
- sqlFile: options.sqlFile,
- maxRows: options.maxRows,
- });
- });
-}
diff --git a/packages/cli/src/commands/knowledge-commands.ts b/packages/cli/src/commands/knowledge-commands.ts
index c85a118c..f8d716f7 100644
--- a/packages/cli/src/commands/knowledge-commands.ts
+++ b/packages/cli/src/commands/knowledge-commands.ts
@@ -1,5 +1,10 @@
import { type Command, Option } from '@commander-js/extra-typings';
-import { collectOption, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
+import {
+ collectOption,
+ type KtxCliCommandContext,
+ parsePositiveIntegerOption,
+ resolveCommandProjectDir,
+} from '../cli-program.js';
import { wikiWriteCommandSchema } from '../command-schemas.js';
import type { KtxKnowledgeArgs } from '../knowledge.js';
import { profileMark } from '../startup-profile.js';
@@ -24,12 +29,14 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
wiki
.command('list')
.description('List local wiki pages')
+ .option('--json', 'Print JSON output', false)
.option('--user-id ', 'Local user id', 'local')
- .action(async (options: { userId: string }, command) => {
+ .action(async (options: { userId: string; json?: boolean }, command) => {
await runKnowledgeArgs(context, {
command: 'list',
projectDir: resolveCommandProjectDir(command),
userId: options.userId,
+ json: options.json,
});
});
@@ -37,13 +44,15 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
.command('read')
.description('Read one local wiki page')
.argument('', 'Wiki page key')
+ .option('--json', 'Print JSON output', false)
.option('--user-id ', 'Local user id', 'local')
- .action(async (key: string, options: { userId: string }, command) => {
+ .action(async (key: string, options: { userId: string; json?: boolean }, command) => {
await runKnowledgeArgs(context, {
command: 'read',
projectDir: resolveCommandProjectDir(command),
key,
userId: options.userId,
+ json: options.json,
});
});
@@ -51,13 +60,17 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
.command('search')
.description('Search local wiki pages')
.argument('', 'Search query')
+ .option('--json', 'Print JSON output', false)
.option('--user-id ', 'Local user id', 'local')
- .action(async (query: string, options: { userId: string }, command) => {
+ .option('--limit ', 'Maximum search results', parsePositiveIntegerOption)
+ .action(async (query: string, options: { userId: string; json?: boolean; limit?: number }, command) => {
await runKnowledgeArgs(context, {
command: 'search',
projectDir: resolveCommandProjectDir(command),
query,
userId: options.userId,
+ json: options.json,
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
});
});
diff --git a/packages/cli/src/commands/sl-commands.ts b/packages/cli/src/commands/sl-commands.ts
index 36d75fac..e1b985a3 100644
--- a/packages/cli/src/commands/sl-commands.ts
+++ b/packages/cli/src/commands/sl-commands.ts
@@ -51,6 +51,7 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
sl.command('list')
.description('List semantic-layer sources')
.option('--connection-id ', 'KTX connection id')
+ .option('--query ', 'Search source names and descriptions')
.addOption(
new Option('--output ', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
'pretty',
@@ -59,26 +60,34 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
]),
)
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
- .action(async (options: { connectionId?: string; output?: 'pretty' | 'plain' | 'json'; json?: boolean }, command) => {
+ .action(
+ async (
+ options: { connectionId?: string; query?: string; output?: 'pretty' | 'plain' | 'json'; json?: boolean },
+ command,
+ ) => {
await runSlArgs(context, {
command: 'list',
projectDir: resolveCommandProjectDir(command),
connectionId: options.connectionId,
+ query: options.query,
output: options.output,
json: options.json,
});
- });
+ },
+ );
sl.command('read')
.description('Read a semantic-layer source')
.argument('', 'Semantic-layer source name')
.requiredOption('--connection-id ', 'KTX connection id')
- .action(async (sourceName: string, options: { connectionId: string }, command) => {
+ .option('--json', 'Print JSON output', false)
+ .action(async (sourceName: string, options: { connectionId: string; json?: boolean }, command) => {
await runSlArgs(context, {
command: 'read',
projectDir: resolveCommandProjectDir(command),
connectionId: options.connectionId,
sourceName,
+ json: options.json,
});
});
@@ -113,6 +122,7 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
sl.command('query')
.description('Compile or execute a semantic-layer query')
.option('--connection-id ', 'KTX connection id')
+ .option('--query-file ', 'JSON semantic-layer query file')
.option('--measure ', 'Measure to query; repeatable', collectOption, [])
.option('--dimension ', 'Dimension to include; repeatable', collectOption, [])
.option('--filter ', 'Filter expression; repeatable', collectOption, [])
@@ -126,22 +136,26 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
.option('--no-input', 'Disable interactive managed runtime installation')
.option('--max-rows ', 'Maximum rows to return when executing', parsePositiveIntegerOption)
.action(async (options, command) => {
- if (options.measure.length === 0) {
+ if (options.measure.length === 0 && !options.queryFile) {
throw new Error('sl query requires at least one --measure');
}
const args = slQueryCommandSchema.parse({
command: 'query',
projectDir: resolveCommandProjectDir(command),
connectionId: options.connectionId,
- query: {
- measures: options.measure,
- dimensions: options.dimension,
- ...(options.filter.length > 0 ? { filters: options.filter } : {}),
- ...(options.segment.length > 0 ? { segments: options.segment } : {}),
- ...(options.orderBy.length > 0 ? { order_by: options.orderBy } : {}),
- ...(options.limit !== undefined ? { limit: options.limit } : {}),
- ...(options.includeEmpty === true ? { include_empty: true } : {}),
- },
+ ...(options.queryFile
+ ? { queryFile: options.queryFile }
+ : {
+ query: {
+ measures: options.measure,
+ dimensions: options.dimension,
+ ...(options.filter.length > 0 ? { filters: options.filter } : {}),
+ ...(options.segment.length > 0 ? { segments: options.segment } : {}),
+ ...(options.orderBy.length > 0 ? { order_by: options.orderBy } : {}),
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
+ ...(options.includeEmpty === true ? { include_empty: true } : {}),
+ },
+ }),
format: options.format,
execute: options.execute === true,
cliVersion: context.packageInfo.version,
diff --git a/packages/cli/src/example-smoke.test.ts b/packages/cli/src/example-smoke.test.ts
index f5b70bfc..221c20f2 100644
--- a/packages/cli/src/example-smoke.test.ts
+++ b/packages/cli/src/example-smoke.test.ts
@@ -73,26 +73,27 @@ describe('standalone local warehouse example', () => {
const projectDir = await copyExampleProject(tempDir);
const sourceDir = join(projectDir, 'source');
- const knowledgeList = await runBuiltCli(['agent', 'wiki', 'search', 'revenue', '--json', '--project-dir', projectDir]);
+ const knowledgeList = await runBuiltCli(['wiki', 'search', 'revenue', '--json', '--project-dir', projectDir]);
expect(knowledgeList).toMatchObject({ code: 0, stderr: '' });
- expect(parseJsonOutput<{ results: Array<{ key: string; summary: string }> }>(knowledgeList.stdout).results).toContainEqual(
- expect.objectContaining({ key: 'revenue', summary: 'Paid order value after refunds' }),
- );
+ expect(
+ parseJsonOutput<{ data: { items: Array<{ key: string; summary: string }> } }>(knowledgeList.stdout).data.items,
+ ).toContainEqual(expect.objectContaining({ key: 'revenue', summary: 'Paid order value after refunds' }));
- const knowledgeRead = await runBuiltCli(['agent', 'wiki', 'read', 'revenue', '--json', '--project-dir', projectDir]);
+ const knowledgeRead = await runBuiltCli(['wiki', 'read', 'revenue', '--json', '--project-dir', projectDir]);
expect(knowledgeRead).toMatchObject({ code: 0, stderr: '' });
- expect(parseJsonOutput<{ content: string }>(knowledgeRead.stdout).content).toContain(
+ expect(parseJsonOutput<{ data: { content: string } }>(knowledgeRead.stdout).data.content).toContain(
'Revenue is paid order amount after refund adjustments.',
);
- const slList = await runBuiltCli(['agent', 'sl', 'list', '--json', '--project-dir', projectDir, '--connection-id', 'warehouse']);
+ const slList = await runBuiltCli(['sl', 'list', '--json', '--project-dir', projectDir, '--connection-id', 'warehouse']);
expect(slList).toMatchObject({ code: 0, stderr: '' });
- expect(parseJsonOutput<{ sources: Array<{ connectionId: string; name: string; columnCount: number }> }>(slList.stdout).sources).toContainEqual(
- expect.objectContaining({ connectionId: 'warehouse', name: 'orders', columnCount: 3 }),
- );
+ expect(
+ parseJsonOutput<{ data: { items: Array<{ connectionId: string; name: string; columnCount: number }> } }>(
+ slList.stdout,
+ ).data.items,
+ ).toContainEqual(expect.objectContaining({ connectionId: 'warehouse', name: 'orders', columnCount: 3 }));
const slRead = await runBuiltCli([
- 'agent',
'sl',
'read',
'orders',
@@ -103,7 +104,7 @@ describe('standalone local warehouse example', () => {
projectDir,
]);
expect(slRead).toMatchObject({ code: 0, stderr: '' });
- expect(parseJsonOutput<{ yaml: string }>(slRead.stdout).yaml).toContain('name: orders');
+ expect(parseJsonOutput<{ data: { yaml: string } }>(slRead.stdout).data.yaml).toContain('name: orders');
const ingest = await runBuiltCli([
'ingest',
diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts
index b79a0bb3..7887e552 100644
--- a/packages/cli/src/index.test.ts
+++ b/packages/cli/src/index.test.ts
@@ -1141,136 +1141,28 @@ describe('runKtxCli', () => {
expect(setupIo.stderr()).toContain('Choose only one Historic SQL action');
});
- it('registers hidden agent help and tools discovery without showing agent in root help', async () => {
- const helpIo = makeIo();
- const toolsIo = makeIo();
- const agent = vi.fn(async () => 0);
+ it('rejects the removed hidden agent command', async () => {
+ const io = makeIo();
- await expect(runKtxCli(['agent', '--help'], helpIo.io, { agent })).resolves.toBe(0);
- await expect(
- runKtxCli(['--project-dir', tempDir, 'agent', 'tools', '--json'], toolsIo.io, { agent }),
- ).resolves.toBe(0);
+ await expect(runKtxCli(['agent'], io.io)).resolves.toBe(1);
- expect(helpIo.stdout()).toContain('Usage: ktx agent');
- expect(toolsIo.stderr()).toBe('');
- expect(agent).toHaveBeenCalledWith({ command: 'tools', projectDir: tempDir, json: true }, toolsIo.io);
+ expect(io.stderr()).toContain("unknown command 'agent'");
+ expect(io.stdout()).toBe('');
});
- it('dispatches full hidden agent commands without exposing agent in root help', async () => {
- const agent = vi.fn(async () => 0);
- const cases = [
- {
- argv: ['--project-dir', tempDir, 'agent', 'context', '--json'],
- args: { command: 'context', projectDir: tempDir, json: true },
- },
- {
- argv: [
- '--project-dir',
- tempDir,
- 'agent',
- 'sl',
- 'list',
- '--json',
- '--connection-id',
- 'warehouse',
- '--query',
- 'orders',
- ],
- args: { command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'warehouse', query: 'orders' },
- },
- {
- argv: ['--project-dir', tempDir, 'agent', 'sl', 'read', 'orders', '--json', '--connection-id', 'warehouse'],
- args: { command: 'sl-read', projectDir: tempDir, json: true, sourceName: 'orders', connectionId: 'warehouse' },
- },
- {
- argv: [
- '--project-dir',
- tempDir,
- 'agent',
- 'sl',
- 'query',
- '--json',
- '--connection-id',
- 'warehouse',
- '--query-file',
- '/tmp/query.json',
- '--execute',
- '--max-rows',
- '100',
- ],
- args: {
- command: 'sl-query',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- queryFile: '/tmp/query.json',
- execute: true,
- maxRows: 100,
- cliVersion: '0.0.0-private',
- runtimeInstallPolicy: 'prompt',
- },
- },
- {
- argv: ['--project-dir', tempDir, 'agent', 'wiki', 'search', 'revenue', '--json', '--limit', '5'],
- args: { command: 'wiki-search', projectDir: tempDir, json: true, query: 'revenue', limit: 5 },
- },
- {
- argv: ['--project-dir', tempDir, 'agent', 'wiki', 'read', 'page-1', '--json'],
- args: { command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'page-1' },
- },
- {
- argv: [
- '--project-dir',
- tempDir,
- 'agent',
- 'sql',
- 'execute',
- '--json',
- '--connection-id',
- 'warehouse',
- '--sql-file',
- '/tmp/query.sql',
- '--max-rows',
- '100',
- ],
- args: {
- command: 'sql-execute',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- sqlFile: '/tmp/query.sql',
- maxRows: 100,
- },
- },
- ];
-
- for (const entry of cases) {
- const io = makeIo();
- await expect(runKtxCli(entry.argv, io.io, { agent })).resolves.toBe(0);
- expect(agent).toHaveBeenLastCalledWith(entry.args, io.io);
- expect(io.stderr()).toBe('');
- }
-
- const helpIo = makeIo();
- await expect(runKtxCli(['--help'], helpIo.io, { agent })).resolves.toBe(0);
- expect(helpIo.stdout()).not.toContain('agent ');
- });
-
- it('routes hidden agent SL query managed runtime policies', async () => {
+ it('routes public SL query files with managed runtime policies', async () => {
const autoIo = makeIo();
const neverIo = makeIo();
const conflictIo = makeIo();
- const agent = vi.fn(async () => 0);
+ const sl = vi.fn(async () => 0);
await expect(
runKtxCli(
[
'--project-dir',
tempDir,
- 'agent',
'sl',
'query',
- '--json',
'--connection-id',
'warehouse',
'--query-file',
@@ -1278,7 +1170,7 @@ describe('runKtxCli', () => {
'--yes',
],
autoIo.io,
- { agent },
+ { sl },
),
).resolves.toBe(0);
@@ -1287,10 +1179,8 @@ describe('runKtxCli', () => {
[
'--project-dir',
tempDir,
- 'agent',
'sl',
'query',
- '--json',
'--connection-id',
'warehouse',
'--query-file',
@@ -1298,7 +1188,7 @@ describe('runKtxCli', () => {
'--no-input',
],
neverIo.io,
- { agent },
+ { sl },
),
).resolves.toBe(0);
@@ -1307,10 +1197,8 @@ describe('runKtxCli', () => {
[
'--project-dir',
tempDir,
- 'agent',
'sl',
'query',
- '--json',
'--connection-id',
'warehouse',
'--query-file',
@@ -1319,33 +1207,33 @@ describe('runKtxCli', () => {
'--no-input',
],
conflictIo.io,
- { agent },
+ { sl },
),
).resolves.toBe(1);
- expect(agent).toHaveBeenNthCalledWith(
+ expect(sl).toHaveBeenNthCalledWith(
1,
{
- command: 'sl-query',
+ command: 'query',
projectDir: tempDir,
- json: true,
connectionId: 'warehouse',
queryFile: '/tmp/query.json',
execute: false,
+ format: 'json',
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'auto',
},
autoIo.io,
);
- expect(agent).toHaveBeenNthCalledWith(
+ expect(sl).toHaveBeenNthCalledWith(
2,
{
- command: 'sl-query',
+ command: 'query',
projectDir: tempDir,
- json: true,
connectionId: 'warehouse',
queryFile: '/tmp/query.json',
execute: false,
+ format: 'json',
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'never',
},
@@ -1354,112 +1242,6 @@ describe('runKtxCli', () => {
expect(conflictIo.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
});
- it('prints semantic-layer hybrid search metadata from the hidden agent sl list command', async () => {
- const agent = vi.fn(async (args, io) => {
- expect(args).toEqual({
- command: 'sl-list',
- projectDir: tempDir,
- json: true,
- connectionId: 'warehouse',
- query: 'paid',
- });
- io.stdout.write(
- `${JSON.stringify(
- {
- sources: [
- {
- connectionId: 'warehouse',
- connectionName: 'warehouse',
- name: 'orders',
- columnCount: 2,
- measureCount: 1,
- joinCount: 0,
- score: 0.03278688524590164,
- matchReasons: ['dictionary'],
- dictionaryMatches: [{ column: 'status', values: ['paid'] }],
- },
- ],
- totalSources: 1,
- },
- null,
- 2,
- )}\n`,
- );
- return 0;
- });
- const io = makeIo();
-
- await expect(
- runKtxCli(
- ['--project-dir', tempDir, 'agent', 'sl', 'list', '--json', '--connection-id', 'warehouse', '--query', 'paid'],
- io.io,
- { agent },
- ),
- ).resolves.toBe(0);
-
- expect(JSON.parse(io.stdout())).toEqual({
- sources: [
- expect.objectContaining({
- connectionId: 'warehouse',
- name: 'orders',
- matchReasons: ['dictionary'],
- dictionaryMatches: [{ column: 'status', values: ['paid'] }],
- }),
- ],
- totalSources: 1,
- });
- });
-
- it('prints wiki hybrid search metadata from the hidden agent wiki search command', async () => {
- const agent = vi.fn(async (args, io) => {
- expect(args).toEqual({
- command: 'wiki-search',
- projectDir: tempDir,
- json: true,
- query: 'paid order',
- limit: 5,
- });
- io.stdout.write(
- `${JSON.stringify(
- {
- results: [
- {
- key: 'metrics-revenue',
- path: 'knowledge/global/metrics-revenue.md',
- scope: 'GLOBAL',
- summary: 'Revenue metric definition',
- score: 0.02459016393442623,
- matchReasons: ['lexical', 'token'],
- },
- ],
- totalFound: 1,
- },
- null,
- 2,
- )}\n`,
- );
- return 0;
- });
- const io = makeIo();
-
- await expect(
- runKtxCli(['--project-dir', tempDir, 'agent', 'wiki', 'search', 'paid order', '--json', '--limit', '5'], io.io, {
- agent,
- }),
- ).resolves.toBe(0);
-
- expect(JSON.parse(io.stdout())).toEqual({
- results: [
- expect.objectContaining({
- key: 'metrics-revenue',
- path: 'knowledge/global/metrics-revenue.md',
- matchReasons: ['lexical', 'token'],
- }),
- ],
- totalFound: 1,
- });
- });
-
it('dispatches public connection subcommands through the existing connection implementation', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-connection-dispatch-'));
const connection = vi.fn(async () => 0);
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index de906ece..2cf9d5b2 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -9,17 +9,6 @@ export {
type KtxCliIo,
type KtxCliPackageInfo,
} from './cli-runtime.js';
-export { runKtxAgent, type KtxAgentArgs } from './agent.js';
-export {
- KTX_AGENT_MAX_ROWS_CAP,
- createKtxAgentRuntime,
- parseAgentMaxRows,
- readAgentJsonFile,
- writeAgentJson,
- writeAgentJsonError,
- type KtxAgentRuntime,
- type KtxAgentRuntimeDeps,
-} from './agent-runtime.js';
export { runKtxSetup, type KtxSetupArgs, type KtxSetupStatus } from './setup.js';
export type {
KtxSetupDatabaseDriver,
diff --git a/packages/cli/src/knowledge.test.ts b/packages/cli/src/knowledge.test.ts
index d7e17605..db794289 100644
--- a/packages/cli/src/knowledge.test.ts
+++ b/packages/cli/src/knowledge.test.ts
@@ -93,6 +93,65 @@ describe('runKtxKnowledge', () => {
expect(searchIo.stdout()).toContain('metrics-revenue');
});
+ it('prints wiki list, search, and read as public JSON envelopes', async () => {
+ const projectDir = join(tempDir, 'project');
+ await initKtxProject({ projectDir, projectName: 'warehouse' });
+
+ await expect(
+ runKtxKnowledge(
+ {
+ command: 'write',
+ projectDir,
+ key: 'metrics-revenue',
+ scope: 'GLOBAL',
+ userId: 'local',
+ summary: 'Revenue',
+ content: 'Revenue is paid order value.',
+ tags: ['finance'],
+ refs: [],
+ slRefs: ['orders'],
+ },
+ makeIo().io,
+ ),
+ ).resolves.toBe(0);
+
+ const listIo = makeIo();
+ await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', json: true }, listIo.io)).resolves.toBe(
+ 0,
+ );
+ expect(JSON.parse(listIo.stdout())).toMatchObject({
+ kind: 'list',
+ data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] },
+ meta: { command: 'wiki list' },
+ });
+
+ const searchIo = makeIo();
+ await expect(
+ runKtxKnowledge(
+ { command: 'search', projectDir, query: 'paid order', userId: 'local', json: true, limit: 5 },
+ searchIo.io,
+ ),
+ ).resolves.toBe(0);
+ expect(JSON.parse(searchIo.stdout())).toMatchObject({
+ kind: 'list',
+ data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] },
+ meta: { command: 'wiki search' },
+ });
+
+ const readIo = makeIo();
+ await expect(
+ runKtxKnowledge({ command: 'read', projectDir, key: 'metrics-revenue', userId: 'local', json: true }, readIo.io),
+ ).resolves.toBe(0);
+ expect(JSON.parse(readIo.stdout())).toMatchObject({
+ kind: 'wiki.page',
+ data: {
+ key: 'metrics-revenue',
+ summary: 'Revenue',
+ content: 'Revenue is paid order value.',
+ },
+ });
+ });
+
it('rejects slash-delimited write keys with a flat-key suggestion', async () => {
const projectDir = join(tempDir, 'project');
await initKtxProject({ projectDir, projectName: 'warehouse' });
diff --git a/packages/cli/src/knowledge.ts b/packages/cli/src/knowledge.ts
index 40cc5372..5c5df1ea 100644
--- a/packages/cli/src/knowledge.ts
+++ b/packages/cli/src/knowledge.ts
@@ -11,11 +11,12 @@ import {
searchLocalKnowledgePages,
writeLocalKnowledgePage,
} from '@ktx/context/wiki';
+import { writeJsonResult } from './io/print-list.js';
export type KtxKnowledgeArgs =
- | { command: 'list'; projectDir: string; userId: string }
- | { command: 'read'; projectDir: string; key: string; userId: string }
- | { command: 'search'; projectDir: string; query: string; userId: string }
+ | { command: 'list'; projectDir: string; userId: string; json?: boolean }
+ | { command: 'read'; projectDir: string; key: string; userId: string; json?: boolean }
+ | { command: 'search'; projectDir: string; query: string; userId: string; json?: boolean; limit?: number }
| {
command: 'write';
projectDir: string;
@@ -61,6 +62,14 @@ export async function runKtxKnowledge(
const project = await loadKtxProject({ projectDir: args.projectDir });
if (args.command === 'list') {
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
+ if (args.json) {
+ writeJsonResult(io, {
+ kind: 'list',
+ data: { items: pages },
+ meta: { command: 'wiki list' },
+ });
+ return 0;
+ }
for (const page of pages) {
io.stdout.write(`${page.scope}\t${page.key}\t${page.summary}\n`);
}
@@ -71,6 +80,14 @@ export async function runKtxKnowledge(
if (!page) {
throw new Error(`Knowledge page "${args.key}" was not found`);
}
+ if (args.json) {
+ writeJsonResult(io, {
+ kind: 'wiki.page',
+ data: page,
+ meta: { command: 'wiki read' },
+ });
+ return 0;
+ }
io.stdout.write(`# ${page.key}\n\n`);
io.stdout.write(`Scope: ${page.scope}\n`);
io.stdout.write(`Summary: ${page.summary}\n\n`);
@@ -82,7 +99,16 @@ export async function runKtxKnowledge(
query: args.query,
userId: args.userId,
embeddingService: wikiSearchEmbeddingService(project, deps),
+ limit: args.limit,
});
+ if (args.json) {
+ writeJsonResult(io, {
+ kind: 'list',
+ data: { items: results },
+ meta: { command: 'wiki search' },
+ });
+ return 0;
+ }
if (results.length === 0) {
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
if (pages.length === 0) {
diff --git a/packages/cli/src/next-steps.test.ts b/packages/cli/src/next-steps.test.ts
index fd8d8216..b4706d72 100644
--- a/packages/cli/src/next-steps.test.ts
+++ b/packages/cli/src/next-steps.test.ts
@@ -25,12 +25,8 @@ describe('KTX demo next steps', () => {
it('uses supported final public commands', () => {
expect(KTX_NEXT_STEP_COMMANDS).toEqual([
{
- command: 'ktx agent context --json',
- description: 'Verify the project context your agent can read',
- },
- {
- command: 'ktx agent tools --json',
- description: 'List direct CLI tools available to agents',
+ command: 'ktx status --json',
+ description: 'Verify project setup and context readiness',
},
{
command: 'ktx sl list',
@@ -46,8 +42,8 @@ describe('KTX demo next steps', () => {
it('uses only the direct CLI route for agent verification', () => {
const commands = KTX_NEXT_STEP_COMMANDS.map((step) => step.command);
- expect(commands).toContain('ktx agent context --json');
- expect(commands).toContain('ktx agent tools --json');
+ expect(commands).not.toContain('ktx agent context --json');
+ expect(commands).toContain('ktx status --json');
expect(commands).not.toContain('ktx serve --mcp stdio --user-id local');
});
@@ -64,8 +60,8 @@ describe('KTX demo next steps', () => {
it('does not advertise removed Commander migration commands', () => {
const rendered = formatNextStepLines().join('\n');
- expect(rendered).toContain('ktx agent tools --json');
- expect(rendered).toContain('ktx agent context --json');
+ expect(rendered).toContain('ktx status --json');
+ expect(rendered).not.toContain('ktx agent');
expect(rendered).toContain('ktx sl list');
expect(rendered).toContain('ktx wiki list');
@@ -109,7 +105,8 @@ describe('KTX demo next steps', () => {
}).join('\n');
expect(rendered).toContain('KTX context is ready for agents.');
- expect(rendered).toContain('ktx agent context --json');
+ expect(rendered).toContain('ktx status --json');
+ expect(rendered).not.toContain('ktx agent');
expect(rendered).not.toContain('ktx serve --mcp stdio --user-id local');
expect(rendered).not.toContain('Build KTX context next.');
});
diff --git a/packages/cli/src/next-steps.ts b/packages/cli/src/next-steps.ts
index db85da66..ee7535d7 100644
--- a/packages/cli/src/next-steps.ts
+++ b/packages/cli/src/next-steps.ts
@@ -11,12 +11,8 @@ export const KTX_CONTEXT_BUILD_COMMANDS = [
export const KTX_NEXT_STEP_DIRECT_COMMANDS = [
{
- command: 'ktx agent context --json',
- description: 'Verify the project context your agent can read',
- },
- {
- command: 'ktx agent tools --json',
- description: 'List direct CLI tools available to agents',
+ command: 'ktx status --json',
+ description: 'Verify project setup and context readiness',
},
{
command: 'ktx sl list',
diff --git a/packages/cli/src/project-dir.test.ts b/packages/cli/src/project-dir.test.ts
index c59172a6..c0022d4d 100644
--- a/packages/cli/src/project-dir.test.ts
+++ b/packages/cli/src/project-dir.test.ts
@@ -35,8 +35,7 @@ describe('project directory defaults', () => {
const ingest = vi.fn(async () => 0);
const scan = vi.fn(async () => 0);
const setup = vi.fn(async () => 0);
- const agent = vi.fn(async () => 0);
- const deps: KtxCliDeps = { agent, connection, doctor, ingest, scan, setup };
+ const deps: KtxCliDeps = { connection, doctor, ingest, scan, setup };
const cases: Array<{
argv: string[];
@@ -74,12 +73,6 @@ describe('project directory defaults', () => {
expected: { command: 'run', projectDir: '/tmp/ktx-env-project', connectionId: 'warehouse' },
expectedStderr: 'Project: /tmp/ktx-env-project\n',
},
- {
- argv: ['agent', 'tools', '--json'],
- spy: agent,
- expected: { command: 'tools', projectDir: '/tmp/ktx-env-project' },
- expectedStderr: '',
- },
];
for (const item of cases) {
diff --git a/packages/cli/src/setup-agents.test.ts b/packages/cli/src/setup-agents.test.ts
index 322db2aa..19647a3f 100644
--- a/packages/cli/src/setup-agents.test.ts
+++ b/packages/cli/src/setup-agents.test.ts
@@ -84,7 +84,10 @@ describe('setup agents', () => {
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');
- expect(skill).toContain('agent sql execute');
+ expect(skill).toContain('status --json');
+ expect(skill).toContain('sl list --json');
+ expect(skill).not.toContain('agent ');
+ expect(skill).not.toContain('sql execute');
expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
version: 1,
projectDir: tempDir,
@@ -115,8 +118,9 @@ describe('setup agents', () => {
const skill = await readFile(join(tempDir, '.agents/skills/ktx/SKILL.md'), 'utf-8');
expect(skill).not.toContain('`ktx agent');
- expect(skill).toContain('agent context --json');
- expect(skill).toContain('agent sql execute');
+ expect(skill).toContain('status --json');
+ expect(skill).toContain('sl query');
+ expect(skill).not.toContain('sql execute');
});
it('removes only manifest-listed files', async () => {
diff --git a/packages/cli/src/setup-agents.ts b/packages/cli/src/setup-agents.ts
index 151967aa..b4202ed6 100644
--- a/packages/cli/src/setup-agents.ts
+++ b/packages/cli/src/setup-agents.ts
@@ -124,7 +124,7 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun
return [
'---',
'name: ktx',
- 'description: Use local KTX semantic context, wiki knowledge, and safe SQL execution for this project.',
+ 'description: Use local KTX semantic context and wiki knowledge for this project.',
'---',
'',
'# KTX Local Context',
@@ -137,11 +137,11 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun
'',
'Available commands:',
'',
- `- \`${ktxCommandLine(input.launcher, ['agent', 'context', ...projectDirArgs])}\``,
- `- \`${ktxCommandLine(input.launcher, ['agent', 'sl', 'list', ...projectDirArgs])}\``,
- `- \`${ktxCommandLine(input.launcher, ['agent', 'sl', 'read', '', ...projectDirArgs])}\``,
+ `- \`${ktxCommandLine(input.launcher, ['status', ...projectDirArgs])}\``,
+ `- \`${ktxCommandLine(input.launcher, ['sl', 'list', ...projectDirArgs])}\``,
+ `- \`${ktxCommandLine(input.launcher, ['sl', 'list', ...projectDirArgs, '--query', ''])}\``,
+ `- \`${ktxCommandLine(input.launcher, ['sl', 'read', '', ...projectDirArgs, '--connection-id', ''])}\``,
`- \`${ktxCommandLine(input.launcher, [
- 'agent',
'sl',
'query',
...projectDirArgs,
@@ -153,29 +153,17 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun
'--max-rows',
'100',
])}\``,
- `- \`${ktxCommandLine(input.launcher, ['agent', 'wiki', 'search', '', ...projectDirArgs])}\``,
- `- \`${ktxCommandLine(input.launcher, ['agent', 'wiki', 'read', '', ...projectDirArgs])}\``,
- `- \`${ktxCommandLine(input.launcher, [
- 'agent',
- 'sql',
- 'execute',
- ...projectDirArgs,
- '--connection-id',
- '',
- '--sql-file',
- '',
- '--max-rows',
- '100',
- ])}\``,
+ `- \`${ktxCommandLine(input.launcher, ['wiki', 'search', '', ...projectDirArgs, '--limit', '10'])}\``,
+ `- \`${ktxCommandLine(input.launcher, ['wiki', 'read', '', ...projectDirArgs])}\``,
'',
- 'SQL execution is read-only, requires an explicit row limit, and should use the smallest useful limit.',
+ 'Use semantic-layer queries before direct database access. Do not print secrets or credential references.',
'',
].join('\n');
}
function ruleInstructionContent(input: { projectDir: string }): string {
return [
- `Use the \`ktx\` CLI to query local semantic context, wiki knowledge, and execute safe SQL for this project (\`--project-dir ${input.projectDir}\`).`,
+ `Use the \`ktx\` CLI to query local semantic context and wiki knowledge for this project (\`--project-dir ${input.projectDir}\`).`,
'',
'Use when the user asks about data schemas, metrics, dimensions, database structure, or wants to run SQL queries.',
'',
diff --git a/packages/cli/src/sl.test.ts b/packages/cli/src/sl.test.ts
index c04ec15d..8d360c58 100644
--- a/packages/cli/src/sl.test.ts
+++ b/packages/cli/src/sl.test.ts
@@ -84,6 +84,71 @@ describe('runKtxSl', () => {
expect(listIo.stdout()).toContain('warehouse\torders\tcolumns=1\tmeasures=0\tjoins=0');
});
+ it('prints semantic-layer reads and searched lists as public JSON envelopes', async () => {
+ const projectDir = join(tempDir, 'project');
+ await initKtxProject({ projectDir, projectName: 'warehouse' });
+
+ await expect(
+ runKtxSl(
+ {
+ command: 'write',
+ projectDir,
+ connectionId: 'warehouse',
+ sourceName: 'orders',
+ yaml: [
+ 'name: orders',
+ 'table: public.orders',
+ 'description: Paid order facts',
+ 'grain: [order_id]',
+ 'columns:',
+ ' - name: order_id',
+ ' type: string',
+ '',
+ ].join('\n'),
+ },
+ makeIo().io,
+ ),
+ ).resolves.toBe(0);
+
+ const readIo = makeIo();
+ await expect(
+ runKtxSl(
+ { command: 'read', projectDir, connectionId: 'warehouse', sourceName: 'orders', json: true },
+ readIo.io,
+ ),
+ ).resolves.toBe(0);
+ expect(JSON.parse(readIo.stdout())).toMatchObject({
+ kind: 'sl.source',
+ data: {
+ connectionId: 'warehouse',
+ name: 'orders',
+ yaml: expect.stringContaining('name: orders'),
+ },
+ });
+
+ const listIo = makeIo();
+ await expect(
+ runKtxSl(
+ { command: 'list', projectDir, connectionId: 'warehouse', query: 'paid', json: true },
+ listIo.io,
+ ),
+ ).resolves.toBe(0);
+ expect(JSON.parse(listIo.stdout())).toMatchObject({
+ kind: 'list',
+ data: {
+ items: [
+ expect.objectContaining({
+ connectionId: 'warehouse',
+ name: 'orders',
+ score: expect.any(Number),
+ matchReasons: expect.arrayContaining(['token']),
+ }),
+ ],
+ },
+ meta: { command: 'sl list' },
+ });
+ });
+
it('fails validation when a table-backed source declares columns absent from a matching warehouse manifest', async () => {
const projectDir = join(tempDir, 'project');
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
@@ -191,6 +256,73 @@ joins: []
expect(stderr.write).not.toHaveBeenCalled();
});
+ it('runs sl query from a JSON query file', async () => {
+ const projectDir = join(tempDir, 'project');
+ const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
+ project.config.connections.warehouse = { driver: 'postgres', readonly: true };
+ await project.fileStore.writeFile(
+ 'semantic-layer/warehouse/orders.yaml',
+ `name: orders
+table: public.orders
+grain: [id]
+columns:
+ - name: id
+ type: number
+measures:
+ - name: order_count
+ expr: count(*)
+joins: []
+`,
+ 'ktx',
+ 'ktx@example.com',
+ 'Add orders source',
+ );
+ const queryFile = join(tempDir, 'query.json');
+ await writeFile(queryFile, '{"measures":["orders.order_count"],"dimensions":[]}', 'utf-8');
+
+ const stdout = { write: vi.fn() };
+ const stderr = { write: vi.fn() };
+ const query = vi.fn(async () => ({
+ sql: 'select count(*) as order_count from public.orders',
+ dialect: 'postgres',
+ columns: [{ name: 'orders.order_count' }],
+ plan: {},
+ }));
+ const createSemanticLayerCompute = vi.fn(() => ({
+ query,
+ validateSources: vi.fn(),
+ generateSources: vi.fn(),
+ }));
+
+ await expect(
+ runKtxSl(
+ {
+ command: 'query',
+ projectDir,
+ connectionId: 'warehouse',
+ queryFile,
+ format: 'json',
+ execute: false,
+ cliVersion: '0.2.0',
+ runtimeInstallPolicy: 'auto',
+ },
+ { stdout, stderr },
+ { createSemanticLayerCompute },
+ ),
+ ).resolves.toBe(0);
+
+ expect(query).toHaveBeenCalledWith(
+ expect.objectContaining({
+ query: { measures: ['orders.order_count'], dimensions: [] },
+ }),
+ );
+ expect(JSON.parse(String(stdout.write.mock.calls[0][0]))).toMatchObject({
+ sql: 'select count(*) as order_count from public.orders',
+ plan: { execution: { mode: 'compile_only' } },
+ });
+ expect(stderr.write).not.toHaveBeenCalled();
+ });
+
it('creates default sl query compute through the managed runtime helper', async () => {
const projectDir = join(tempDir, 'project');
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
diff --git a/packages/cli/src/sl.ts b/packages/cli/src/sl.ts
index fb0f129e..ebf3eca7 100644
--- a/packages/cli/src/sl.ts
+++ b/packages/cli/src/sl.ts
@@ -1,14 +1,22 @@
+import { readFile } from 'node:fs/promises';
import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
+import {
+ createLocalKtxEmbeddingProviderFromConfig,
+ KtxIngestEmbeddingPortAdapter,
+ type KtxEmbeddingPort,
+} from '@ktx/context';
import type { KtxSemanticLayerComputePort } from '@ktx/context/daemon';
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import {
compileLocalSlQuery,
listLocalSlSources,
readLocalSlSource,
+ searchLocalSlSources,
validateLocalSlSource,
writeLocalSlSource,
type SemanticLayerQueryInput,
} from '@ktx/context/sl';
+import { writeJsonResult } from './io/print-list.js';
import {
createManagedPythonSemanticLayerComputePort,
type KtxManagedPythonInstallPolicy,
@@ -20,15 +28,16 @@ profileMark('module:sl');
type SlQueryFormat = 'json' | 'sql';
export type KtxSlArgs =
- | { command: 'list'; projectDir: string; connectionId?: string; output?: string; json?: boolean }
- | { command: 'read'; projectDir: string; connectionId: string; sourceName: string }
+ | { command: 'list'; projectDir: string; connectionId?: string; query?: string; output?: string; json?: boolean }
+ | { command: 'read'; projectDir: string; connectionId: string; sourceName: string; json?: boolean }
| { command: 'validate'; projectDir: string; connectionId: string; sourceName: string }
| { command: 'write'; projectDir: string; connectionId: string; sourceName: string; yaml: string }
| {
command: 'query';
projectDir: string;
connectionId?: string;
- query: SemanticLayerQueryInput;
+ query?: SemanticLayerQueryInput;
+ queryFile?: string;
format: SlQueryFormat;
execute: boolean;
maxRows?: number;
@@ -43,6 +52,8 @@ interface KtxSlIo {
interface KtxSlDeps {
loadProject?: typeof loadKtxProject;
+ embeddingService?: KtxEmbeddingPort | null;
+ createEmbeddingProvider?: typeof createLocalKtxEmbeddingProviderFromConfig;
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
createManagedSemanticLayerCompute?: (options: {
cliVersion: string;
@@ -52,11 +63,35 @@ interface KtxSlDeps {
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
}
+function slSearchEmbeddingService(project: KtxLocalProject, deps: KtxSlDeps): KtxEmbeddingPort | null {
+ if ('embeddingService' in deps) {
+ return deps.embeddingService ?? null;
+ }
+ const provider = (deps.createEmbeddingProvider ?? createLocalKtxEmbeddingProviderFromConfig)(
+ project.config.ingest.embeddings,
+ );
+ return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
+}
+
+async function readSlQueryFile(path: string): Promise {
+ const parsed = JSON.parse(await readFile(path, 'utf-8')) as unknown;
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
+ throw new Error(`${path} must contain a JSON object.`);
+ }
+ return parsed as SemanticLayerQueryInput;
+}
+
export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: KtxSlDeps = {}): Promise {
try {
const project = await (deps.loadProject ?? loadKtxProject)({ projectDir: args.projectDir });
if (args.command === 'list') {
- const sources = await listLocalSlSources(project, { connectionId: args.connectionId });
+ const sources = args.query
+ ? await searchLocalSlSources(project, {
+ connectionId: args.connectionId,
+ query: args.query,
+ embeddingService: slSearchEmbeddingService(project, deps),
+ })
+ : await listLocalSlSources(project, { connectionId: args.connectionId });
const { resolveOutputMode } = await import('./io/mode.js');
const { printList } = await import('./io/print-list.js');
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
@@ -86,6 +121,14 @@ export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: Ktx
if (!source) {
throw new Error(`Semantic-layer source "${args.connectionId}/${args.sourceName}" was not found`);
}
+ if (args.json) {
+ writeJsonResult(io, {
+ kind: 'sl.source',
+ data: source,
+ meta: { command: 'sl read' },
+ });
+ return 0;
+ }
io.stdout.write(source.yaml);
return 0;
}
@@ -108,6 +151,10 @@ export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: Ktx
return 0;
}
if (args.command === 'query') {
+ const query = args.query ?? (args.queryFile ? await readSlQueryFile(args.queryFile) : undefined);
+ if (!query) {
+ throw new Error('sl query requires query input from --query-file or at least one --measure');
+ }
const compute = deps.createSemanticLayerCompute
? deps.createSemanticLayerCompute()
: await (deps.createManagedSemanticLayerCompute ?? createManagedPythonSemanticLayerComputePort)({
@@ -118,7 +165,7 @@ export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: Ktx
const queryExecutor = args.execute ? (deps.createQueryExecutor ?? createDefaultLocalQueryExecutor)() : undefined;
const result = await compileLocalSlQuery(project as KtxLocalProject, {
connectionId: args.connectionId,
- query: args.query,
+ query,
compute,
execute: args.execute,
maxRows: args.maxRows,
diff --git a/packages/cli/src/standalone-smoke.test.ts b/packages/cli/src/standalone-smoke.test.ts
index 9efa52cb..026b4834 100644
--- a/packages/cli/src/standalone-smoke.test.ts
+++ b/packages/cli/src/standalone-smoke.test.ts
@@ -190,49 +190,21 @@ describe('standalone built ktx CLI smoke', () => {
);
});
- it('prints guided JSON for agent semantic-layer search outside a project through the built binary', async () => {
- const projectDir = join(tempDir, 'missing-search-project');
- await mkdir(projectDir, { recursive: true });
-
- const result = await runBuiltCli([
- 'agent',
- 'sl',
- 'list',
- '--json',
- '--query',
- 'revenue',
- '--project-dir',
- projectDir,
- ]);
+ it('rejects the removed agent command through the built binary', async () => {
+ const result = await runBuiltCli(['agent']);
expect(result.code).toBe(1);
expect(result.stdout).toBe('');
- const errorJson = parseJsonOutput<{
- ok: false;
- error: { code: string; message: string; nextSteps: string[] };
- }>(result.stderr);
- expect(errorJson).toEqual({
- ok: false,
- error: {
- code: 'agent_sl_search_missing_project',
- message: `Semantic-layer search needs an initialized KTX project at ${projectDir}.`,
- nextSteps: [
- `ktx setup --project-dir ${projectDir}`,
- `ktx status --project-dir ${projectDir}`,
- 'ktx ingest run --connection-id --adapter ',
- `ktx agent sl list --json --query "revenue" --project-dir ${projectDir}`,
- ],
- },
- });
+ expect(result.stderr).toContain("unknown command 'agent'");
});
it('runs doctor setup through the built binary', async () => {
const result = await runBuiltCli(['status', '--no-input']);
- expect(result.stdout).toContain('KTX setup doctor');
+ expect(result.stdout).toMatch(/KTX (setup|project) doctor/);
expect(result.stdout).toContain('Node 22+');
expect(result.stdout).toContain('Workspace-local CLI');
- expect(result.stderr).toBe('');
+ expect(result.stderr === '' || result.stderr.startsWith('Project: ')).toBe(true);
expect([0, 1]).toContain(result.code);
});
diff --git a/packages/context/src/search/pglite-runtime-boundary.test.ts b/packages/context/src/search/pglite-runtime-boundary.test.ts
index ce2f5b7a..2db3209b 100644
--- a/packages/context/src/search/pglite-runtime-boundary.test.ts
+++ b/packages/context/src/search/pglite-runtime-boundary.test.ts
@@ -46,7 +46,8 @@ describe('PGlite hybrid search runtime boundary', () => {
}
const productionRoutingFiles = [
- 'packages/cli/src/agent.ts',
+ 'packages/cli/src/sl.ts',
+ 'packages/cli/src/knowledge.ts',
'packages/context/src/mcp/local-project-ports.ts',
'packages/context/src/wiki/local-knowledge.ts',
'packages/context/src/ingest/context-evidence/sqlite-context-evidence-store.ts',
diff --git a/scripts/examples-docs.test.mjs b/scripts/examples-docs.test.mjs
index 504e0d36..26db4ae8 100644
--- a/scripts/examples-docs.test.mjs
+++ b/scripts/examples-docs.test.mjs
@@ -156,14 +156,12 @@ describe('standalone example docs', () => {
const servingAgents = await readText('docs-site/content/docs/guides/serving-agents.mdx');
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',
+ 'ktx status --json',
+ 'ktx sl list --json',
+ 'ktx sl read orders --json',
+ 'ktx sl query --json',
+ 'ktx wiki search "revenue recognition" --json',
+ 'ktx wiki read order-status-definitions --json',
]) {
assert.match(servingAgents, new RegExp(command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
diff --git a/scripts/package-artifacts.mjs b/scripts/package-artifacts.mjs
index 5f080068..2428dd2e 100644
--- a/scripts/package-artifacts.mjs
+++ b/scripts/package-artifacts.mjs
@@ -557,12 +557,6 @@ function parseJsonResultWithExitCode(label, result, expectedCode) {
return JSON.parse(result.stdout);
}
-function parseJsonFailure(label, result) {
- assert.equal(result.code, 1, label + ' should fail with exit code 1');
- assert.equal(result.stdout, '', label + ' should not write stdout when failing');
- return JSON.parse(result.stderr);
-}
-
function requireIncludes(values, expected, label) {
assert.ok(Array.isArray(values), label + ' must be an array');
assert.ok(values.includes(expected), label + ' did not include ' + expected + ': ' + values.join(', '));
@@ -612,30 +606,6 @@ try {
assert.equal(runtimeStatusBefore.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
process.stdout.write('ktx managed runtime starts missing in isolated root\\n');
- const missingProjectDir = join(root, 'missing-project');
- await mkdir(missingProjectDir, { recursive: true });
- const missingProjectSearch = await run('pnpm', [
- 'exec',
- 'ktx',
- 'agent',
- 'sl',
- 'list',
- '--json',
- '--query',
- 'revenue',
- '--project-dir',
- missingProjectDir,
- ]);
- const missingProjectError = parseJsonFailure('ktx agent sl list missing project', missingProjectSearch);
- assert.equal(missingProjectError.error.code, 'agent_sl_search_missing_project');
- assert.deepEqual(missingProjectError.error.nextSteps, [
- 'ktx setup --project-dir ' + missingProjectDir,
- 'ktx status --project-dir ' + missingProjectDir,
- 'ktx ingest run --connection-id --adapter ',
- 'ktx agent sl list --json --query "revenue" --project-dir ' + missingProjectDir,
- ]);
- process.stdout.write('ktx agent sl list missing project guidance verified\\n');
-
const init = await run('pnpm', [
'exec',
'ktx',
@@ -671,28 +641,6 @@ try {
'--skip-agents',
]);
requireProjectStderr('ktx setup empty project', emptyInit, emptyProjectDir);
- const emptySearch = await run('pnpm', [
- 'exec',
- 'ktx',
- 'agent',
- 'sl',
- 'list',
- '--json',
- '--query',
- 'revenue',
- '--project-dir',
- emptyProjectDir,
- ]);
- const emptySearchError = parseJsonFailure('ktx agent sl list no connections', emptySearch);
- assert.equal(emptySearchError.error.code, 'agent_sl_search_no_connections');
- assert.deepEqual(emptySearchError.error.nextSteps, [
- 'ktx setup --project-dir ' + emptyProjectDir,
- 'ktx status --project-dir ' + emptyProjectDir,
- 'ktx ingest run --connection-id --adapter ',
- 'ktx agent sl list --json --query "revenue" --project-dir ' + emptyProjectDir,
- ]);
- process.stdout.write('ktx agent sl list no connections guidance verified\\n');
-
await writeFile(
join(projectDir, 'ktx.yaml'),
[
@@ -737,10 +685,9 @@ try {
'utf-8',
);
- const agentWikiSearch = await run('pnpm', [
+ const wikiSearch = await run('pnpm', [
'exec',
'ktx',
- 'agent',
'wiki',
'search',
'revenue',
@@ -750,40 +697,17 @@ try {
'--project-dir',
projectDir,
]);
- const agentWikiSearchJson = parseJsonResult('ktx agent wiki search', agentWikiSearch);
- assert.equal(agentWikiSearchJson.totalFound, 1);
- assert.equal(agentWikiSearchJson.results[0].key, 'revenue');
- assert.equal(agentWikiSearchJson.results[0].path, 'knowledge/global/revenue.md');
- assert.equal(typeof agentWikiSearchJson.results[0].score, 'number');
- requireIncludes(agentWikiSearchJson.results[0].matchReasons, 'lexical', 'agent wiki search match reasons');
- process.stdout.write('ktx agent wiki search hybrid metadata verified\\n');
+ const wikiSearchJson = parseJsonResult('ktx wiki search', wikiSearch);
+ assert.equal(wikiSearchJson.kind, 'list');
+ assert.equal(wikiSearchJson.data.items.length, 1);
+ assert.equal(wikiSearchJson.data.items[0].key, 'revenue');
+ assert.equal(wikiSearchJson.data.items[0].path, 'knowledge/global/revenue.md');
+ assert.equal(typeof wikiSearchJson.data.items[0].score, 'number');
+ requireIncludes(wikiSearchJson.data.items[0].matchReasons, 'lexical', 'wiki search match reasons');
+ process.stdout.write('ktx wiki search hybrid metadata verified\\n');
await access(join(projectDir, '.ktx', 'db.sqlite'));
process.stdout.write('SQLite knowledge index: ' + join(projectDir, '.ktx', 'db.sqlite') + '\\n');
- const noSourceSearch = await run('pnpm', [
- 'exec',
- 'ktx',
- 'agent',
- 'sl',
- 'list',
- '--json',
- '--connection-id',
- 'warehouse',
- '--query',
- 'revenue',
- '--project-dir',
- projectDir,
- ]);
- const noSourceSearchError = parseJsonFailure('ktx agent sl list no indexed sources', noSourceSearch);
- assert.equal(noSourceSearchError.error.code, 'agent_sl_search_no_indexed_sources');
- assert.deepEqual(noSourceSearchError.error.nextSteps, [
- 'ktx setup --project-dir ' + projectDir,
- 'ktx status --project-dir ' + projectDir,
- 'ktx ingest run --connection-id --adapter ',
- 'ktx agent sl list --json --query "revenue" --project-dir ' + projectDir,
- ]);
- process.stdout.write('ktx agent sl list no indexed sources guidance verified\\n');
-
const slYaml = [
'name: orders',
'table: orders',
@@ -804,10 +728,9 @@ try {
await mkdir(join(projectDir, 'semantic-layer', 'warehouse'), { recursive: true });
await writeFile(join(projectDir, 'semantic-layer', 'warehouse', 'orders.yaml'), slYaml, 'utf-8');
- const agentSlSearch = await run('pnpm', [
+ const slSearch = await run('pnpm', [
'exec',
'ktx',
- 'agent',
'sl',
'list',
'--json',
@@ -818,13 +741,14 @@ try {
'--project-dir',
projectDir,
]);
- const agentSlSearchJson = parseJsonResult('ktx agent sl list', agentSlSearch);
- assert.equal(agentSlSearchJson.totalSources, 1);
- assert.equal(agentSlSearchJson.sources[0].connectionId, 'warehouse');
- assert.equal(agentSlSearchJson.sources[0].name, 'orders');
- assert.equal(typeof agentSlSearchJson.sources[0].score, 'number');
- requireIncludes(agentSlSearchJson.sources[0].matchReasons, 'lexical', 'agent sl search match reasons');
- process.stdout.write('ktx agent sl list hybrid metadata verified\\n');
+ const slSearchJson = parseJsonResult('ktx sl list', slSearch);
+ assert.equal(slSearchJson.kind, 'list');
+ assert.equal(slSearchJson.data.items.length, 1);
+ assert.equal(slSearchJson.data.items[0].connectionId, 'warehouse');
+ assert.equal(slSearchJson.data.items[0].name, 'orders');
+ assert.equal(typeof slSearchJson.data.items[0].score, 'number');
+ requireIncludes(slSearchJson.data.items[0].matchReasons, 'lexical', 'sl search match reasons');
+ process.stdout.write('ktx sl list hybrid metadata verified\\n');
const slQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query',
'--connection-id',
diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs
index b4176353..64ce9466 100644
--- a/scripts/package-artifacts.test.mjs
+++ b/scripts/package-artifacts.test.mjs
@@ -461,9 +461,9 @@ describe('verification snippets', () => {
assert.doesNotMatch(source, /startSemanticDaemon/);
assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'setup'/);
assert.match(source, /knowledge', 'global', 'revenue\.md'/);
- assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'agent',\s*'wiki',\s*'search'/);
+ assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'wiki',\s*'search'/);
assert.match(source, /semantic-layer', 'warehouse', 'orders\.yaml'/);
- assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'agent',\s*'sl',\s*'list'/);
+ assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'sl',\s*'list'/);
assert.match(source, /orders\.order_count/);
assert.match(source, /sqlite3/);
assert.match(source, /driver: sqlite/);