From 721f1a998fdf2f85ce99244ec0cea45749686192 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 13 May 2026 13:01:56 +0200 Subject: [PATCH] feat(cli)!: remove ktx agent command (#58) * feat(cli)!: remove ktx agent command * test(context): update PGlite boundary guardrail --- docs-site/components/terminal-preview.tsx | 2 +- .../docs/ai-resources/agent-quickstart.mdx | 2 +- .../docs/ai-resources/markdown-access.mdx | 3 +- .../content/docs/cli-reference/ktx-agent.mdx | 148 ------ .../content/docs/cli-reference/ktx-sl.mdx | 16 + .../content/docs/cli-reference/ktx-wiki.mdx | 30 +- .../content/docs/cli-reference/meta.json | 1 - .../docs/getting-started/quickstart.mdx | 2 +- .../content/docs/guides/serving-agents.mdx | 41 +- .../docs/integrations/agent-clients.mdx | 43 +- .../docs/integrations/primary-sources.mdx | 2 +- docs-site/lib/llm-docs.ts | 4 +- packages/cli/src/agent-runtime.test.ts | 152 ------- packages/cli/src/agent-runtime.ts | 109 ----- .../cli/src/agent-search-readiness.test.ts | 51 --- packages/cli/src/agent-search-readiness.ts | 94 ---- packages/cli/src/agent.test.ts | 428 ------------------ packages/cli/src/agent.ts | 219 --------- packages/cli/src/cli-program.ts | 2 - packages/cli/src/cli-runtime.ts | 2 - packages/cli/src/command-schemas.ts | 21 +- packages/cli/src/commands/agent-commands.ts | 149 ------ .../cli/src/commands/knowledge-commands.ts | 21 +- packages/cli/src/commands/sl-commands.ts | 40 +- packages/cli/src/example-smoke.test.ts | 25 +- packages/cli/src/index.test.ts | 250 +--------- packages/cli/src/index.ts | 11 - packages/cli/src/knowledge.test.ts | 59 +++ packages/cli/src/knowledge.ts | 32 +- packages/cli/src/next-steps.test.ts | 19 +- packages/cli/src/next-steps.ts | 8 +- packages/cli/src/project-dir.test.ts | 9 +- packages/cli/src/setup-agents.test.ts | 10 +- packages/cli/src/setup-agents.ts | 30 +- packages/cli/src/sl.test.ts | 132 ++++++ packages/cli/src/sl.ts | 57 ++- packages/cli/src/standalone-smoke.test.ts | 38 +- .../search/pglite-runtime-boundary.test.ts | 3 +- scripts/examples-docs.test.mjs | 14 +- scripts/package-artifacts.mjs | 112 +---- scripts/package-artifacts.test.mjs | 4 +- 41 files changed, 500 insertions(+), 1895 deletions(-) delete mode 100644 docs-site/content/docs/cli-reference/ktx-agent.mdx delete mode 100644 packages/cli/src/agent-runtime.test.ts delete mode 100644 packages/cli/src/agent-runtime.ts delete mode 100644 packages/cli/src/agent-search-readiness.test.ts delete mode 100644 packages/cli/src/agent-search-readiness.ts delete mode 100644 packages/cli/src/agent.test.ts delete mode 100644 packages/cli/src/agent.ts delete mode 100644 packages/cli/src/commands/agent-commands.ts 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/);