From c202202e6b368cdd56303d1c9de8138d2d0ed9ea Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 13 May 2026 15:41:10 +0200 Subject: [PATCH] feat(cli): clean up wiki and sl commands (#65) * feat(cli): clean up wiki and sl commands * test(scripts): update package artifact CLI smoke assertion --- .../content/docs/cli-reference/ktx-sl.mdx | 34 +--- .../content/docs/cli-reference/ktx-wiki.mdx | 63 +------ .../docs/getting-started/quickstart.mdx | 6 +- .../content/docs/guides/serving-agents.mdx | 8 +- .../content/docs/guides/writing-context.mdx | 93 +++------- .../docs/integrations/agent-clients.mdx | 10 +- packages/cli/src/command-schemas.ts | 13 -- .../cli/src/commands/knowledge-commands.ts | 49 +----- packages/cli/src/commands/sl-commands.ts | 78 ++++----- packages/cli/src/example-smoke.test.ts | 16 +- packages/cli/src/index.test.ts | 50 ++++++ packages/cli/src/knowledge.test.ts | 162 ++++++------------ packages/cli/src/knowledge.ts | 59 +------ packages/cli/src/setup-agents.ts | 4 +- packages/cli/src/sl.test.ts | 130 ++++++-------- packages/cli/src/sl.ts | 109 ++++++------ scripts/examples-docs.test.mjs | 3 +- scripts/package-artifacts.mjs | 9 +- scripts/package-artifacts.test.mjs | 2 +- 19 files changed, 312 insertions(+), 586 deletions(-) diff --git a/docs-site/content/docs/cli-reference/ktx-sl.mdx b/docs-site/content/docs/cli-reference/ktx-sl.mdx index f5a31b27..b3e5305f 100644 --- a/docs-site/content/docs/cli-reference/ktx-sl.mdx +++ b/docs-site/content/docs/cli-reference/ktx-sl.mdx @@ -1,6 +1,6 @@ --- title: "ktx sl" -description: "List, read, validate, query, or write semantic-layer sources." +description: "List, search, validate, or query semantic-layer sources." --- Interact with your project's semantic layer. Semantic sources are YAML definitions that describe your tables, columns, measures, joins, and grain — the vocabulary agents use to generate correct SQL. @@ -16,9 +16,8 @@ ktx sl [options] | Subcommand | Description | |-----------|-------------| | `list` | List semantic-layer sources | -| `read ` | Read a semantic-layer source | +| `search ` | Search semantic-layer sources | | `validate ` | Validate a semantic-layer source against the database schema | -| `write ` | Write a semantic-layer source | | `query` | Compile or execute a semantic-layer query | ## Options @@ -28,16 +27,17 @@ 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` | -### `sl read` +### `sl search` | Flag | Description | Default | |------|-------------|---------| -| `--connection-id ` | KTX connection id (required) | — | -| `--json` | Print JSON output | `false` | +| `--connection-id ` | Filter by KTX connection id | — | +| `--limit ` | Maximum search results | — | +| `--output ` | Output mode: `pretty` (default in TTY), `plain` (TSV), or `json` | `pretty` | +| `--json` | Shortcut for `--output=json` (overrides `--output`) | `false` | ### `sl validate` @@ -45,13 +45,6 @@ ktx sl [options] |------|-------------|---------| | `--connection-id ` | KTX connection id (required) | — | -### `sl write` - -| Flag | Description | Default | -|------|-------------|---------| -| `--connection-id ` | KTX connection id (required) | — | -| `--yaml ` | Semantic-layer source YAML content (required) | — | - ### `sl query` | Flag | Description | Default | @@ -82,20 +75,11 @@ ktx sl list --connection-id my-warehouse 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 +ktx sl search "revenue" --json # Validate a source against the live schema ktx sl validate orders --connection-id my-warehouse -# Write a new source from YAML -ktx sl write customers --connection-id my-warehouse --yaml "$(cat sources/customers.yaml)" - # Compile a query and view the generated SQL ktx sl query \ --connection-id my-warehouse \ @@ -159,5 +143,5 @@ Semantic-layer commands return human-readable output by default. Use `--json` or |-------|-------|----------| | Source not found | Source name or connection id is wrong | Run `ktx sl list --json` and retry with an exact source name and connection id | | Validation fails | YAML references missing columns, invalid joins, or invalid SQL expressions | Fix the source YAML and rerun `ktx sl validate` | -| Query compile fails | Measure, dimension, filter, or segment name is invalid | Read the source with `ktx sl read`, then retry using declared fields | +| Query compile fails | Measure, dimension, filter, or segment name is invalid | Search sources with `ktx sl search`, inspect the source YAML in your project files, then retry using declared fields | | Execution returns too many rows | `--max-rows` is missing or too high | Add `--max-rows` with a bounded value before executing | diff --git a/docs-site/content/docs/cli-reference/ktx-wiki.mdx b/docs-site/content/docs/cli-reference/ktx-wiki.mdx index 7e45420e..8e27b5ff 100644 --- a/docs-site/content/docs/cli-reference/ktx-wiki.mdx +++ b/docs-site/content/docs/cli-reference/ktx-wiki.mdx @@ -1,6 +1,6 @@ --- title: "ktx wiki" -description: "List, read, search, or write knowledge pages." +description: "List or search knowledge pages." --- Manage knowledge pages in your KTX project. Knowledge pages are Markdown documents that capture business definitions, rules, and gotchas. Agents search them for context when answering questions about your data. @@ -16,9 +16,7 @@ ktx wiki [options] | Subcommand | Description | |-----------|-------------| | `list` | List local wiki pages | -| `read ` | Read one local wiki page | | `search ` | Search local wiki pages | -| `write ` | Write one local wiki page | ## Options @@ -29,13 +27,6 @@ ktx wiki [options] | `--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 | @@ -44,18 +35,6 @@ ktx wiki [options] | `--user-id ` | Local user id | `local` | | `--limit ` | Maximum search results | — | -### `wiki write` - -| Flag | Description | Default | -|------|-------------|---------| -| `--user-id ` | Local user id | `local` | -| `--scope ` | Scope: `global` or `user` | `global` | -| `--summary ` | Wiki page summary (required) | — | -| `--content ` | Wiki page content (required) | — | -| `--tag ` | Wiki tag; repeatable | — | -| `--ref ` | Wiki ref; repeatable | — | -| `--sl-ref ` | Semantic-layer ref; repeatable | — | - ## Examples ```bash @@ -65,48 +44,16 @@ 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" \ - --content "## MRR\nMonthly Recurring Revenue is calculated as..." - -# Write a user-scoped knowledge page -ktx wiki write my-notes \ - --scope user \ - --summary "Personal analysis notes" \ - --content "Things to check when revenue numbers look off..." - -# Write a page with tags and references -ktx wiki write churn-rules \ - --summary "Churn calculation business rules" \ - --content "A customer is considered churned when..." \ - --tag finance \ - --tag retention \ - --sl-ref customers \ - --sl-ref subscriptions - -# Write a page with external references -ktx wiki write data-freshness \ - --summary "Data pipeline SLAs and freshness guarantees" \ - --content "The orders table refreshes every 15 minutes..." \ - --ref "https://wiki.example.com/data-pipelines" ``` ## Output -Wiki commands print local knowledge pages and search results. Agents should search first, then read the most relevant page by key. +Wiki commands print local knowledge pages and search results. ```json { @@ -127,7 +74,5 @@ Wiki commands print local knowledge pages and search results. Agents should sear | Error | Cause | Recovery | |-------|-------|----------| -| Search returns no results | The query terms do not match summaries, tags, or content | Retry with business synonyms, then create a page if the knowledge is missing | -| Read fails for a key | The page key is wrong or scoped to a different user | Run `ktx wiki list` or search again to get the exact key | -| Write fails due to missing fields | `--summary` or `--content` was omitted | Pass both fields, and keep the summary short enough for search results | -| Agent writes duplicate pages | It did not search existing pages first | Always run `ktx wiki search` before `ktx wiki write` | +| Search returns no results | The query terms do not match summaries, tags, or content | Retry with business synonyms or run ingest to capture more context | +| A page is missing | The page has not been created by ingest or memory capture yet | Run ingest, then search again with `ktx wiki search` | diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx index d71a0754..59a512cb 100644 --- a/docs-site/content/docs/getting-started/quickstart.mdx +++ b/docs-site/content/docs/getting-started/quickstart.mdx @@ -208,9 +208,9 @@ KTX writes project state as plain files so agents can inspect and edit changes i |------|------------|---------| | `ktx.yaml` | `ktx setup` | Main project configuration: connections, LLM settings, embeddings, and context sources | | `.ktx/secrets/*` | `ktx setup` when file-backed secrets are selected | Local secret files referenced from `ktx.yaml`; do not commit these | -| `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 | +| `semantic-layer//*.yaml` | context build, ingestion, or direct file edits | Semantic source definitions agents use for SQL generation | +| `knowledge/global/*.md` | ingestion, memory capture, or direct file edits | Shared business context and metric definitions | +| `knowledge/user//*.md` | memory capture or direct file edits | 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 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 b6f073b8..0de6934e 100644 --- a/docs-site/content/docs/guides/serving-agents.mdx +++ b/docs-site/content/docs/guides/serving-agents.mdx @@ -26,10 +26,7 @@ ktx status --json # List sources ktx sl list --json ktx sl list --json --connection-id my-postgres -ktx sl list --json --query "revenue" - -# Read a source -ktx sl read orders --json --connection-id my-postgres +ktx sl search "revenue" --json # Run a query from a JSON file ktx sl query --json \ @@ -44,9 +41,6 @@ ktx sl query --json \ ```bash # Search knowledge pages ktx wiki search "revenue recognition" --json --limit 10 - -# Read a specific page -ktx wiki read order-status-definitions --json ``` ## Setting Up Your Agent diff --git a/docs-site/content/docs/guides/writing-context.mdx b/docs-site/content/docs/guides/writing-context.mdx index 3f0e3fbd..9e08fcc7 100644 --- a/docs-site/content/docs/guides/writing-context.mdx +++ b/docs-site/content/docs/guides/writing-context.mdx @@ -10,11 +10,11 @@ After building context through scanning and ingestion, you'll want to refine it Agents should refine context in this order: 1. `ktx sl list --json` — discover available sources and connection ids. -2. `ktx sl read --connection-id ` — inspect the current YAML. -3. Edit the source YAML directly or use `ktx sl write`. +2. `ktx sl search --json` — find source candidates for a concept. +3. Edit the source YAML directly in `semantic-layer//`. 4. `ktx sl validate --connection-id ` — verify columns, joins, and table references. 5. `ktx sl query ... --format sql` — compile a representative query without executing it. -6. `ktx wiki search ...` and `ktx wiki write ...` — add business context that does not belong in schema YAML. +6. `ktx wiki search ...` — check business context captured by ingest or memory. ## Semantic Sources @@ -33,13 +33,14 @@ ktx sl list --connection-id my-postgres ktx sl list --json ``` -### Reading a source +### Searching sources ```bash -ktx sl read orders --connection-id my-postgres +ktx sl search "revenue" --connection-id my-postgres --json ``` -This prints the full YAML definition for the source. +Search returns ranked source summaries. To inspect or edit a source, open the +YAML file under `semantic-layer//`. ### The source schema @@ -147,25 +148,10 @@ Column visibility controls what agents see: | `internal` | Available for joins and measures but not shown to agents | | `hidden` | Excluded entirely — useful for ETL columns | -### Writing a source +### Editing a source -```bash -ktx sl write orders --connection-id my-postgres --yaml ' -name: orders -table: public.orders -grain: [order_id] -columns: - - name: order_id - type: string - - name: total_amount - type: number -measures: - - name: total_revenue - expr: SUM(total_amount) -' -``` - -You can also edit source files directly — they live at `semantic-layer//.yaml` in your project directory. +Edit source files directly. They live at +`semantic-layer//.yaml` in your project directory. ### Validating sources @@ -225,11 +211,10 @@ The query planner is grain-aware — it understands the cardinality of joins and ### Workflow: edit and validate a source -1. `ktx sl read orders --connection-id my-postgres > /tmp/orders.yaml` — capture the current definition. -2. Edit `/tmp/orders.yaml` to add columns, measures, joins, or descriptions. -3. `ktx sl write orders --connection-id my-postgres --yaml "$(cat /tmp/orders.yaml)"` — write the updated source. -4. `ktx sl validate orders --connection-id my-postgres` — check the definition against the live schema. -5. `ktx sl query --connection-id my-postgres --measure total_revenue --dimension order_date --format sql` — compile a representative query. +1. Open `semantic-layer/my-postgres/orders.yaml`. +2. Edit the file to add columns, measures, joins, or descriptions. +3. `ktx sl validate orders --connection-id my-postgres` — check the definition against the live schema. +4. `ktx sl query --connection-id my-postgres --measure total_revenue --dimension order_date --format sql` — compile a representative query. If validation fails, fix the YAML before asking an agent to use the source. Common validation failures are missing columns, invalid join targets, and measure expressions that reference fields outside the source. @@ -260,42 +245,16 @@ knowledge/ - **Global pages** apply across all connections — business definitions, metric standards, company terminology. - **User-scoped pages** are private to a user ID — personal notes, local gotchas, or context you do not want shared globally. -### Writing pages +### Editing pages -```bash -ktx wiki write order-status-definitions \ - --scope global \ - --summary "Business definitions for order status values" \ - --content "## Order Statuses - -- **pending**: Order placed but not yet processed -- **confirmed**: Payment received, awaiting fulfillment -- **shipped**: Order dispatched to carrier -- **delivered**: Order received by customer -- **cancelled**: Order cancelled before shipment - -Orders in pending status for more than 48 hours are flagged for review." \ - --tag orders \ - --tag definitions \ - --sl-ref orders -``` - -Write flags: - -| Flag | Description | -|------|-------------| -| `--scope ` | `global` (default) or `user` | -| `--summary ` | Short description for search results (required) | -| `--content ` | Full Markdown content (required) | -| `--tag ` | Categorization tag (repeatable) | -| `--ref ` | Reference to external resources (repeatable) | -| `--sl-ref ` | Link to a semantic source (repeatable) | +Create and edit knowledge pages directly as Markdown files in the `knowledge/` +directory. Ingest and memory capture also create these pages automatically. Knowledge page fields: | Field | Required | Description | |-------|----------|-------------| -| Key | Yes | Stable page identifier passed to `ktx wiki read` | +| Key | Yes | Stable page identifier used as the Markdown filename | | Summary | Yes | Short text shown in search results | | Content | Yes | Full Markdown business context | | Scope | No | `global` for shared context or `user` for user-scoped notes | @@ -303,20 +262,12 @@ Knowledge page fields: | External refs | No | Links or identifiers for source-of-truth systems | | Semantic-layer refs | No | Source names the page explains or constrains | -You can also create and edit knowledge pages directly as Markdown files in the `knowledge/` directory. - ### Listing pages ```bash ktx wiki list ``` -### Reading a page - -```bash -ktx wiki read order-status-definitions -``` - ### Searching ```bash @@ -328,9 +279,9 @@ Search uses both full-text matching and semantic similarity — it finds relevan ### Workflow: add searchable business context 1. Search first: `ktx wiki search "order status definitions"`. -2. If no page already covers the rule, write a page with `ktx wiki write`. -3. Include a concise `--summary`; agents see this before loading full content. -4. Add `--tag` values for the business area and `--sl-ref` values for related semantic sources. +2. If no page already covers the rule, create or edit a Markdown file under `knowledge/global/`. +3. Include concise frontmatter; agents see the summary before loading full content. +4. Add `tags` values for the business area and `sl_refs` values for related semantic sources. 5. Search again with the user's likely wording to confirm the page is discoverable. ## Common errors @@ -341,4 +292,4 @@ Search uses both full-text matching and semantic similarity — it finds relevan | Query compilation double-counts a measure | Join relationship or grain is missing or wrong | Add `grain` and explicit `relationship` values, then validate and recompile | | Agent cannot find a metric | Measure name or description does not match business terminology | Add a measure description and a knowledge page with common synonyms | | Knowledge search misses a page | Summary and tags do not include likely user wording | Rewrite the summary and add relevant tags, then search again | -| `ktx sl write` changes are hard to review | Large YAML was passed inline | Edit the source file directly or write from a temporary file, then review the git diff | +| Semantic-layer changes are hard to review | The YAML edit is too large or unfocused | Split the change into smaller source-file edits, then review the git diff | diff --git a/docs-site/content/docs/integrations/agent-clients.mdx b/docs-site/content/docs/integrations/agent-clients.mdx index 8a055fda..61538140 100644 --- a/docs-site/content/docs/integrations/agent-clients.mdx +++ b/docs-site/content/docs/integrations/agent-clients.mdx @@ -34,11 +34,9 @@ description: Use local KTX semantic context and wiki knowledge for this project. Available commands: - `ktx status --json --project-dir /path/to/project` - `ktx sl list --json --project-dir /path/to/project` -- `ktx sl list --json --project-dir /path/to/project --query ''` -- `ktx sl read '' --json --project-dir /path/to/project --connection-id ''` +- `ktx sl search '' --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 @@ -127,12 +125,8 @@ All supported agent clients call the same KTX CLI commands: |---------|-------------| | `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 search --json` | Search semantic-layer sources | | `ktx sl validate --connection-id ` | Validate semantic source definitions | | `ktx sl query --json` | Execute a semantic-layer query when semantic compute is configured | diff --git a/packages/cli/src/command-schemas.ts b/packages/cli/src/command-schemas.ts index 5caece1f..e1365d86 100644 --- a/packages/cli/src/command-schemas.ts +++ b/packages/cli/src/command-schemas.ts @@ -3,19 +3,6 @@ import { z } from 'zod'; const projectDirSchema = z.string().min(1); const stringArraySchema = z.array(z.string()); -export const wikiWriteCommandSchema = z.object({ - command: z.literal('write'), - projectDir: projectDirSchema, - key: z.string().min(1), - scope: z.enum(['GLOBAL', 'USER']), - userId: z.string().min(1), - summary: z.string().min(1), - content: z.string().min(1), - tags: stringArraySchema, - refs: stringArraySchema, - slRefs: stringArraySchema, -}); - const orderBySchema = z.union([ z.string().min(1), z.object({ diff --git a/packages/cli/src/commands/knowledge-commands.ts b/packages/cli/src/commands/knowledge-commands.ts index f8d716f7..382ebf0a 100644 --- a/packages/cli/src/commands/knowledge-commands.ts +++ b/packages/cli/src/commands/knowledge-commands.ts @@ -1,11 +1,9 @@ -import { type Command, Option } from '@commander-js/extra-typings'; +import { type Command } from '@commander-js/extra-typings'; 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'; @@ -19,7 +17,7 @@ async function runKnowledgeArgs(context: KtxCliCommandContext, args: KtxKnowledg export function registerWikiCommands(program: Command, context: KtxCliCommandContext): void { const wiki = program .command('wiki') - .description('List, read, search, or write local wiki pages') + .description('List or search local wiki pages') .showHelpAfterError() .addHelpText( 'after', @@ -40,22 +38,6 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon }); }); - wiki - .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; json?: boolean }, command) => { - await runKnowledgeArgs(context, { - command: 'read', - projectDir: resolveCommandProjectDir(command), - key, - userId: options.userId, - json: options.json, - }); - }); - wiki .command('search') .description('Search local wiki pages') @@ -73,31 +55,4 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon ...(options.limit !== undefined ? { limit: options.limit } : {}), }); }); - - wiki - .command('write') - .description('Write one local wiki page') - .argument('', 'Wiki page key') - .option('--user-id ', 'Local user id', 'local') - .addOption(new Option('--scope ', 'global or user').choices(['global', 'user']).default('global')) - .requiredOption('--summary ', 'Wiki summary') - .requiredOption('--content ', 'Wiki content') - .option('--tag ', 'Wiki tag; repeatable', collectOption, []) - .option('--ref ', 'Wiki ref; repeatable', collectOption, []) - .option('--sl-ref ', 'Semantic-layer ref; repeatable', collectOption, []) - .action(async (key: string, options, command) => { - const args = wikiWriteCommandSchema.parse({ - command: 'write', - projectDir: resolveCommandProjectDir(command), - key, - scope: options.scope === 'user' ? 'USER' : 'GLOBAL', - userId: options.userId, - summary: options.summary, - content: options.content, - tags: options.tag, - refs: options.ref, - slRefs: options.slRef, - }); - await runKnowledgeArgs(context, args); - }); } diff --git a/packages/cli/src/commands/sl-commands.ts b/packages/cli/src/commands/sl-commands.ts index e1b985a3..d23674cd 100644 --- a/packages/cli/src/commands/sl-commands.ts +++ b/packages/cli/src/commands/sl-commands.ts @@ -41,7 +41,7 @@ async function runSlArgs(context: KtxCliCommandContext, args: KtxSlArgs): Promis export function registerSlCommands(program: Command, context: KtxCliCommandContext, commandName = 'sl'): void { const sl = program .command(commandName) - .description('List, read, validate, query, or write local semantic-layer sources') + .description('List, search, validate, or query local semantic-layer sources') .showHelpAfterError() .addHelpText( 'after', @@ -51,7 +51,31 @@ 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', + 'plain', + 'json', + ]), + ) + .option('--json', 'Shortcut for --output=json (overrides --output)', false) + .action( + async (options: { connectionId?: string; output?: 'pretty' | 'plain' | 'json'; json?: boolean }, command) => { + await runSlArgs(context, { + command: 'list', + projectDir: resolveCommandProjectDir(command), + connectionId: options.connectionId, + output: options.output, + json: options.json, + }); + }, + ); + + sl.command('search') + .description('Search semantic-layer sources') + .argument('', 'Search query') + .option('--connection-id ', 'KTX connection id') + .option('--limit ', 'Maximum search results', parsePositiveIntegerOption) .addOption( new Option('--output ', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([ 'pretty', @@ -62,35 +86,22 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte .option('--json', 'Shortcut for --output=json (overrides --output)', false) .action( async ( - options: { connectionId?: string; query?: string; output?: 'pretty' | 'plain' | 'json'; json?: boolean }, + query: string, + options: { connectionId?: string; limit?: number; 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, - }); + await runSlArgs(context, { + command: 'search', + projectDir: resolveCommandProjectDir(command), + connectionId: options.connectionId, + query, + ...(options.limit !== undefined ? { limit: options.limit } : {}), + 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') - .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, - }); - }); - sl.command('validate') .description('Validate a semantic-layer source') .argument('', 'Semantic-layer source name') @@ -104,21 +115,6 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte }); }); - sl.command('write') - .description('Write a semantic-layer source') - .argument('', 'Semantic-layer source name') - .requiredOption('--connection-id ', 'KTX connection id') - .requiredOption('--yaml ', 'Semantic-layer source YAML') - .action(async (sourceName: string, options: { connectionId: string; yaml: string }, command) => { - await runSlArgs(context, { - command: 'write', - projectDir: resolveCommandProjectDir(command), - connectionId: options.connectionId, - sourceName, - yaml: options.yaml, - }); - }); - sl.command('query') .description('Compile or execute a semantic-layer query') .option('--connection-id ', 'KTX connection id') diff --git a/packages/cli/src/example-smoke.test.ts b/packages/cli/src/example-smoke.test.ts index 221c20f2..f1670544 100644 --- a/packages/cli/src/example-smoke.test.ts +++ b/packages/cli/src/example-smoke.test.ts @@ -79,12 +79,6 @@ describe('standalone local warehouse example', () => { 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(['wiki', 'read', 'revenue', '--json', '--project-dir', projectDir]); - expect(knowledgeRead).toMatchObject({ code: 0, stderr: '' }); - expect(parseJsonOutput<{ data: { content: string } }>(knowledgeRead.stdout).data.content).toContain( - 'Revenue is paid order amount after refund adjustments.', - ); - const slList = await runBuiltCli(['sl', 'list', '--json', '--project-dir', projectDir, '--connection-id', 'warehouse']); expect(slList).toMatchObject({ code: 0, stderr: '' }); expect( @@ -93,9 +87,9 @@ describe('standalone local warehouse example', () => { ).data.items, ).toContainEqual(expect.objectContaining({ connectionId: 'warehouse', name: 'orders', columnCount: 3 })); - const slRead = await runBuiltCli([ + const slSearch = await runBuiltCli([ 'sl', - 'read', + 'search', 'orders', '--json', '--connection-id', @@ -103,8 +97,10 @@ describe('standalone local warehouse example', () => { '--project-dir', projectDir, ]); - expect(slRead).toMatchObject({ code: 0, stderr: '' }); - expect(parseJsonOutput<{ data: { yaml: string } }>(slRead.stdout).data.yaml).toContain('name: orders'); + expect(slSearch).toMatchObject({ code: 0, stderr: '' }); + expect( + parseJsonOutput<{ data: { items: Array<{ connectionId: string; name: string }> } }>(slSearch.stdout).data.items, + ).toContainEqual(expect.objectContaining({ connectionId: 'warehouse', name: 'orders' })); const ingest = await runBuiltCli([ 'ingest', diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 9064143a..817653f6 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -139,6 +139,56 @@ describe('runKtxCli', () => { expect(testIo.stderr()).toBe(''); }); + it('rejects removed public wiki and sl read/write commands', async () => { + const sl = vi.fn(async () => 0); + const knowledge = vi.fn(async () => 0); + + for (const argv of [ + ['--project-dir', tempDir, 'wiki', 'read', 'revenue'], + ['--project-dir', tempDir, 'wiki', 'write', 'revenue', '--summary', 'Revenue', '--content', 'Revenue.'], + ['--project-dir', tempDir, 'sl', 'read', 'orders', '--connection-id', 'warehouse'], + ['--project-dir', tempDir, 'sl', 'write', 'orders', '--connection-id', 'warehouse', '--yaml', 'name: orders'], + ]) { + const io = makeIo(); + await expect(runKtxCli(argv, io.io, { knowledge, sl })).resolves.toBe(1); + expect(io.stderr()).toMatch(/unknown command|error:/); + } + + expect(knowledge).not.toHaveBeenCalled(); + expect(sl).not.toHaveBeenCalled(); + }); + + it('routes sl search and rejects the old sl list --query flag', async () => { + const sl = vi.fn(async () => 0); + + const searchIo = makeIo(); + await expect( + runKtxCli( + ['--project-dir', tempDir, 'sl', 'search', 'revenue', '--connection-id', 'warehouse', '--limit', '5', '--json'], + searchIo.io, + { sl }, + ), + ).resolves.toBe(0); + expect(sl).toHaveBeenCalledWith( + { + command: 'search', + projectDir: tempDir, + connectionId: 'warehouse', + query: 'revenue', + limit: 5, + json: true, + output: undefined, + }, + searchIo.io, + ); + + const listIo = makeIo(); + await expect( + runKtxCli(['--project-dir', tempDir, 'sl', 'list', '--query', 'revenue'], listIo.io, { sl }), + ).resolves.toBe(1); + expect(listIo.stderr()).toContain("unknown option '--query'"); + }); + it('routes runtime management commands with the CLI package version', async () => { const runtime = vi.fn(async () => 0); const installIo = makeIo(); diff --git a/packages/cli/src/knowledge.test.ts b/packages/cli/src/knowledge.test.ts index db794289..1982fe1c 100644 --- a/packages/cli/src/knowledge.test.ts +++ b/packages/cli/src/knowledge.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { initKtxProject } from '@ktx/context/project'; import type { KtxEmbeddingPort } from '@ktx/context'; +import { type LocalKnowledgeScope, writeLocalKnowledgePage } from '@ktx/context/wiki'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { runKtxKnowledge } from './knowledge.js'; @@ -40,6 +41,29 @@ class FakeEmbeddingPort implements KtxEmbeddingPort { } } +async function seedKnowledgePage(input: { + projectDir: string; + key: string; + summary: string; + content: string; + scope?: LocalKnowledgeScope; + tags?: string[]; + refs?: string[]; + slRefs?: string[]; +}): Promise { + const project = await initKtxProject({ projectDir: input.projectDir, projectName: 'warehouse' }); + await writeLocalKnowledgePage(project, { + key: input.key, + scope: input.scope ?? 'GLOBAL', + userId: 'local', + summary: input.summary, + content: input.content, + tags: input.tags ?? [], + refs: input.refs ?? [], + slRefs: input.slRefs ?? [], + }); +} + describe('runKtxKnowledge', () => { let tempDir: string; @@ -51,36 +75,16 @@ describe('runKtxKnowledge', () => { await rm(tempDir, { recursive: true, force: true }); }); - it('writes, reads, lists, and searches knowledge pages', async () => { + it('lists and searches knowledge pages', async () => { const projectDir = join(tempDir, 'project'); - await initKtxProject({ projectDir, projectName: 'warehouse' }); - - const writeIo = makeIo(); - 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'], - }, - writeIo.io, - ), - ).resolves.toBe(0); - expect(writeIo.stdout()).toContain('Wrote knowledge/global/metrics-revenue.md'); - - const readIo = makeIo(); - await expect( - runKtxKnowledge({ command: 'read', projectDir, key: 'metrics-revenue', userId: 'local' }, readIo.io), - ).resolves.toBe(0); - expect(readIo.stdout()).toContain('# metrics-revenue'); - expect(readIo.stdout()).toContain('Revenue is paid order value.'); + await seedKnowledgePage({ + projectDir, + key: 'metrics-revenue', + summary: 'Revenue', + content: 'Revenue is paid order value.', + tags: ['finance'], + slRefs: ['orders'], + }); const listIo = makeIo(); await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0); @@ -93,27 +97,16 @@ describe('runKtxKnowledge', () => { expect(searchIo.stdout()).toContain('metrics-revenue'); }); - it('prints wiki list, search, and read as public JSON envelopes', async () => { + it('prints wiki list and search 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); + await seedKnowledgePage({ + projectDir, + key: 'metrics-revenue', + summary: 'Revenue', + content: 'Revenue is paid order value.', + tags: ['finance'], + slRefs: ['orders'], + }); const listIo = makeIo(); await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', json: true }, listIo.io)).resolves.toBe( @@ -137,48 +130,6 @@ describe('runKtxKnowledge', () => { 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' }); - - const writeIo = makeIo(); - await expect( - runKtxKnowledge( - { - command: 'write', - projectDir, - key: 'orbit/company-overview', - scope: 'GLOBAL', - userId: 'local', - summary: 'Orbit', - content: 'Orbit overview.', - tags: [], - refs: [], - slRefs: [], - }, - writeIo.io, - ), - ).resolves.toBe(1); - - expect(writeIo.stderr()).toContain( - 'Invalid wiki key "orbit/company-overview". Wiki keys must be flat; use "orbit-company-overview".', - ); - expect(writeIo.stdout()).toBe(''); }); it('explains empty search results for a project without wiki pages', async () => { @@ -192,30 +143,19 @@ describe('runKtxKnowledge', () => { expect(searchIo.stdout()).toBe(''); expect(searchIo.stderr()).toContain('No local wiki pages found'); - expect(searchIo.stderr()).toContain('ktx wiki write'); + expect(searchIo.stderr()).toContain('Run ingest'); + expect(searchIo.stderr()).not.toContain('ktx wiki write'); }); it('uses configured embeddings for semantic wiki search', async () => { const projectDir = join(tempDir, 'semantic-project'); - await initKtxProject({ projectDir, projectName: 'warehouse' }); - - await expect( - runKtxKnowledge( - { - command: 'write', - projectDir, - key: 'active-contract-arr-open-tickets', - scope: 'GLOBAL', - userId: 'local', - summary: 'Active Contract ARR Ranked by Open Support Ticket Count', - content: 'Accounts ranked by annual recurring contract value and support ticket load.', - tags: ['historic-sql'], - refs: [], - slRefs: [], - }, - makeIo().io, - ), - ).resolves.toBe(0); + await seedKnowledgePage({ + projectDir, + key: 'active-contract-arr-open-tickets', + summary: 'Active Contract ARR Ranked by Open Support Ticket Count', + content: 'Accounts ranked by annual recurring contract value and support ticket load.', + tags: ['historic-sql'], + }); const searchIo = makeIo(); await expect( diff --git a/packages/cli/src/knowledge.ts b/packages/cli/src/knowledge.ts index 5c5df1ea..0d1e194b 100644 --- a/packages/cli/src/knowledge.ts +++ b/packages/cli/src/knowledge.ts @@ -4,31 +4,12 @@ import { type KtxEmbeddingPort, } from '@ktx/context'; import { loadKtxProject } from '@ktx/context/project'; -import { - type LocalKnowledgeScope, - listLocalKnowledgePages, - readLocalKnowledgePage, - searchLocalKnowledgePages, - writeLocalKnowledgePage, -} from '@ktx/context/wiki'; +import { listLocalKnowledgePages, searchLocalKnowledgePages } from '@ktx/context/wiki'; import { writeJsonResult } from './io/print-list.js'; export type KtxKnowledgeArgs = | { 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; - key: string; - scope: LocalKnowledgeScope; - userId: string; - summary: string; - content: string; - tags: string[]; - refs: string[]; - slRefs: string[]; - }; + | { command: 'search'; projectDir: string; query: string; userId: string; json?: boolean; limit?: number }; interface KtxKnowledgeIo { stdout: { write(chunk: string): void }; @@ -75,25 +56,6 @@ export async function runKtxKnowledge( } return 0; } - if (args.command === 'read') { - const page = await readLocalKnowledgePage(project, { key: args.key, userId: args.userId }); - 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`); - io.stdout.write(`${page.content}\n`); - return 0; - } if (args.command === 'search') { const results = await searchLocalKnowledgePages(project, { query: args.query, @@ -113,7 +75,7 @@ export async function runKtxKnowledge( const pages = await listLocalKnowledgePages(project, { userId: args.userId }); if (pages.length === 0) { io.stderr.write( - `No local wiki pages found in ${project.projectDir}. Create one with \`ktx wiki write --summary --content \` or run ingest.\n`, + `No local wiki pages found in ${project.projectDir}. Run ingest to capture wiki context, then retry the search.\n`, ); } else { io.stderr.write( @@ -127,19 +89,8 @@ export async function runKtxKnowledge( } return 0; } - - const write = await writeLocalKnowledgePage(project, { - key: args.key, - scope: args.scope, - userId: args.userId, - summary: args.summary, - content: args.content, - tags: args.tags, - refs: args.refs, - slRefs: args.slRefs, - }); - io.stdout.write(`Wrote ${write.path}\n`); - return 0; + const _exhaustive: never = args; + throw new Error(`Unsupported wiki command: ${JSON.stringify(_exhaustive)}`); } catch (error) { io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); return 1; diff --git a/packages/cli/src/setup-agents.ts b/packages/cli/src/setup-agents.ts index 3c7829c7..da9486f5 100644 --- a/packages/cli/src/setup-agents.ts +++ b/packages/cli/src/setup-agents.ts @@ -138,8 +138,7 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun '', `- \`${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, ['sl', 'search', '', ...projectDirArgs, '--connection-id', ''])}\``, `- \`${ktxCommandLine(input.launcher, [ 'sl', 'query', @@ -153,7 +152,6 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun '100', ])}\``, `- \`${ktxCommandLine(input.launcher, ['wiki', 'search', '', ...projectDirArgs, '--limit', '10'])}\``, - `- \`${ktxCommandLine(input.launcher, ['wiki', 'read', '', ...projectDirArgs])}\``, '', 'Use semantic-layer queries before direct database access. Do not print secrets or credential references.', '', diff --git a/packages/cli/src/sl.test.ts b/packages/cli/src/sl.test.ts index 8d360c58..48c7f4c7 100644 --- a/packages/cli/src/sl.test.ts +++ b/packages/cli/src/sl.test.ts @@ -38,6 +38,22 @@ function makeIo() { }; } +async function seedSlSource(input: { + projectDir: string; + connectionId?: string; + sourceName?: string; + yaml?: string; +}): Promise { + const project = await initKtxProject({ projectDir: input.projectDir, projectName: 'warehouse' }); + await project.fileStore.writeFile( + `semantic-layer/${input.connectionId ?? 'warehouse'}/${input.sourceName ?? 'orders'}.yaml`, + input.yaml ?? ORDERS_YAML, + 'ktx', + 'ktx@example.com', + 'Add semantic-layer source', + ); +} + describe('runKtxSl', () => { let tempDir: string; @@ -49,24 +65,9 @@ describe('runKtxSl', () => { await rm(tempDir, { recursive: true, force: true }); }); - it('writes, validates, reads, and lists semantic-layer sources', async () => { + it('validates, lists, and searches semantic-layer sources', async () => { const projectDir = join(tempDir, 'project'); - await initKtxProject({ projectDir, projectName: 'warehouse' }); - - const writeIo = makeIo(); - await expect( - runKtxSl( - { - command: 'write', - projectDir, - connectionId: 'warehouse', - sourceName: 'orders', - yaml: ORDERS_YAML, - }, - writeIo.io, - ), - ).resolves.toBe(0); - expect(writeIo.stdout()).toContain('Wrote semantic-layer/warehouse/orders.yaml'); + await seedSlSource({ projectDir }); const validateIo = makeIo(); await expect( @@ -74,62 +75,49 @@ describe('runKtxSl', () => { ).resolves.toBe(0); expect(validateIo.stdout()).toContain('Valid semantic-layer source: warehouse/orders'); - const readIo = makeIo(); - await expect(runKtxSl({ command: 'read', projectDir, connectionId: 'warehouse', sourceName: 'orders' }, readIo.io)) - .resolves.toBe(0); - expect(readIo.stdout()).toContain('name: orders'); - const listIo = makeIo(); await expect(runKtxSl({ command: 'list', projectDir, connectionId: 'warehouse' }, listIo.io)).resolves.toBe(0); expect(listIo.stdout()).toContain('warehouse\torders\tcolumns=1\tmeasures=0\tjoins=0'); + + const searchIo = makeIo(); + await expect( + runKtxSl({ command: 'search', projectDir, connectionId: 'warehouse', query: 'order', json: true }, searchIo.io), + ).resolves.toBe(0); + expect(JSON.parse(searchIo.stdout())).toMatchObject({ + kind: 'list', + data: { + items: [ + expect.objectContaining({ + connectionId: 'warehouse', + name: 'orders', + score: expect.any(Number), + }), + ], + }, + meta: { command: 'sl search' }, + }); }); - it('prints semantic-layer reads and searched lists as public JSON envelopes', async () => { + it('prints semantic-layer list and search 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'), - }, + await seedSlSource({ + projectDir, + yaml: [ + 'name: orders', + 'table: public.orders', + 'description: Paid order facts', + 'grain: [order_id]', + 'columns:', + ' - name: order_id', + ' type: string', + '', + ].join('\n'), }); const listIo = makeIo(); await expect( runKtxSl( - { command: 'list', projectDir, connectionId: 'warehouse', query: 'paid', json: true }, + { command: 'search', projectDir, connectionId: 'warehouse', query: 'paid', json: true }, listIo.io, ), ).resolves.toBe(0); @@ -145,7 +133,7 @@ describe('runKtxSl', () => { }), ], }, - meta: { command: 'sl list' }, + meta: { command: 'sl search' }, }); }); @@ -566,13 +554,7 @@ joins: [] it('emits sl list as a JSON envelope when output=json', async () => { const projectDir = join(tempDir, 'project'); - await initKtxProject({ projectDir, projectName: 'warehouse' }); - - const writeIo = makeIo(); - await runKtxSl( - { command: 'write', projectDir, connectionId: 'warehouse', sourceName: 'orders', yaml: ORDERS_YAML }, - writeIo.io, - ); + await seedSlSource({ projectDir }); const listIo = makeIo(); const code = await runKtxSl( @@ -604,13 +586,7 @@ joins: [] it('emits sl list with grouping and Clack-style framing when output=pretty', async () => { const projectDir = join(tempDir, 'project'); - await initKtxProject({ projectDir, projectName: 'warehouse' }); - - const writeIo = makeIo(); - await runKtxSl( - { command: 'write', projectDir, connectionId: 'warehouse', sourceName: 'orders', yaml: ORDERS_YAML }, - writeIo.io, - ); + await seedSlSource({ projectDir }); const listIo = makeIo(); const code = await runKtxSl( diff --git a/packages/cli/src/sl.ts b/packages/cli/src/sl.ts index ebf3eca7..baff239b 100644 --- a/packages/cli/src/sl.ts +++ b/packages/cli/src/sl.ts @@ -13,10 +13,10 @@ import { readLocalSlSource, searchLocalSlSources, validateLocalSlSource, - writeLocalSlSource, + type LocalSlSourceSearchResult, + type LocalSlSourceSummary, type SemanticLayerQueryInput, } from '@ktx/context/sl'; -import { writeJsonResult } from './io/print-list.js'; import { createManagedPythonSemanticLayerComputePort, type KtxManagedPythonInstallPolicy, @@ -28,10 +28,17 @@ profileMark('module:sl'); type SlQueryFormat = 'json' | 'sql'; export type KtxSlArgs = - | { command: 'list'; projectDir: string; connectionId?: string; query?: string; output?: string; json?: boolean } - | { command: 'read'; projectDir: string; connectionId: string; sourceName: string; json?: boolean } + | { command: 'list'; projectDir: string; connectionId?: string; output?: string; json?: boolean } + | { + command: 'search'; + projectDir: string; + connectionId?: string; + query: string; + limit?: number; + output?: 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; @@ -73,6 +80,35 @@ function slSearchEmbeddingService(project: KtxLocalProject, deps: KtxSlDeps): Kt return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null; } +async function printSlSources(input: { + rows: ReadonlyArray; + command: 'sl list' | 'sl search'; + output?: string; + json?: boolean; + io: KtxSlIo; + emptyMessage: string; +}): Promise { + const { resolveOutputMode } = await import('./io/mode.js'); + const { printList } = await import('./io/print-list.js'); + const mode = resolveOutputMode({ explicit: input.output, json: input.json, io: input.io }); + printList({ + rows: input.rows, + columns: [ + { key: 'connectionId', label: 'CONNECTION', plain: '' }, + { key: 'name', label: 'NAME', plain: '' }, + { key: 'columnCount', label: 'COLS', plain: 'columns=', dim: true }, + { key: 'measureCount', label: 'MEASURES', plain: 'measures=', dim: true }, + { key: 'joinCount', label: 'JOINS', plain: 'joins=', dim: true }, + { key: 'description', label: 'DESCRIPTION', plain: false, optional: true, dim: true }, + ], + groupBy: 'connectionId', + emptyMessage: input.emptyMessage, + command: input.command, + mode, + io: input.io, + }); +} + 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)) { @@ -85,51 +121,32 @@ export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: Ktx try { const project = await (deps.loadProject ?? loadKtxProject)({ projectDir: args.projectDir }); if (args.command === 'list') { - 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 }); - printList({ + const sources = await listLocalSlSources(project, { connectionId: args.connectionId }); + await printSlSources({ rows: sources, - columns: [ - { key: 'connectionId', label: 'CONNECTION', plain: '' }, - { key: 'name', label: 'NAME', plain: '' }, - { key: 'columnCount', label: 'COLS', plain: 'columns=', dim: true }, - { key: 'measureCount', label: 'MEASURES', plain: 'measures=', dim: true }, - { key: 'joinCount', label: 'JOINS', plain: 'joins=', dim: true }, - { key: 'description', label: 'DESCRIPTION', plain: false, optional: true, dim: true }, - ], - groupBy: 'connectionId', emptyMessage: `No semantic-layer sources found in ${project.projectDir}`, command: 'sl list', - mode, + output: args.output, + json: args.json, io, }); return 0; } - if (args.command === 'read') { - const source = await readLocalSlSource(project, { + if (args.command === 'search') { + const sources = await searchLocalSlSources(project, { connectionId: args.connectionId, - sourceName: args.sourceName, + query: args.query, + embeddingService: slSearchEmbeddingService(project, deps), + limit: args.limit, + }); + await printSlSources({ + rows: sources, + emptyMessage: `No semantic-layer sources matched "${args.query}" in ${project.projectDir}`, + command: 'sl search', + output: args.output, + json: args.json, + io, }); - 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; } if (args.command === 'validate') { @@ -178,14 +195,8 @@ export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: Ktx io.stdout.write(`${JSON.stringify(result, null, 2)}\n`); return 0; } - - const write = await writeLocalSlSource(project, { - connectionId: args.connectionId, - sourceName: args.sourceName, - yaml: args.yaml, - }); - io.stdout.write(`Wrote ${write.path}\n`); - return 0; + const _exhaustive: never = args; + throw new Error(`Unsupported sl command: ${JSON.stringify(_exhaustive)}`); } catch (error) { io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); return 1; diff --git a/scripts/examples-docs.test.mjs b/scripts/examples-docs.test.mjs index 793566ed..62a25cf7 100644 --- a/scripts/examples-docs.test.mjs +++ b/scripts/examples-docs.test.mjs @@ -154,10 +154,9 @@ describe('standalone example docs', () => { for (const command of [ 'ktx status --json', 'ktx sl list --json', - 'ktx sl read orders --json', + 'ktx sl search "revenue" --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 ab8d7adf..7e184dde 100644 --- a/scripts/package-artifacts.mjs +++ b/scripts/package-artifacts.mjs @@ -729,23 +729,22 @@ try { 'exec', 'ktx', 'sl', - 'list', + 'search', + 'orders', '--json', '--connection-id', 'warehouse', - '--query', - 'orders', '--project-dir', projectDir, ]); - const slSearchJson = parseJsonResult('ktx sl list', slSearch); + const slSearchJson = parseJsonResult('ktx sl search', 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'); + process.stdout.write('ktx sl search 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 9fe5a2c1..7694ddc3 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -459,7 +459,7 @@ describe('verification snippets', () => { assert.match(source, /knowledge', 'global', 'revenue\.md'/); 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*'sl',\s*'list'/); + assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'sl',\s*'search',\s*'orders'/); assert.match(source, /orders\.order_count/); assert.match(source, /node:sqlite/); assert.match(source, /driver: sqlite/);