Merge pull request #1 from Kaelio/rename-klo-to-ktx

Rename KLO to KTX
This commit is contained in:
Andrey Avtomonov 2026-05-10 23:55:13 +02:00 committed by GitHub
commit d89be2390f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
704 changed files with 10205 additions and 10255 deletions

View file

@ -1,4 +1,4 @@
name: KLO CI
name: KTX CI
on:
push:
@ -11,7 +11,7 @@ permissions:
contents: read
concurrency:
group: klo-ci-${{ github.ref }}
group: ktx-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
@ -62,7 +62,7 @@ jobs:
- name: Upload package artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: klo-package-artifacts-${{ github.sha }}
name: ktx-package-artifacts-${{ github.sha }}
path: |
dist/artifacts/manifest.json
dist/artifacts/npm/*.tgz

2
.gitignore vendored
View file

@ -39,7 +39,7 @@ yarn-error.log*
.pnpm-debug.log*
# Local project runtime state
.klo/
.ktx/
*.db
*.sqlite
*.sqlite3

View file

@ -22,7 +22,7 @@ database migrations, ORPC contracts, or `python-service/` layout exist here.
- **MUST**: Remove dead code; do not leave commented-out code, unused wrappers,
or empty directories.
- **MUST**: Keep package/public API changes intentional. Do not add compatibility
wrappers for old KLO names unless the user explicitly asks for a migration
wrappers for old KTX names unless the user explicitly asks for a migration
bridge.
### Absolute Prohibitions
@ -65,14 +65,14 @@ KTX is a pnpm + uv workspace.
- Core context package: `packages/context`
- LLM package: `packages/llm`
- Database connectors: `packages/connector-*`
- Python semantic layer: `python/klo-sl`
- Python daemon: `python/klo-daemon`
- Python semantic layer: `python/ktx-sl`
- Python daemon: `python/ktx-daemon`
- Examples and fixtures: `examples/`
- Workspace scripts: `scripts/`
- Local agent skills are private overlays. Do not commit `.agents/` or
`.claude/` to this public repository.
Some package names still contain `klo` during the split. Do not mass-rename
Some package names still contain `ktx` during the split. Do not mass-rename
symbols, package names, paths, or docs to `ktx` unless the task asks for that
rename.
@ -86,7 +86,7 @@ pnpm run build
pnpm run type-check
pnpm run test
pnpm run check
pnpm --filter @klo/cli run smoke
pnpm --filter @ktx/cli run smoke
pnpm --filter './packages/*' run build
pnpm --filter './packages/*' run test
pnpm --filter './packages/*' run type-check
@ -97,8 +97,8 @@ pnpm --filter './packages/*' run type-check
```bash
uv sync --all-groups
uv run pytest -q
uv run pytest python/klo-sl/tests -q
uv run pytest python/klo-daemon/tests -q
uv run pytest python/ktx-sl/tests -q
uv run pytest python/ktx-daemon/tests -q
uv run pre-commit run --files [FILES]
```
@ -127,8 +127,8 @@ shared contracts or package exports are affected.
- Build/export changes: `pnpm run build`
- Workspace scripts: `node --test scripts/*.test.mjs` or the specific script
test file
- Python semantic layer: `uv run pytest python/klo-sl/tests -q`
- Python daemon: `uv run pytest python/klo-daemon/tests -q`
- Python semantic layer: `uv run pytest python/ktx-sl/tests -q`
- Python daemon: `uv run pytest python/ktx-daemon/tests -q`
- Python files: also run `uv run pre-commit run --files [FILES]` when
pre-commit is configured
@ -178,15 +178,15 @@ use `PascalCase` without the suffix.
- Use `pathlib` instead of `os.path`.
- Use `logger.exception()` when catching and logging exceptions.
- Prefer explicit exception types over broad `except Exception`.
- Keep `python/klo-sl` focused on semantic-layer planning and SQL generation.
- Keep `python/klo-daemon` focused on portable daemon/API behavior around the
- Keep `python/ktx-sl` focused on semantic-layer planning and SQL generation.
- Keep `python/ktx-daemon` focused on portable daemon/API behavior around the
semantic layer.
### SQL and Structured Parsing
- Prefer AST-based parsing over regex for structured input.
- For SQL, use `sqlglot`; it is already a dependency.
- In `python/klo-sl`, follow the local `python/klo-sl/AGENTS.md` guidance:
- In `python/ktx-sl`, follow the local `python/ktx-sl/AGENTS.md` guidance:
parse expressions with sqlglot, quote reserved identifiers before parsing,
and generate postgres-shaped SQL before final dialect transpilation.
- Regex may be used for non-structural sanitization, but not to interpret SQL

View file

@ -1,14 +1,14 @@
# KLO
# KTX
KLO is a workspace-first context layer for database agents. It stores warehouse
KTX is a workspace-first context layer for database agents. It stores warehouse
memory in a project directory, generates and validates semantic-layer YAML,
indexes knowledge, scans database schemas, and exposes the result through a CLI
and MCP server.
KLO projects are plain files: YAML, Markdown, SQLite state, and generated
KTX projects are plain files: YAML, Markdown, SQLite state, and generated
artifacts. You can inspect them, commit them, and serve them to any MCP client.
## What KLO provides
## What KTX provides
- Durable warehouse memory with semantic-layer sources and knowledge pages.
- Native scan connectors for SQLite, Postgres, MySQL, ClickHouse, SQL Server,
@ -25,8 +25,8 @@ Run the pre-seeded demo from the repository root:
```bash
pnpm install
pnpm run setup:dev
pnpm run klo -- setup demo --no-input
pnpm run klo -- setup demo inspect
pnpm run ktx -- setup demo --no-input
pnpm run ktx -- setup demo inspect
```
The default demo uses packaged sample data and prebuilt context. It does not
@ -35,7 +35,7 @@ require API keys, network access, or an LLM provider.
To replay the packaged ingest run, use:
```bash
pnpm run klo -- setup demo --mode replay --no-input
pnpm run ktx -- setup demo --mode replay --no-input
```
To run the full agentic demo with an LLM provider, set a provider key for the
@ -43,11 +43,11 @@ current process:
```bash
ANTHROPIC_API_KEY=$YOUR_ANTHROPIC_API_KEY \
pnpm run klo -- setup demo --mode full --no-input
pnpm run ktx -- setup demo --mode full --no-input
```
Interactive full-demo setup can prompt for a provider key without writing the
key to `klo.yaml`.
key to `ktx.yaml`.
## Build a local project
@ -57,8 +57,8 @@ Create a project from the repository root:
uv sync --all-packages
source .venv/bin/activate
PROJECT_DIR="$(mktemp -d)/klo-demo"
pnpm run klo -- init "$PROJECT_DIR" --name klo-demo
PROJECT_DIR="$(mktemp -d)/ktx-demo"
pnpm run ktx -- init "$PROJECT_DIR" --name ktx-demo
```
Create a SQLite warehouse:
@ -88,11 +88,11 @@ conn.close()
PY
```
Replace the generated `klo.yaml`:
Replace the generated `ktx.yaml`:
```bash
cat > "$PROJECT_DIR/klo.yaml" <<YAML
project: klo-demo
cat > "$PROJECT_DIR/ktx.yaml" <<YAML
project: ktx-demo
connections:
warehouse:
driver: sqlite
@ -103,7 +103,7 @@ storage:
search: sqlite-fts5
git:
auto_commit: true
author: "klo <klo@example.com>"
author: "ktx <ktx@example.com>"
memory:
auto_commit: true
YAML
@ -112,7 +112,7 @@ YAML
Write and validate a semantic-layer source:
```bash
pnpm run klo -- sl write accounts --project-dir "$PROJECT_DIR" \
pnpm run ktx -- sl write accounts --project-dir "$PROJECT_DIR" \
--connection-id warehouse --yaml 'name: accounts
table: accounts
description: CRM accounts with segmentation attributes.
@ -133,14 +133,14 @@ measures:
joins: []
'
pnpm run klo -- sl validate accounts --project-dir "$PROJECT_DIR" \
pnpm run ktx -- sl validate accounts --project-dir "$PROJECT_DIR" \
--connection-id warehouse
```
Generate SQL and execute the query:
```bash
pnpm run klo -- sl query --project-dir "$PROJECT_DIR" \
pnpm run ktx -- sl query --project-dir "$PROJECT_DIR" \
--connection-id warehouse \
--measure accounts.account_count \
--dimension accounts.segment \
@ -148,7 +148,7 @@ pnpm run klo -- sl query --project-dir "$PROJECT_DIR" \
--limit 5 \
--format sql
pnpm run klo -- sl query --project-dir "$PROJECT_DIR" \
pnpm run ktx -- sl query --project-dir "$PROJECT_DIR" \
--connection-id warehouse \
--measure accounts.account_count \
--dimension accounts.segment \
@ -161,8 +161,8 @@ pnpm run klo -- sl query --project-dir "$PROJECT_DIR" \
List and test the warehouse connection:
```bash
pnpm run klo -- connection list --project-dir "$PROJECT_DIR"
pnpm run klo -- connection test warehouse --project-dir "$PROJECT_DIR"
pnpm run ktx -- connection list --project-dir "$PROJECT_DIR"
pnpm run ktx -- connection test warehouse --project-dir "$PROJECT_DIR"
```
The connection test prints the configured driver and discovered table count:
@ -179,11 +179,11 @@ Scan artifacts are written under
```bash
SCAN_OUTPUT="$(pnpm run klo -- scan warehouse --project-dir "$PROJECT_DIR")"
SCAN_OUTPUT="$(pnpm run ktx -- scan warehouse --project-dir "$PROJECT_DIR")"
printf '%s\n' "$SCAN_OUTPUT"
SCAN_RUN_ID="$(printf '%s\n' "$SCAN_OUTPUT" | awk '/^Run: / { print $2 }')"
pnpm run klo -- scan status --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
pnpm run klo -- scan report --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
pnpm run ktx -- scan status --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
pnpm run ktx -- scan report --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
```
For non-SQLite drivers, prefer credential references such as `--url env:NAME`
@ -195,13 +195,13 @@ Start the Python compute daemon in one terminal:
```bash
source .venv/bin/activate
uv run klo-daemon serve-http --host 127.0.0.1 --port 8765
uv run ktx-daemon serve-http --host 127.0.0.1 --port 8765
```
Start the stdio MCP server in another terminal:
```bash
pnpm run klo -- serve --mcp stdio --project-dir "$PROJECT_DIR" \
pnpm run ktx -- serve --mcp stdio --project-dir "$PROJECT_DIR" \
--user-id local \
--semantic-compute-url http://127.0.0.1:8765 \
--execute-queries
@ -225,8 +225,8 @@ The MCP server exposes `connection_list`, `knowledge_search`,
- `packages/connector-snowflake`: Snowflake scan connector.
- `packages/connector-sqlite`: SQLite scan connector.
- `packages/connector-sqlserver`: SQL Server scan connector.
- `python/klo-sl`: semantic-layer engine.
- `python/klo-daemon`: portable compute service for semantic-layer operations.
- `python/ktx-sl`: semantic-layer engine.
- `python/ktx-daemon`: portable compute service for semantic-layer operations.
## Development
@ -240,11 +240,11 @@ source .venv/bin/activate
uv run pytest
```
Use the optional development binary when you want a local `klo-dev` command:
Use the optional development binary when you want a local `ktx-dev` command:
```bash
pnpm run link:dev
klo-dev --help
ktx-dev --help
```
The repository uses `pnpm` for TypeScript packages and `uv` for Python
@ -267,4 +267,4 @@ pnpm run release:readiness
## License
KLO is licensed under the Apache License, Version 2.0. See `LICENSE`.
KTX is licensed under the Apache License, Version 2.0. See `LICENSE`.

View file

@ -1,15 +1,15 @@
# klo examples
# ktx examples
## local-warehouse
`local-warehouse/` is a runnable standalone KLO project for local CLI and MCP
`local-warehouse/` is a runnable standalone KTX project for local CLI and MCP
smoke testing. It uses the fake ingest adapter and does not require a database
or external app server.
Copy it before running commands:
```bash
pnpm --filter @klo/cli run build
pnpm --filter @ktx/cli run build
EXAMPLE_DIR="$(mktemp -d)/local-warehouse"
cp -R examples/local-warehouse "$EXAMPLE_DIR"
node packages/cli/dist/bin.js knowledge list --project-dir "$EXAMPLE_DIR"
@ -21,7 +21,7 @@ The copied project initializes its own Git repository on first use.
## orbit-relationship-verification
`orbit-relationship-verification/` is a checked-in KLO project used by
`orbit-relationship-verification/` is a checked-in KTX project used by
`pnpm run relationships:verify-orbit`. It points the `orbit` SQLite connection
at the Orbit-style no-declared-constraint relationship fixture and verifies that
relationship enrichment writes nine accepted joins without requiring a local

View file

@ -1,13 +1,13 @@
# Local Warehouse Example
This example is a standalone KLO project that can be copied to a temp directory
This example is a standalone KTX project that can be copied to a temp directory
and used with the local CLI and stdio MCP server. It uses the `fake` ingest
adapter so it does not require a database or external app server.
Run the example from the repository root after building the CLI:
```bash
pnpm --filter @klo/cli run build
pnpm --filter @ktx/cli run build
EXAMPLE_DIR="$(mktemp -d)/local-warehouse"
cp -R examples/local-warehouse "$EXAMPLE_DIR"
node packages/cli/dist/bin.js knowledge list --project-dir "$EXAMPLE_DIR"

View file

@ -8,7 +8,7 @@ storage:
search: sqlite-fts5
git:
auto_commit: true
author: "klo <klo@example.com>"
author: "ktx <ktx@example.com>"
ingest:
adapters:
- fake

View file

@ -1,11 +1,11 @@
# Orbit-style relationship discovery verification
This KLO project backs the default `relationships:verify-orbit` command. It uses
This KTX project backs the default `relationships:verify-orbit` command. It uses
the checked-in Orbit-style SQLite fixture from the relationship discovery
benchmark corpus, with no declared primary keys or foreign keys in the database
schema.
Run from the KLO workspace root:
Run from the KTX workspace root:
```bash
pnpm run relationships:verify-orbit
@ -29,5 +29,5 @@ examples/orbit-relationship-verification/reports/orbit-verification.md
Use a real local Orbit project by overriding the project directory:
```bash
KLO_ORBIT_PROJECT_DIR=/path/to/orbit-project pnpm run relationships:verify-orbit
KTX_ORBIT_PROJECT_DIR=/path/to/orbit-project pnpm run relationships:verify-orbit
```

View file

@ -9,7 +9,7 @@ storage:
search: sqlite-fts5
git:
auto_commit: true
author: "klo <klo@example.com>"
author: "ktx <ktx@example.com>"
ingest:
adapters:
- live-database

View file

@ -1,17 +1,17 @@
# Package artifact smoke checks
The package artifact smoke checks create temporary projects instead of storing
sample projects in this directory. Run the checks from `klo/`:
sample projects in this directory. Run the checks from `ktx/`:
```bash
source .venv/bin/activate
pnpm run artifacts:check
```
The npm smoke project installs the generated `@klo/context` and `@klo/cli`
tarballs, imports public package entry points, and runs installed `klo`
The npm smoke project installs the generated `@ktx/context` and `@ktx/cli`
tarballs, imports public package entry points, and runs installed `ktx`
commands against a generated local project.
The Python smoke project installs `klo-daemon` through the local artifact
directory, imports `semantic_layer` and `klo_daemon`, and runs
`python -m klo_daemon semantic-validate`.
The Python smoke project installs `ktx-daemon` through the local artifact
directory, imports `semantic_layer` and `ktx_daemon`, and runs
`python -m ktx_daemon semantic-validate`.

View file

@ -2,7 +2,7 @@
This example is a manual smoke for Postgres historic-SQL ingest through
`pg_stat_statements`. It starts Postgres 14 with the extension preloaded,
generates query workload under separate users, runs `klo setup` with
generates query workload under separate users, runs `ktx setup` with
`--enable-historic-sql`, and verifies three local ingest runs:
- first run creates a fresh PGSS baseline
@ -12,30 +12,30 @@ generates query workload under separate users, runs `klo setup` with
## Prerequisites
- Docker with Compose v2
- Node and pnpm matching the KLO workspace
- `python-service/.venv` already created, or `KLO_SQL_ANALYSIS_URL` pointing at
- Node and pnpm matching the KTX workspace
- `python-service/.venv` already created, or `KTX_SQL_ANALYSIS_URL` pointing at
a running service that exposes `/api/sql/analyze-for-fingerprint`
## Run
From the KLO repository root:
From the KTX repository root:
```bash
examples/postgres-historic/scripts/smoke.sh
```
The smoke creates a temporary KLO project, starts Postgres on
The smoke creates a temporary KTX project, starts Postgres on
`127.0.0.1:55432`, and uses this connection URL:
```bash
postgresql://klo_reader:klo_reader@127.0.0.1:55432/analytics # pragma: allowlist secret
postgresql://ktx_reader:ktx_reader@127.0.0.1:55432/analytics # pragma: allowlist secret
```
Set `KLO_POSTGRES_HISTORIC_KEEP_DOCKER=1` to leave the container running after
Set `KTX_POSTGRES_HISTORIC_KEEP_DOCKER=1` to leave the container running after
the script exits.
The smoke validates the historic-SQL raw snapshot path without requiring LLM
credentials. It uses KLO's local stage-only ingest API after `klo setup` so the
credentials. It uses KTX's local stage-only ingest API after `ktx setup` so the
PGSS baseline and delta behavior can be checked independently from curation.
## Manual Commands
@ -50,9 +50,9 @@ examples/postgres-historic/scripts/generate-workload.sh base
Create a project and enable historic SQL:
```bash
export WAREHOUSE_DATABASE_URL=postgresql://klo_reader:klo_reader@127.0.0.1:55432/analytics # pragma: allowlist secret
pnpm --filter @klo/cli run build
node packages/cli/dist/bin.js --project-dir /tmp/klo-postgres-historic setup \
export WAREHOUSE_DATABASE_URL=postgresql://ktx_reader:ktx_reader@127.0.0.1:55432/analytics # pragma: allowlist secret
pnpm --filter @ktx/cli run build
node packages/cli/dist/bin.js --project-dir /tmp/ktx-postgres-historic setup \
--new \
--skip-agents \
--skip-llm \
@ -71,11 +71,11 @@ node packages/cli/dist/bin.js --project-dir /tmp/klo-postgres-historic setup \
### Readiness check
```bash
pnpm run klo -- dev doctor --project-dir /tmp/klo-postgres-historic --no-input
pnpm run ktx -- dev doctor --project-dir /tmp/ktx-postgres-historic --no-input
```
The installed CLI form is `klo dev doctor --project-dir
/tmp/klo-postgres-historic --no-input`. Expected output includes `PASS Postgres
The installed CLI form is `ktx dev doctor --project-dir
/tmp/ktx-postgres-historic --no-input`. Expected output includes `PASS Postgres
Historic SQL (warehouse)` when `pg_stat_statements` is installed,
`pg_read_all_stats` is granted, tracking is enabled, and
`pg_stat_statements.max` is at least 5000.
@ -83,7 +83,7 @@ Historic SQL (warehouse)` when `pg_stat_statements` is installed,
Run local historic-SQL ingest:
```bash
node packages/cli/dist/bin.js --project-dir /tmp/klo-postgres-historic dev ingest run \
node packages/cli/dist/bin.js --project-dir /tmp/ktx-postgres-historic dev ingest run \
--connection-id warehouse \
--adapter historic-sql \
--plain \
@ -96,7 +96,7 @@ configured LLM provider.
Inspect the latest manifest:
```bash
find /tmp/klo-postgres-historic/raw-sources/warehouse/historic-sql -name manifest.json | sort | tail -n 1
find /tmp/ktx-postgres-historic/raw-sources/warehouse/historic-sql -name manifest.json | sort | tail -n 1
```
The manifest should have `dialect: "postgres"`, `degraded: true`,
@ -108,8 +108,8 @@ The manifest should have `dialect: "postgres"`, `degraded: true`,
- Missing extension: confirm `shared_preload_libraries=pg_stat_statements` and
`CREATE EXTENSION pg_stat_statements;` both happened in the `analytics`
database.
- Missing grants: confirm `GRANT pg_read_all_stats TO klo_reader;`.
- Missing grants: confirm `GRANT pg_read_all_stats TO ktx_reader;`.
- Empty templates: rerun `scripts/generate-workload.sh base` and keep
`--historic-sql-min-calls 2` for the smoke.
- SQL-analysis failures: set `KLO_SQL_ANALYSIS_URL` to the running service URL
- SQL-analysis failures: set `KTX_SQL_ANALYSIS_URL` to the running service URL
or create `python-service/.venv` before running `scripts/smoke.sh`.

View file

@ -2,9 +2,9 @@ CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE ROLE app_user LOGIN PASSWORD 'app_pass';
CREATE ROLE etl_user LOGIN PASSWORD 'etl_pass';
CREATE ROLE klo_reader LOGIN PASSWORD 'klo_reader';
CREATE ROLE ktx_reader LOGIN PASSWORD 'ktx_reader';
GRANT pg_read_all_stats TO klo_reader;
GRANT pg_read_all_stats TO ktx_reader;
CREATE TABLE customers (
id integer PRIMARY KEY,
@ -47,5 +47,5 @@ INSERT INTO events (id, customer_id, event_name, occurred_at) VALUES
(4, 3, 'sync_completed', now() - interval '6 hours'),
(5, 4, 'dashboard_viewed', now() - interval '5 hours');
GRANT USAGE ON SCHEMA public TO app_user, etl_user, klo_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_user, etl_user, klo_reader;
GRANT USAGE ON SCHEMA public TO app_user, etl_user, ktx_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_user, etl_user, ktx_reader;

View file

@ -3,12 +3,12 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EXAMPLE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
KLO_ROOT="$(cd "$EXAMPLE_DIR/../.." && pwd)"
REPO_ROOT="$(cd "$KLO_ROOT/.." && pwd)"
KTX_ROOT="$(cd "$EXAMPLE_DIR/../.." && pwd)"
REPO_ROOT="$(cd "$KTX_ROOT/.." && pwd)"
COMPOSE_FILE="$EXAMPLE_DIR/docker-compose.yml"
PROJECT_PARENT="${KLO_POSTGRES_HISTORIC_PROJECT_PARENT:-$(mktemp -d)}"
PROJECT_DIR="$PROJECT_PARENT/postgres-historic-klo"
KLO_BIN="$KLO_ROOT/packages/cli/dist/bin.js"
PROJECT_PARENT="${KTX_POSTGRES_HISTORIC_PROJECT_PARENT:-$(mktemp -d)}"
PROJECT_DIR="$PROJECT_PARENT/postgres-historic-ktx"
KTX_BIN="$KTX_ROOT/packages/cli/dist/bin.js"
PYTHON_SERVICE_LOG="$PROJECT_PARENT/python-service.log"
PYTHON_SERVICE_PID=""
@ -16,18 +16,18 @@ cleanup() {
if [[ -n "$PYTHON_SERVICE_PID" ]]; then
kill "$PYTHON_SERVICE_PID" >/dev/null 2>&1 || true
fi
if [[ "${KLO_POSTGRES_HISTORIC_KEEP_DOCKER:-0}" != "1" ]]; then
if [[ "${KTX_POSTGRES_HISTORIC_KEEP_DOCKER:-0}" != "1" ]]; then
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
start_sql_analysis_if_needed() {
if [[ -n "${KLO_SQL_ANALYSIS_URL:-}" ]]; then
if [[ -n "${KTX_SQL_ANALYSIS_URL:-}" ]]; then
return
fi
if [[ ! -d "$REPO_ROOT/python-service/.venv" ]]; then
echo "Set KLO_SQL_ANALYSIS_URL or create python-service/.venv before running this smoke." >&2
echo "Set KTX_SQL_ANALYSIS_URL or create python-service/.venv before running this smoke." >&2
exit 1
fi
(
@ -36,9 +36,9 @@ start_sql_analysis_if_needed() {
uvicorn app.main:app --host 127.0.0.1 --port 18081 >"$PYTHON_SERVICE_LOG" 2>&1
) &
PYTHON_SERVICE_PID="$!"
export KLO_SQL_ANALYSIS_URL="http://127.0.0.1:18081"
export KTX_SQL_ANALYSIS_URL="http://127.0.0.1:18081"
for _ in $(seq 1 60); do
if curl -fsS "$KLO_SQL_ANALYSIS_URL/health" >/dev/null 2>&1; then
if curl -fsS "$KTX_SQL_ANALYSIS_URL/health" >/dev/null 2>&1; then
return
fi
sleep 1
@ -74,18 +74,18 @@ NODE
run_historic_stage_only() {
local job_id="$1"
node - "$KLO_ROOT" "$PROJECT_DIR" "$job_id" <<'NODE'
node - "$KTX_ROOT" "$PROJECT_DIR" "$job_id" <<'NODE'
const { join } = await import('node:path');
const kloRoot = process.argv[2];
const ktxRoot = process.argv[2];
const projectDir = process.argv[3];
const jobId = process.argv[4];
const { loadKloProject } = await import(join(kloRoot, 'packages/context/dist/project/index.js'));
const { runLocalStageOnlyIngest } = await import(join(kloRoot, 'packages/context/dist/ingest/index.js'));
const { createKloCliLocalIngestAdapters } = await import(join(kloRoot, 'packages/cli/dist/local-adapters.js'));
const { loadKtxProject } = await import(join(ktxRoot, 'packages/context/dist/project/index.js'));
const { runLocalStageOnlyIngest } = await import(join(ktxRoot, 'packages/context/dist/ingest/index.js'));
const { createKtxCliLocalIngestAdapters } = await import(join(ktxRoot, 'packages/cli/dist/local-adapters.js'));
const project = await loadKloProject({ projectDir });
const adapters = createKloCliLocalIngestAdapters(project, { historicSqlConnectionId: 'warehouse' });
const project = await loadKtxProject({ projectDir });
const adapters = createKtxCliLocalIngestAdapters(project, { historicSqlConnectionId: 'warehouse' });
const adapter = adapters.find((candidate) => candidate.source === 'historic-sql');
if (!adapter) throw new Error('historic-sql adapter was not registered for local run');
const record = await runLocalStageOnlyIngest({
@ -102,22 +102,22 @@ await adapter.onPullSucceeded?.({
syncId: record.syncId,
trigger: 'manual_resync',
completedAt: new Date(record.completedAt),
stagedDir: join(project.projectDir, '.klo/cache/local-ingest', jobId, 'staged'),
stagedDir: join(project.projectDir, '.ktx/cache/local-ingest', jobId, 'staged'),
});
console.log(record.syncId);
NODE
}
cd "$KLO_ROOT"
pnpm --filter @klo/context run build
pnpm --filter @klo/cli run build
cd "$KTX_ROOT"
pnpm --filter @ktx/context run build
pnpm --filter @ktx/cli run build
start_sql_analysis_if_needed
docker compose -f "$COMPOSE_FILE" up -d --wait
"$EXAMPLE_DIR/scripts/generate-workload.sh" base
export WAREHOUSE_DATABASE_URL="${WAREHOUSE_DATABASE_URL:-postgresql://klo_reader:klo_reader@127.0.0.1:55432/analytics}" # pragma: allowlist secret
node "$KLO_BIN" --project-dir "$PROJECT_DIR" setup \
export WAREHOUSE_DATABASE_URL="${WAREHOUSE_DATABASE_URL:-postgresql://ktx_reader:ktx_reader@127.0.0.1:55432/analytics}" # pragma: allowlist secret
node "$KTX_BIN" --project-dir "$PROJECT_DIR" setup \
--new \
--skip-agents \
--skip-llm \

View file

@ -1,7 +1,7 @@
{
"name": "klo-workspace",
"name": "ktx-workspace",
"version": "0.0.0-private",
"description": "Workspace root for klo packages",
"description": "Workspace root for ktx packages",
"private": true,
"type": "module",
"packageManager": "pnpm@10.28.0",
@ -18,7 +18,7 @@
"artifacts:verify-manifest": "node scripts/package-artifacts.mjs verify-manifest",
"build": "pnpm --filter './packages/*' run build",
"check": "node scripts/check-boundaries.mjs && node --test scripts/*.test.mjs && pnpm --filter './packages/*' run build && pnpm --filter './packages/*' run test",
"klo": "node scripts/run-klo.mjs",
"ktx": "node scripts/run-ktx.mjs",
"link:dev": "node scripts/link-dev-cli.mjs",
"native:rebuild": "pnpm -r rebuild better-sqlite3",
"setup:dev": "node scripts/setup-dev.mjs",
@ -28,7 +28,7 @@
"relationships:rebuild-public-snapshots": "node scripts/build-benchmark-snapshot.mjs --rebuild-all",
"relationships:build-adventureworks-oltp": "node scripts/build-adventureworks-oltp-fixture.mjs",
"relationships:verify-orbit": "node scripts/relationship-orbit-verification.mjs",
"smoke": "pnpm run build && pnpm --filter @klo/cli run smoke",
"smoke": "pnpm run build && pnpm --filter @ktx/cli run smoke",
"test": "node --test scripts/*.test.mjs && pnpm --filter './packages/*' run test",
"type-check": "pnpm --filter './packages/*' run type-check"
},

View file

@ -1,14 +1,14 @@
{
"name": "@klo/cli",
"name": "@ktx/cli",
"version": "0.0.0-private",
"description": "CLI wrapper for klo context packages",
"description": "CLI wrapper for ktx context packages",
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0"
},
"bin": {
"klo": "./dist/bin.js"
"ktx": "./dist/bin.js"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -34,16 +34,16 @@
"dependencies": {
"@clack/prompts": "1.3.0",
"@commander-js/extra-typings": "14.0.0",
"@klo/connector-bigquery": "workspace:*",
"@klo/connector-clickhouse": "workspace:*",
"@klo/connector-mysql": "workspace:*",
"@klo/connector-postgres": "workspace:*",
"@klo/connector-posthog": "workspace:*",
"@klo/connector-snowflake": "workspace:*",
"@klo/connector-sqlite": "workspace:*",
"@klo/connector-sqlserver": "workspace:*",
"@klo/context": "workspace:*",
"@klo/llm": "workspace:*",
"@ktx/connector-bigquery": "workspace:*",
"@ktx/connector-clickhouse": "workspace:*",
"@ktx/connector-mysql": "workspace:*",
"@ktx/connector-postgres": "workspace:*",
"@ktx/connector-posthog": "workspace:*",
"@ktx/connector-snowflake": "workspace:*",
"@ktx/connector-sqlite": "workspace:*",
"@ktx/connector-sqlserver": "workspace:*",
"@ktx/context": "workspace:*",
"@ktx/llm": "workspace:*",
"@modelcontextprotocol/sdk": "^1.27.1",
"commander": "14.0.3",
"ink": "^7.0.1",

View file

@ -7,7 +7,7 @@ import Database from 'better-sqlite3';
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const repoRoot = resolve(packageRoot, '../..');
const defaultDemoSource = resolve(repoRoot, '../../../orbit-demo-source');
const sourceRoot = resolve(process.env.KLO_DEMO_SOURCE_DIR ?? defaultDemoSource);
const sourceRoot = resolve(process.env.KTX_DEMO_SOURCE_DIR ?? defaultDemoSource);
const assetDir = join(packageRoot, 'assets/demo/orbit');
const dbPath = join(assetDir, 'demo.db');
const exampleDbtProjectDir = ['dbt', `${'kae'}lio_demo`].join('/');
@ -330,7 +330,7 @@ async function pathExists(path) {
async function assertReadable(path, label) {
if (!(await pathExists(path))) {
throw new Error(
`${label} not found at ${path}. Set KLO_DEMO_SOURCE_DIR to the Orbit demo source directory.`,
`${label} not found at ${path}. Set KTX_DEMO_SOURCE_DIR to the Orbit demo source directory.`,
);
}
}

View file

@ -3,8 +3,8 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
KLO_AGENT_MAX_ROWS_CAP,
createKloAgentRuntime,
KTX_AGENT_MAX_ROWS_CAP,
createKtxAgentRuntime,
parseAgentMaxRows,
readAgentJsonFile,
writeAgentJson,
@ -28,7 +28,7 @@ describe('agent runtime helpers', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-agent-runtime-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-runtime-'));
});
afterEach(async () => {
@ -69,13 +69,13 @@ describe('agent runtime helpers', () => {
expect(parseAgentMaxRows(100)).toBe(100);
expect(() => parseAgentMaxRows(undefined)).toThrow('maxRows is required');
expect(() => parseAgentMaxRows(0)).toThrow('positive integer');
expect(() => parseAgentMaxRows(KLO_AGENT_MAX_ROWS_CAP + 1)).toThrow(String(KLO_AGENT_MAX_ROWS_CAP));
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, 'klo.yaml'),
configPath: join(tempDir, 'ktx.yaml'),
config: { project: 'revenue', connections: {} },
coreConfig: {},
git: {},
@ -88,7 +88,7 @@ describe('agent runtime helpers', () => {
const createContextTools = vi.fn(() => ports);
await expect(
createKloAgentRuntime(
createKtxAgentRuntime(
{ projectDir: tempDir, enableSemanticCompute: true, enableQueryExecution: true },
{
loadProject,

View file

@ -1,38 +1,38 @@
import { readFile } from 'node:fs/promises';
import { createDefaultLocalQueryExecutor, type KloSqlQueryExecutorPort } from '@klo/context/connections';
import { createPythonSemanticLayerComputePort, type KloSemanticLayerComputePort } from '@klo/context/daemon';
import { createLocalProjectMcpContextPorts, type KloMcpContextPorts } from '@klo/context/mcp';
import { type KloLocalProject, loadKloProject } from '@klo/context/project';
import type { KloCliIo } from './cli-runtime.js';
import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
import { createPythonSemanticLayerComputePort, 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';
export const KLO_AGENT_MAX_ROWS_CAP = 1000;
export const KTX_AGENT_MAX_ROWS_CAP = 1000;
export interface KloAgentRuntimeOptions {
export interface KtxAgentRuntimeOptions {
projectDir: string;
enableSemanticCompute: boolean;
enableQueryExecution: boolean;
}
export interface KloAgentRuntime {
project: KloLocalProject;
ports: KloMcpContextPorts;
semanticLayerCompute?: KloSemanticLayerComputePort;
queryExecutor?: KloSqlQueryExecutorPort;
export interface KtxAgentRuntime {
project: KtxLocalProject;
ports: KtxMcpContextPorts;
semanticLayerCompute?: KtxSemanticLayerComputePort;
queryExecutor?: KtxSqlQueryExecutorPort;
}
export interface KloAgentRuntimeDeps {
loadProject?: typeof loadKloProject;
export interface KtxAgentRuntimeDeps {
loadProject?: typeof loadKtxProject;
createContextTools?: typeof createLocalProjectMcpContextPorts;
createSemanticLayerCompute?: () => KloSemanticLayerComputePort;
createQueryExecutor?: () => KloSqlQueryExecutorPort;
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
}
export function writeAgentJson(io: KloCliIo, value: unknown): void {
export function writeAgentJson(io: KtxCliIo, value: unknown): void {
io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
}
export function writeAgentJsonError(
io: KloCliIo,
io: KtxCliIo,
message: string,
detail: Record<string, unknown> = {},
): void {
@ -51,17 +51,17 @@ 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 > KLO_AGENT_MAX_ROWS_CAP) {
throw new Error(`maxRows must be less than or equal to ${KLO_AGENT_MAX_ROWS_CAP}.`);
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;
}
export async function createKloAgentRuntime(
options: KloAgentRuntimeOptions,
deps: KloAgentRuntimeDeps = {},
): Promise<KloAgentRuntime> {
const project = await (deps.loadProject ?? loadKloProject)({ projectDir: options.projectDir });
export async function createKtxAgentRuntime(
options: KtxAgentRuntimeOptions,
deps: KtxAgentRuntimeDeps = {},
): Promise<KtxAgentRuntime> {
const project = await (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
const semanticLayerCompute = options.enableSemanticCompute
? (deps.createSemanticLayerCompute ?? createPythonSemanticLayerComputePort)()
: undefined;

View file

@ -9,40 +9,40 @@ import {
describe('agent semantic-layer search readiness guidance', () => {
it('formats missing project guidance with exact recovery commands', () => {
expect(missingProjectSlSearchReadiness('/tmp/klo-search', 'gross revenue')).toEqual({
expect(missingProjectSlSearchReadiness('/tmp/ktx-search', 'gross revenue')).toEqual({
code: 'agent_sl_search_missing_project',
message: 'Semantic-layer search needs an initialized KLO project at /tmp/klo-search.',
message: 'Semantic-layer search needs an initialized KTX project at /tmp/ktx-search.',
nextSteps: [
'klo demo',
'klo setup --project-dir /tmp/klo-search',
'klo ingest <connection>',
'klo agent sl list --json --query "gross revenue" --project-dir /tmp/klo-search',
'ktx demo',
'ktx setup --project-dir /tmp/ktx-search',
'ktx ingest <connection>',
'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/klo-search', 'revenue')).toMatchObject({
expect(noConnectionsSlSearchReadiness('/tmp/ktx-search', 'revenue')).toMatchObject({
code: 'agent_sl_search_no_connections',
message: 'Semantic-layer search found no configured connections in /tmp/klo-search.',
message: 'Semantic-layer search found no configured connections in /tmp/ktx-search.',
});
expect(noIndexedSourcesSlSearchReadiness('/tmp/klo-search', 'orders')).toMatchObject({
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/klo-search.',
message: 'Semantic-layer search found no indexed semantic-layer sources in /tmp/ktx-search.',
});
});
it('formats unknown connection guidance', () => {
expect(missingConnectionSlSearchReadiness('/tmp/klo-search', 'warehouse', 'revenue')).toMatchObject({
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/klo-search.',
message: 'Semantic-layer search connection "warehouse" is not configured in /tmp/ktx-search.',
});
});
it('detects missing klo.yaml read errors', () => {
it('detects missing ktx.yaml read errors', () => {
const error = Object.assign(new Error('ENOENT: no such file or directory'), {
code: 'ENOENT',
path: '/tmp/klo-search/klo.yaml',
path: '/tmp/ktx-search/ktx.yaml',
});
expect(isMissingProjectConfigError(error)).toBe(true);

View file

@ -1,11 +1,11 @@
export type KloAgentSlSearchReadinessCode =
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 KloAgentSlSearchReadinessDetail {
code: KloAgentSlSearchReadinessCode;
export interface KtxAgentSlSearchReadinessDetail {
code: KtxAgentSlSearchReadinessCode;
message: string;
nextSteps: string[];
}
@ -16,14 +16,14 @@ function queryForCommand(query: string | undefined): string {
}
function projectSearchCommand(projectDir: string, query: string | undefined): string {
return `klo agent sl list --json --query ${JSON.stringify(queryForCommand(query))} --project-dir ${projectDir}`;
return `ktx agent sl list --json --query ${JSON.stringify(queryForCommand(query))} --project-dir ${projectDir}`;
}
function baseNextSteps(projectDir: string, query: string | undefined): string[] {
return [
'klo demo',
`klo setup --project-dir ${projectDir}`,
'klo ingest <connection>',
'ktx demo',
`ktx setup --project-dir ${projectDir}`,
'ktx ingest <connection>',
projectSearchCommand(projectDir, query),
];
}
@ -31,10 +31,10 @@ function baseNextSteps(projectDir: string, query: string | undefined): string[]
export function missingProjectSlSearchReadiness(
projectDir: string,
query: string | undefined,
): KloAgentSlSearchReadinessDetail {
): KtxAgentSlSearchReadinessDetail {
return {
code: 'agent_sl_search_missing_project',
message: `Semantic-layer search needs an initialized KLO project at ${projectDir}.`,
message: `Semantic-layer search needs an initialized KTX project at ${projectDir}.`,
nextSteps: baseNextSteps(projectDir, query),
};
}
@ -42,7 +42,7 @@ export function missingProjectSlSearchReadiness(
export function noConnectionsSlSearchReadiness(
projectDir: string,
query: string | undefined,
): KloAgentSlSearchReadinessDetail {
): KtxAgentSlSearchReadinessDetail {
return {
code: 'agent_sl_search_no_connections',
message: `Semantic-layer search found no configured connections in ${projectDir}.`,
@ -54,7 +54,7 @@ export function missingConnectionSlSearchReadiness(
projectDir: string,
connectionId: string,
query: string | undefined,
): KloAgentSlSearchReadinessDetail {
): KtxAgentSlSearchReadinessDetail {
return {
code: 'agent_sl_search_unknown_connection',
message: `Semantic-layer search connection "${connectionId}" is not configured in ${projectDir}.`,
@ -65,7 +65,7 @@ export function missingConnectionSlSearchReadiness(
export function noIndexedSourcesSlSearchReadiness(
projectDir: string,
query: string | undefined,
): KloAgentSlSearchReadinessDetail {
): KtxAgentSlSearchReadinessDetail {
return {
code: 'agent_sl_search_no_indexed_sources',
message: `Semantic-layer search found no indexed semantic-layer sources in ${projectDir}.`,
@ -90,5 +90,5 @@ function errorPath(error: unknown): string | undefined {
}
export function isMissingProjectConfigError(error: unknown): boolean {
return errorCode(error) === 'ENOENT' && (errorPath(error)?.endsWith('klo.yaml') ?? false);
return errorCode(error) === 'ENOENT' && (errorPath(error)?.endsWith('ktx.yaml') ?? false);
}

View file

@ -1,10 +1,10 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { buildDefaultKloProjectConfig } from '@klo/context/project';
import { buildDefaultKtxProjectConfig } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloAgent } from './agent.js';
import type { KloAgentRuntime } from './agent-runtime.js';
import { runKtxAgent } from './agent.js';
import type { KtxAgentRuntime } from './agent-runtime.js';
function makeIo() {
let stdout = '';
@ -19,21 +19,21 @@ function makeIo() {
};
}
function runtime(overrides: Record<string, unknown> = {}): KloAgentRuntime {
const config = buildDefaultKloProjectConfig('revenue');
function runtime(overrides: Record<string, unknown> = {}): KtxAgentRuntime {
const config = buildDefaultKtxProjectConfig('revenue');
return {
project: {
projectDir: '/tmp/revenue',
configPath: '/tmp/revenue/klo.yaml',
configPath: '/tmp/revenue/ktx.yaml',
config: {
...config,
connections: {
warehouse: { driver: 'sqlite', path: 'warehouse.sqlite', readonly: true as const },
},
},
coreConfig: {} as KloAgentRuntime['project']['coreConfig'],
git: {} as KloAgentRuntime['project']['git'],
fileStore: {} as KloAgentRuntime['project']['fileStore'],
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' }]) },
@ -86,7 +86,7 @@ function runtime(overrides: Record<string, unknown> = {}): KloAgentRuntime {
};
}
function runtimeWithoutConnections(): KloAgentRuntime {
function runtimeWithoutConnections(): KtxAgentRuntime {
const base = runtime();
return {
...base,
@ -107,11 +107,11 @@ function runtimeWithoutConnections(): KloAgentRuntime {
};
}
describe('runKloAgent', () => {
describe('runKtxAgent', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-agent-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-'));
});
afterEach(async () => {
@ -121,7 +121,7 @@ describe('runKloAgent', () => {
it('prints tool discovery with every stable command', async () => {
const io = makeIo();
await expect(runKloAgent({ command: 'tools', projectDir: tempDir, json: true }, io.io)).resolves.toBe(0);
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);
@ -143,7 +143,7 @@ describe('runKloAgent', () => {
const readSetupStatus = vi.fn(async () => ({ project: { path: tempDir, ready: true }, agents: [] }));
await expect(
runKloAgent({ command: 'context', projectDir: tempDir, json: true }, io.io, { createRuntime, readSetupStatus }),
runKtxAgent({ command: 'context', projectDir: tempDir, json: true }, io.io, { createRuntime, readSetupStatus }),
).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toMatchObject({
@ -168,7 +168,7 @@ describe('runKloAgent', () => {
{ command: 'wiki-read' as const, projectDir: tempDir, json: true as const, pageId: 'page-1' },
]) {
const io = makeIo();
await expect(runKloAgent(args, io.io, { createRuntime: async () => runtime() })).resolves.toBe(0);
await expect(runKtxAgent(args, io.io, { createRuntime: async () => runtime() })).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toBeTruthy();
expect(io.stderr()).toBe('');
}
@ -199,7 +199,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent({ command: 'wiki-search', projectDir: tempDir, json: true, query: 'paid order', limit: 5 }, io.io, {
runKtxAgent({ command: 'wiki-search', projectDir: tempDir, json: true, query: 'paid order', limit: 5 }, io.io, {
createRuntime: async () => fakeRuntime,
}),
).resolves.toBe(0);
@ -222,7 +222,7 @@ describe('runKloAgent', () => {
await writeFile(queryFile, '{"measures":["total_revenue"],"dimensions":[]}', 'utf-8');
await expect(
runKloAgent(
runKtxAgent(
{
command: 'sl-query',
projectDir: tempDir,
@ -247,7 +247,7 @@ describe('runKloAgent', () => {
await writeFile(sqlFile, 'select 1', 'utf-8');
await expect(
runKloAgent(
runKtxAgent(
{
command: 'sql-execute',
projectDir: tempDir,
@ -274,11 +274,11 @@ describe('runKloAgent', () => {
const io = makeIo();
const missingProjectError = Object.assign(new Error('ENOENT: no such file or directory'), {
code: 'ENOENT',
path: join(tempDir, 'klo.yaml'),
path: join(tempDir, 'ktx.yaml'),
});
await expect(
runKloAgent(
runKtxAgent(
{ command: 'sl-list', projectDir: tempDir, json: true, query: 'gross revenue' },
io.io,
{ createRuntime: vi.fn(async () => Promise.reject(missingProjectError)) },
@ -289,12 +289,12 @@ describe('runKloAgent', () => {
ok: false,
error: {
code: 'agent_sl_search_missing_project',
message: `Semantic-layer search needs an initialized KLO project at ${tempDir}.`,
message: `Semantic-layer search needs an initialized KTX project at ${tempDir}.`,
nextSteps: [
'klo demo',
`klo setup --project-dir ${tempDir}`,
'klo ingest <connection>',
`klo agent sl list --json --query "gross revenue" --project-dir ${tempDir}`,
'ktx demo',
`ktx setup --project-dir ${tempDir}`,
'ktx ingest <connection>',
`ktx agent sl list --json --query "gross revenue" --project-dir ${tempDir}`,
],
},
});
@ -305,7 +305,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent(
runKtxAgent(
{ command: 'sl-list', projectDir: tempDir, json: true, query: 'revenue' },
io.io,
{ createRuntime: async () => runtimeWithoutConnections() },
@ -318,10 +318,10 @@ describe('runKloAgent', () => {
code: 'agent_sl_search_no_connections',
message: `Semantic-layer search found no configured connections in ${tempDir}.`,
nextSteps: [
'klo demo',
`klo setup --project-dir ${tempDir}`,
'klo ingest <connection>',
`klo agent sl list --json --query "revenue" --project-dir ${tempDir}`,
'ktx demo',
`ktx setup --project-dir ${tempDir}`,
'ktx ingest <connection>',
`ktx agent sl list --json --query "revenue" --project-dir ${tempDir}`,
],
},
});
@ -331,7 +331,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent(
runKtxAgent(
{ command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'missing', query: 'revenue' },
io.io,
{ createRuntime: async () => runtime() },
@ -357,7 +357,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent(
runKtxAgent(
{ command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'warehouse', query: 'revenue' },
io.io,
{ createRuntime: async () => fakeRuntime },
@ -377,7 +377,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent({ command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'missing' }, io.io, {
runKtxAgent({ command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'missing' }, io.io, {
createRuntime: async () =>
runtime({
ports: { knowledge: { read: vi.fn(async () => null) } },

View file

@ -1,13 +1,13 @@
import { readFile } from 'node:fs/promises';
import type { KloCliIo } from './cli-runtime.js';
import type { KtxCliIo } from './cli-runtime.js';
import {
createKloAgentRuntime,
createKtxAgentRuntime,
parseAgentMaxRows,
readAgentJsonFile,
writeAgentJson,
writeAgentJsonError,
type KloAgentRuntime,
type KloAgentRuntimeDeps,
type KtxAgentRuntime,
type KtxAgentRuntimeDeps,
} from './agent-runtime.js';
import {
isMissingProjectConfigError,
@ -15,11 +15,11 @@ import {
missingProjectSlSearchReadiness,
noConnectionsSlSearchReadiness,
noIndexedSourcesSlSearchReadiness,
type KloAgentSlSearchReadinessDetail,
type KtxAgentSlSearchReadinessDetail,
} from './agent-search-readiness.js';
import { readKloSetupStatus, type KloSetupStatus } from './setup.js';
import { readKtxSetupStatus, type KtxSetupStatus } from './setup.js';
export type KloAgentArgs =
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 }
@ -37,38 +37,38 @@ export type KloAgentArgs =
| { command: 'wiki-read'; projectDir: string; json: true; pageId: string }
| { command: 'sql-execute'; projectDir: string; json: true; connectionId: string; sqlFile: string; maxRows?: number };
export interface KloAgentDeps extends KloAgentRuntimeDeps {
export interface KtxAgentDeps extends KtxAgentRuntimeDeps {
createRuntime?: (options: {
projectDir: string;
enableSemanticCompute: boolean;
enableQueryExecution: boolean;
}) => Promise<KloAgentRuntime>;
}) => Promise<KtxAgentRuntime>;
readSetupStatus?: (
projectDir: string,
) => Promise<KloSetupStatus | { project: { path?: string; ready: boolean }; agents: unknown[] }>;
) => Promise<KtxSetupStatus | { project: { path?: string; ready: boolean }; agents: unknown[] }>;
}
const AGENT_TOOLS = [
{ name: 'context', command: 'klo agent context --json' },
{ name: 'sl.list', command: 'klo agent sl list --json [--connection-id <id>] [--query <text>]' },
{ name: 'sl.read', command: 'klo agent sl read <sourceName> --json [--connection-id <id>]' },
{ name: 'context', command: 'ktx agent context --json' },
{ name: 'sl.list', command: 'ktx agent sl list --json [--connection-id <id>] [--query <text>]' },
{ name: 'sl.read', command: 'ktx agent sl read <sourceName> --json [--connection-id <id>]' },
{
name: 'sl.query',
command: 'klo agent sl query --json --connection-id <id> --query-file <path> --execute --max-rows 100',
command: 'ktx agent sl query --json --connection-id <id> --query-file <path> --execute --max-rows 100',
},
{ name: 'wiki.search', command: 'klo agent wiki search <query> --json [--limit 10]' },
{ name: 'wiki.read', command: 'klo agent wiki read <pageId> --json' },
{ name: 'wiki.search', command: 'ktx agent wiki search <query> --json [--limit 10]' },
{ name: 'wiki.read', command: 'ktx agent wiki read <pageId> --json' },
{
name: 'sql.execute',
command: 'klo agent sql execute --json --connection-id <id> --sql-file <path> --max-rows 100',
command: 'ktx agent sql execute --json --connection-id <id> --sql-file <path> --max-rows 100',
},
] as const;
function writeAgentSlSearchReadinessError(io: KloCliIo, detail: KloAgentSlSearchReadinessDetail): void {
function writeAgentSlSearchReadinessError(io: KtxCliIo, detail: KtxAgentSlSearchReadinessDetail): void {
writeAgentJsonError(io, detail.message, { code: detail.code, nextSteps: detail.nextSteps });
}
async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAgentRuntime> {
async function runtimeFor(args: KtxAgentArgs, deps: KtxAgentDeps): Promise<KtxAgentRuntime> {
const needsSemanticCompute = args.command === 'sl-query';
const needsQueryExecution = args.command === 'sql-execute' || (args.command === 'sl-query' && args.execute);
return deps.createRuntime
@ -77,7 +77,7 @@ async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAg
enableSemanticCompute: needsSemanticCompute,
enableQueryExecution: needsQueryExecution,
})
: createKloAgentRuntime(
: createKtxAgentRuntime(
{
projectDir: args.projectDir,
enableSemanticCompute: needsSemanticCompute,
@ -87,14 +87,14 @@ async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAg
);
}
function connectionIdForSource(runtime: KloAgentRuntime, requested: string | undefined): string {
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 runKloAgent(args: KloAgentArgs, io: KloCliIo, deps: KloAgentDeps = {}): Promise<number> {
export async function runKtxAgent(args: KtxAgentArgs, io: KtxCliIo, deps: KtxAgentDeps = {}): Promise<number> {
try {
if (args.command === 'tools') {
writeAgentJson(io, { projectDir: args.projectDir, tools: AGENT_TOOLS });
@ -105,7 +105,7 @@ export async function runKloAgent(args: KloAgentArgs, io: KloCliIo, deps: KloAge
if (args.command === 'context') {
const [status, connections, semanticLayer] = await Promise.all([
(deps.readSetupStatus ?? readKloSetupStatus)(args.projectDir),
(deps.readSetupStatus ?? readKtxSetupStatus)(args.projectDir),
runtime.ports.connections?.list() ?? [],
runtime.ports.semanticLayer?.listSources({}) ?? { sources: [], totalSources: 0 },
]);

View file

@ -4,6 +4,6 @@ import { installStartupProfileReporter, profileMark, profileSpan } from './start
installStartupProfileReporter();
profileMark('bin:entry');
const { runKloCli } = await profileSpan('import ./cli-runtime.js', () => import('./cli-runtime.js'));
profileMark('bin:runKloCli');
process.exitCode = await runKloCli(process.argv.slice(2));
const { runKtxCli } = await profileSpan('import ./cli-runtime.js', () => import('./cli-runtime.js'));
profileMark('bin:runKtxCli');
process.exitCode = await runKtxCli(process.argv.slice(2));

View file

@ -1,11 +1,11 @@
import { spinner } from '@clack/prompts';
export interface KloCliSpinner {
export interface KtxCliSpinner {
start(message: string): void;
stop(message: string): void;
error(message: string): void;
}
export function createClackSpinner(): KloCliSpinner {
export function createClackSpinner(): KtxCliSpinner {
return spinner();
}

View file

@ -1,5 +1,5 @@
import { Command, InvalidArgumentError } from '@commander-js/extra-typings';
import type { KloCliDeps, KloCliIo, KloCliPackageInfo } from './cli-runtime.js';
import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js';
import { registerAgentCommands } from './commands/agent-commands.js';
import { registerConnectionCommands } from './commands/connection-commands.js';
import { registerWikiCommands } from './commands/knowledge-commands.js';
@ -9,16 +9,16 @@ import { registerSetupCommands } from './commands/setup-commands.js';
import { registerSlCommands } from './commands/sl-commands.js';
import { registerStatusCommands } from './commands/status-commands.js';
import { registerDevCommands } from './dev.js';
import { findNearestKloProjectDir, resolveKloProjectDir } from './project-resolver.js';
import { findNearestKtxProjectDir, resolveKtxProjectDir } from './project-resolver.js';
import { profileMark, profileSpan } from './startup-profile.js';
profileMark('module:cli-program');
export interface KloCliCommandContext {
io: KloCliIo;
deps: KloCliDeps;
export interface KtxCliCommandContext {
io: KtxCliIo;
deps: KtxCliDeps;
setExitCode: (code: number) => void;
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KloCliIo) => Promise<number>;
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
writeDebug?: (command: string, commandContext: CommandWithGlobalOptions) => void;
}
@ -29,13 +29,13 @@ export interface OutputModeOptions {
input?: boolean;
}
interface KloCommanderProgramOptions {
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KloCliIo) => Promise<number>;
interface KtxCommanderProgramOptions {
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
}
type CommanderExitLike = { exitCode: number; code: string; message: string };
interface KloGlobalOptionValues {
interface KtxGlobalOptionValues {
projectDir?: string;
debug?: boolean;
}
@ -104,7 +104,7 @@ export function parseNonEmptyAssignmentOption(value: string): { key: string; val
};
}
function optionsWithGlobals(command: CommandWithGlobalOptions): KloGlobalOptionValues {
function optionsWithGlobals(command: CommandWithGlobalOptions): KtxGlobalOptionValues {
const options = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
const values = options as { projectDir?: unknown; debug?: unknown };
return {
@ -114,25 +114,25 @@ function optionsWithGlobals(command: CommandWithGlobalOptions): KloGlobalOptionV
}
export function resolveCommandProjectDir(command: CommandWithGlobalOptions): string {
return resolveKloProjectDir({ explicitProjectDir: optionsWithGlobals(command).projectDir });
return resolveKtxProjectDir({ explicitProjectDir: optionsWithGlobals(command).projectDir });
}
export function resolveCommandProjectDirOverride(command: CommandWithGlobalOptions): string | undefined {
return optionsWithGlobals(command).projectDir ?? process.env.KLO_PROJECT_DIR;
return optionsWithGlobals(command).projectDir ?? process.env.KTX_PROJECT_DIR;
}
function createBaseProgram(info: KloCliPackageInfo, io: KloCliIo): Command {
function createBaseProgram(info: KtxCliPackageInfo, io: KtxCliIo): Command {
return new Command()
.name('klo')
.description('Standalone KLO developer CLI')
.option('--project-dir <path>', 'KLO project directory (default: KLO_PROJECT_DIR, nearest klo.yaml, or cwd)')
.name('ktx')
.description('Standalone KTX developer CLI')
.option('--project-dir <path>', 'KTX project directory (default: KTX_PROJECT_DIR, nearest ktx.yaml, or cwd)')
.option('--debug', 'Enable diagnostic logging to stderr')
.version(`${info.name} ${info.version}`, '-v, --version', 'Show CLI version')
.helpOption('-h, --help', 'Show this help text')
.configureHelp({ showGlobalOptions: true })
.addHelpText(
'after',
'\nAdvanced:\n klo dev Low-level diagnostics, scans, adapter commands, and mapping tools.\n',
'\nAdvanced:\n ktx dev Low-level diagnostics, scans, adapter commands, and mapping tools.\n',
)
.showHelpAfterError()
.exitOverride()
@ -143,7 +143,7 @@ function createBaseProgram(info: KloCliPackageInfo, io: KloCliIo): Command {
});
}
function writeDebug(io: KloCliIo, commandContext: CommandWithGlobalOptions, command: string): void {
function writeDebug(io: KtxCliIo, commandContext: CommandWithGlobalOptions, command: string): void {
const global = optionsWithGlobals(commandContext);
if (global.debug !== true) {
return;
@ -158,18 +158,18 @@ function formatCliError(error: unknown): string {
async function runBareInteractiveCommand(
program: Command,
io: KloCliIo,
context: KloCliCommandContext,
io: KtxCliIo,
context: KtxCliCommandContext,
): Promise<number> {
const nearestProjectDir = findNearestKloProjectDir(process.cwd());
const envProjectDir = process.env.KLO_PROJECT_DIR;
const runner = context.deps.setup ?? (await import('./setup.js')).runKloSetup;
const nearestProjectDir = findNearestKtxProjectDir(process.cwd());
const envProjectDir = process.env.KTX_PROJECT_DIR;
const runner = context.deps.setup ?? (await import('./setup.js')).runKtxSetup;
if (!nearestProjectDir && !envProjectDir) {
return await runner(
{
command: 'run',
projectDir: resolveKloProjectDir(),
projectDir: resolveKtxProjectDir(),
mode: 'auto',
agents: false,
agentScope: 'project',
@ -191,18 +191,18 @@ async function runBareInteractiveCommand(
return 0;
}
export async function runCommanderKloCli(
export async function runCommanderKtxCli(
argv: string[],
io: KloCliIo,
deps: KloCliDeps,
info: KloCliPackageInfo,
options: KloCommanderProgramOptions,
io: KtxCliIo,
deps: KtxCliDeps,
info: KtxCliPackageInfo,
options: KtxCommanderProgramOptions,
): Promise<number> {
profileMark('commander:entry');
let exitCode = 0;
const program = createBaseProgram(info, io);
profileMark('commander:base-program');
const context: KloCliCommandContext = {
const context: KtxCliCommandContext = {
io,
deps,
setExitCode: (code: number) => {

View file

@ -1,67 +1,67 @@
import type { KloConnectionMetabaseSetupArgs } from './commands/connection-metabase-setup.js';
import type { KloConnectionNotionArgs } from './commands/connection-notion.js';
import type { KloAgentArgs } from './agent.js';
import type { KloConnectionArgs } from './connection.js';
import type { KloDemoArgs } from './demo.js';
import type { KloDoctorArgs } from './doctor.js';
import type { KloIngestArgs } from './ingest.js';
import type { KloKnowledgeArgs } from './knowledge.js';
import type { KloPublicIngestArgs } from './public-ingest.js';
import type { KloScanArgs } from './scan.js';
import type { KloServeArgs } from './serve.js';
import type { KloSetupArgs } from './setup.js';
import type { KloSlArgs } from './sl.js';
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 { KtxDemoArgs } from './demo.js';
import type { KtxDoctorArgs } from './doctor.js';
import type { KtxIngestArgs } from './ingest.js';
import type { KtxKnowledgeArgs } from './knowledge.js';
import type { KtxPublicIngestArgs } from './public-ingest.js';
import type { KtxScanArgs } from './scan.js';
import type { KtxServeArgs } from './serve.js';
import type { KtxSetupArgs } from './setup.js';
import type { KtxSlArgs } from './sl.js';
import { profileMark, profileSpan } from './startup-profile.js';
profileMark('module:cli-runtime');
export interface KloCliPackageInfo {
name: '@klo/cli';
export interface KtxCliPackageInfo {
name: '@ktx/cli';
version: '0.0.0-private';
contextPackageName: '@klo/context';
contextPackageName: '@ktx/context';
}
export interface KloCliIo {
export interface KtxCliIo {
stdout: { isTTY?: boolean; write(chunk: string): void };
stderr: { write(chunk: string): void };
}
export interface KloCliDeps {
serveStdio?: (args: KloServeArgs) => Promise<number>;
setup?: (args: KloSetupArgs, io: KloCliIo) => Promise<number>;
agent?: (args: KloAgentArgs, io: KloCliIo) => Promise<number>;
connection?: (args: KloConnectionArgs, io: KloCliIo) => Promise<number>;
connectionNotion?: (args: KloConnectionNotionArgs, io: KloCliIo) => Promise<number>;
connectionMetabaseSetup?: (args: KloConnectionMetabaseSetupArgs, io: KloCliIo) => Promise<number>;
demo?: (args: KloDemoArgs, io: KloCliIo) => Promise<number>;
doctor?: (args: KloDoctorArgs, io: KloCliIo) => Promise<number>;
ingest?: (args: KloIngestArgs, io: KloCliIo) => Promise<number>;
publicIngest?: (args: KloPublicIngestArgs, io: KloCliIo) => Promise<number>;
scan?: (args: KloScanArgs, io: KloCliIo) => Promise<number>;
knowledge?: (args: KloKnowledgeArgs, io: KloCliIo) => Promise<number>;
sl?: (args: KloSlArgs, io: KloCliIo) => Promise<number>;
export interface KtxCliDeps {
serveStdio?: (args: KtxServeArgs) => Promise<number>;
setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise<number>;
agent?: (args: KtxAgentArgs, io: KtxCliIo) => Promise<number>;
connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise<number>;
connectionNotion?: (args: KtxConnectionNotionArgs, io: KtxCliIo) => Promise<number>;
connectionMetabaseSetup?: (args: KtxConnectionMetabaseSetupArgs, io: KtxCliIo) => Promise<number>;
demo?: (args: KtxDemoArgs, io: KtxCliIo) => Promise<number>;
doctor?: (args: KtxDoctorArgs, io: KtxCliIo) => Promise<number>;
ingest?: (args: KtxIngestArgs, io: KtxCliIo) => Promise<number>;
publicIngest?: (args: KtxPublicIngestArgs, io: KtxCliIo) => Promise<number>;
scan?: (args: KtxScanArgs, io: KtxCliIo) => Promise<number>;
knowledge?: (args: KtxKnowledgeArgs, io: KtxCliIo) => Promise<number>;
sl?: (args: KtxSlArgs, io: KtxCliIo) => Promise<number>;
}
export function getKloCliPackageInfo(): KloCliPackageInfo {
export function getKtxCliPackageInfo(): KtxCliPackageInfo {
return {
name: '@klo/cli',
name: '@ktx/cli',
version: '0.0.0-private',
contextPackageName: '@klo/context',
contextPackageName: '@ktx/context',
};
}
async function runInit(
args: { projectDir: string; projectName?: string; force: boolean },
io: KloCliIo,
io: KtxCliIo,
): Promise<number> {
const { initKloProject } = await import('@klo/context/project');
const result = await initKloProject({
const { initKtxProject } = await import('@ktx/context/project');
const result = await initKtxProject({
projectDir: args.projectDir,
projectName: args.projectName,
force: args.force,
});
io.stdout.write(`Initialized KLO project at ${result.projectDir}\n`);
io.stdout.write(`Initialized KTX project at ${result.projectDir}\n`);
io.stdout.write(`Config: ${result.configPath}\n`);
io.stdout.write(`Commit: ${result.commitHash ?? 'none'}\n`);
return 0;
@ -69,21 +69,21 @@ async function runInit(
export async function runInitForCommander(
args: { projectDir: string; projectName?: string; force: boolean },
io: KloCliIo,
io: KtxCliIo,
): Promise<number> {
return await runInit(args, io);
}
export async function runKloCli(
export async function runKtxCli(
argv = process.argv.slice(2),
io: KloCliIo = process,
deps: KloCliDeps = {},
io: KtxCliIo = process,
deps: KtxCliDeps = {},
): Promise<number> {
const info = getKloCliPackageInfo();
profileMark('runtime:runKloCli');
const { runCommanderKloCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
const info = getKtxCliPackageInfo();
profileMark('runtime:runKtxCli');
const { runCommanderKtxCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
return await runCommanderKloCli(argv, io, deps, info, {
return await runCommanderKtxCli(argv, io, deps, info, {
runInit: runInitForCommander,
});
}

View file

@ -1,10 +1,10 @@
import { Option, type Command } from '@commander-js/extra-typings';
import type { KloAgentArgs } from '../agent.js';
import type { KloCliCommandContext } from '../cli-program.js';
import type { KtxAgentArgs } from '../agent.js';
import type { KtxCliCommandContext } from '../cli-program.js';
import { parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
async function runAgent(context: KloCliCommandContext, args: KloAgentArgs): Promise<void> {
const runner = context.deps.agent ?? (await import('../agent.js')).runKloAgent;
async function runAgent(context: KtxCliCommandContext, args: KtxAgentArgs): Promise<void> {
const runner = context.deps.agent ?? (await import('../agent.js')).runKtxAgent;
context.setExitCode(await runner(args, context.io));
}
@ -12,10 +12,10 @@ function jsonOption(): Option {
return new Option('--json', 'Print JSON output').makeOptionMandatory();
}
export function registerAgentCommands(program: Command, context: KloCliCommandContext): void {
export function registerAgentCommands(program: Command, context: KtxCliCommandContext): void {
const agent = program
.command('agent', { hidden: true })
.description('Machine-readable KLO commands for coding agents')
.description('Machine-readable KTX commands for coding agents')
.showHelpAfterError();
agent.hook('preAction', (_thisCommand, actionCommand) => {
@ -24,7 +24,7 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
agent
.command('tools')
.description('Print available agent-facing KLO tools')
.description('Print available agent-facing KTX tools')
.addOption(jsonOption())
.action(async (_options, command) => {
await runAgent(context, { command: 'tools', projectDir: resolveCommandProjectDir(command), json: true });
@ -91,10 +91,10 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
},
);
const wiki = agent.command('wiki').description('KLO wiki agent commands');
const wiki = agent.command('wiki').description('KTX wiki agent commands');
wiki
.command('search')
.description('Search KLO wiki pages')
.description('Search KTX wiki pages')
.argument('<query>')
.addOption(jsonOption())
.option('--limit <number>', 'Maximum search results', parsePositiveIntegerOption, 10)
@ -109,7 +109,7 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
});
wiki
.command('read')
.description('Read one KLO wiki page')
.description('Read one KTX wiki page')
.argument('<pageId>')
.addOption(jsonOption())
.action(async (pageId: string, _options, command) => {

View file

@ -1,10 +1,10 @@
import type { CommandUnknownOpts } from '@commander-js/extra-typings';
import type { KloCliCommandContext } from '../cli-program.js';
import type { KtxCliCommandContext } from '../cli-program.js';
import { completeCommanderInput, installZshCompletion, zshCompletionScript } from '../completion.js';
export function registerCompletionCommands(
program: CommandUnknownOpts,
context: KloCliCommandContext,
context: KtxCliCommandContext,
completionRoot: CommandUnknownOpts = program,
): void {
program

View file

@ -1,7 +1,7 @@
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
import {
collectOption,
type KloCliCommandContext,
type KtxCliCommandContext,
parseBooleanStringOption,
parseNonEmptyAssignmentOption,
parseNonNegativeIntegerOption,
@ -10,9 +10,9 @@ import {
resolveCommandProjectDir,
} from '../cli-program.js';
import { connectionAddCommandSchema } from '../command-schemas.js';
import type { KloConnectionArgs } from '../connection.js';
import type { KtxConnectionArgs } from '../connection.js';
import { profileMark } from '../startup-profile.js';
import type { KloConnectionMappingArgs } from './connection-mapping.js';
import type { KtxConnectionMappingArgs } from './connection-mapping.js';
import { registerConnectionMetabaseCommands } from './connection-metabase-commands.js';
import { registerConnectionNotionCommands } from './connection-notion-commands.js';
@ -42,24 +42,24 @@ function parseMappingFieldOption(value: string): 'databaseMappings' | 'connectio
throw new InvalidArgumentError('must be databaseMappings or connectionMappings');
}
async function runConnectionArgs(context: KloCliCommandContext, args: KloConnectionArgs): Promise<void> {
const runner = context.deps.connection ?? (await import('../connection.js')).runKloConnection;
async function runConnectionArgs(context: KtxCliCommandContext, args: KtxConnectionArgs): Promise<void> {
const runner = context.deps.connection ?? (await import('../connection.js')).runKtxConnection;
context.setExitCode(await runner(args, context.io));
}
async function runMappingArgs(context: KloCliCommandContext, args: KloConnectionMappingArgs): Promise<void> {
const { runKloConnectionMapping } = await import('./connection-mapping.js');
context.setExitCode(await runKloConnectionMapping(args, context.io));
async function runMappingArgs(context: KtxCliCommandContext, args: KtxConnectionMappingArgs): Promise<void> {
const { runKtxConnectionMapping } = await import('./connection-mapping.js');
context.setExitCode(await runKtxConnectionMapping(args, context.io));
}
export function registerConnectionCommands(program: Command, context: KloCliCommandContext, commandName = 'connection'): void {
export function registerConnectionCommands(program: Command, context: KtxCliCommandContext, commandName = 'connection'): void {
const connection = program
.command(commandName)
.description('Add, list, test, and map data sources')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the nearest klo.yaml or current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the nearest ktx.yaml or current working directory.\n',
);
connection.hook('preAction', (_thisCommand, actionCommand) => {
context.writeDebug?.(commandName, actionCommand);
@ -75,7 +75,7 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
connection
.command('test')
.description('Test a configured connection')
.argument('<connectionId>', 'KLO connection id')
.argument('<connectionId>', 'KTX connection id')
.action(async (connectionId: string, _options: unknown, command) => {
await runConnectionArgs(context, {
command: 'test',
@ -88,12 +88,12 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
.command('add')
.description('Add or replace a configured connection')
.argument('<driver>', 'Connection driver')
.argument('<connectionId>', 'KLO connection id')
.argument('<connectionId>', 'KTX connection id')
.option('--url <url>', 'Connection URL, env:NAME, or file:/path reference')
.option('--schema <schema>', 'Schema to include; repeatable', collectOption, [])
.option('--readonly', 'Mark the connection as read-only', false)
.option('--force', 'Replace an existing connection', false)
.option('--allow-literal-credentials', 'Allow writing a literal credential URL to klo.yaml', false)
.option('--allow-literal-credentials', 'Allow writing a literal credential URL to ktx.yaml', false)
.addOption(new Option('--token-env <name>', 'Environment variable containing Notion auth token').conflicts('tokenFile'))
.addOption(new Option('--token-file <path>', 'File containing Notion auth token').conflicts('tokenEnv'))
.addOption(
@ -155,8 +155,8 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
connection
.command('remove')
.description('Remove a configured connection from klo.yaml')
.argument('<connectionId>', 'KLO connection id')
.description('Remove a configured connection from ktx.yaml')
.argument('<connectionId>', 'KTX connection id')
.option('--force', 'Remove without prompting', false)
.option('--no-input', 'Disable interactive terminal input')
.action(async (connectionId: string, options: { force?: boolean; input?: boolean }, command) => {
@ -188,14 +188,14 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
registerConnectionNotionCommands(connection, context);
}
export function registerConnectionMappingCommands(connection: Command, context: KloCliCommandContext): void {
export function registerConnectionMappingCommands(connection: Command, context: KtxCliCommandContext): void {
const mapping = connection
.command('mapping')
.description('Manage Metabase warehouse mappings')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
mapping

View file

@ -1,10 +1,10 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { LocalMetabaseSourceStateReader } from '@klo/context/ingest';
import { initKloProject, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
import { LocalMetabaseSourceStateReader } from '@ktx/context/ingest';
import { initKtxProject, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloConnectionMapping } from './connection-mapping.js';
import { runKtxConnectionMapping } from './connection-mapping.js';
function makeIo() {
let stdout = '';
@ -27,18 +27,18 @@ function makeIo() {
};
}
describe('runKloConnectionMapping', () => {
describe('runKtxConnectionMapping', () => {
let tempDir: string;
let projectDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-metabase-mapping-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-metabase-mapping-'));
projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'mapping' });
const project = await loadKloProject({ projectDir });
await initKtxProject({ projectDir, projectName: 'mapping' });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections: {
'prod-metabase': {
@ -53,22 +53,22 @@ describe('runKloConnectionMapping', () => {
},
},
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Seed Metabase mapping test connections',
);
});
async function replaceConnections(connections: Record<string, { driver: string; [key: string]: unknown }>) {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections,
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Replace mapping test connections',
);
}
@ -80,7 +80,7 @@ describe('runKloConnectionMapping', () => {
it('sets, lists, disables, and clears local Metabase mappings', async () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'set',
projectDir,
@ -95,13 +95,13 @@ describe('runKloConnectionMapping', () => {
const listIo = makeIo();
await expect(
runKloConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-metabase', json: false }, listIo.io),
runKtxConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-metabase', json: false }, listIo.io),
).resolves.toBe(0);
expect(listIo.stdout()).toContain('1 -> prod-warehouse');
expect(listIo.stdout()).toContain('unhydrated');
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'set-sync-enabled',
projectDir,
@ -114,7 +114,7 @@ describe('runKloConnectionMapping', () => {
).resolves.toBe(0);
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'clear',
projectDir,
@ -127,12 +127,12 @@ describe('runKloConnectionMapping', () => {
});
it('lists Metabase yaml mapping bootstrap rows before any SQLite command writes', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'klo-cli-yaml-mapping-'));
await initKloProject({ projectDir, projectName: 'yaml-mapping' });
const project = await loadKloProject({ projectDir });
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-cli-yaml-mapping-'));
await initKtxProject({ projectDir, projectName: 'yaml-mapping' });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections: {
'prod-metabase': {
@ -145,21 +145,21 @@ describe('runKloConnectionMapping', () => {
'prod-warehouse': { driver: 'postgres', url: 'postgresql://readonly@db.test/analytics' },
},
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Seed yaml mappings',
);
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{ command: 'list', projectDir, connectionId: 'prod-metabase', json: false },
io.io,
),
).resolves.toBe(0);
expect(io.stdout()).toContain('1 -> prod-warehouse');
expect(io.stdout()).toContain('source: klo.yaml');
expect(io.stdout()).toContain('source: ktx.yaml');
});
it('refreshes Metabase discovery metadata through the injected runtime client', async () => {
@ -178,7 +178,7 @@ describe('runKloConnectionMapping', () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'refresh',
projectDir,
@ -194,7 +194,7 @@ describe('runKloConnectionMapping', () => {
expect(io.stdout()).toContain('Discovery: 1 database');
expect(client.cleanup).toHaveBeenCalledTimes(1);
const store = new LocalMetabaseSourceStateReader({ dbPath: join(projectDir, '.klo', 'db.sqlite') });
const store = new LocalMetabaseSourceStateReader({ dbPath: join(projectDir, '.ktx', 'db.sqlite') });
await expect(store.listDatabaseMappings('prod-metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 1, metabaseDatabaseName: 'Analytics', source: 'refresh' },
]);
@ -215,7 +215,7 @@ describe('runKloConnectionMapping', () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'set',
projectDir,
@ -228,7 +228,7 @@ describe('runKloConnectionMapping', () => {
),
).resolves.toBe(0);
await expect(
runKloConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-looker', json: false }, io.io),
runKtxConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-looker', json: false }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain('analytics -> prod-warehouse');
@ -242,7 +242,7 @@ describe('runKloConnectionMapping', () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'set',
projectDir,
@ -273,7 +273,7 @@ describe('runKloConnectionMapping', () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{ command: 'refresh', projectDir, connectionId: 'prod-looker', autoAccept: true },
io.io,
{
@ -298,12 +298,12 @@ describe('runKloConnectionMapping', () => {
});
it('validates Looker mappings through the canonical local warehouse descriptor', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'klo-cli-descriptor-validation-'));
await initKloProject({ projectDir, projectName: 'descriptor-validation' });
const project = await loadKloProject({ projectDir });
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-cli-descriptor-validation-'));
await initKtxProject({ projectDir, projectName: 'descriptor-validation' });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections: {
'prod-looker': {
@ -313,14 +313,14 @@ describe('runKloConnectionMapping', () => {
'prod-warehouse': { driver: 'postgresql', url: 'postgresql://readonly@db.test/analytics' },
},
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Seed descriptor validation',
);
const io = makeIo();
await expect(
runKloConnectionMapping({ command: 'validate', projectDir, connectionId: 'prod-looker' }, io.io),
runKtxConnectionMapping({ command: 'validate', projectDir, connectionId: 'prod-looker' }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain('Mapping validation passed: prod-looker');

View file

@ -1,5 +1,5 @@
import { readFile } from 'node:fs/promises';
import { localConnectionToWarehouseDescriptor } from '@klo/context/connections';
import { localConnectionToWarehouseDescriptor } from '@ktx/context/connections';
import {
DEFAULT_METABASE_CLIENT_CONFIG,
DefaultLookerConnectionClientFactory,
@ -12,20 +12,20 @@ import {
discoverMetabaseDatabases,
lookerCredentialsFromLocalConnection,
metabaseRuntimeConfigFromLocalConnection,
seedLocalMappingStateFromKloYaml,
seedLocalMappingStateFromKtxYaml,
validateLookerMappings,
validateMappingPhysicalMatch,
type LookerMappingClient,
type MetabaseRuntimeClient,
type MetabaseSyncMode,
} from '@klo/context/ingest';
import { type KloLocalProject, kloLocalStateDbPath, loadKloProject } from '@klo/context/project';
import type { KloCliIo } from '../index.js';
} from '@ktx/context/ingest';
import { type KtxLocalProject, ktxLocalStateDbPath, loadKtxProject } from '@ktx/context/project';
import type { KtxCliIo } from '../index.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/connection-mapping');
export type KloConnectionMappingArgs =
export type KtxConnectionMappingArgs =
| { command: 'list'; projectDir: string; connectionId: string; json: boolean }
| {
command: 'set';
@ -57,13 +57,13 @@ export type KloConnectionMappingArgs =
| { command: 'validate'; projectDir: string; connectionId: string }
| { command: 'clear'; projectDir: string; connectionId: string; metabaseDatabaseId?: number; mappingKey?: string };
interface KloConnectionMappingDeps {
interface KtxConnectionMappingDeps {
createMetabaseClient?: (
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
) => Promise<Pick<MetabaseRuntimeClient, 'getDatabases' | 'cleanup'>>;
createLookerClient?: (
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
) => Promise<Pick<LookerMappingClient, 'listLookerConnections'> & { cleanup?(): Promise<void> }>;
}
@ -85,7 +85,7 @@ function parseId(value: string, label: string): number {
}
async function createDefaultMetabaseClient(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
): Promise<Pick<MetabaseRuntimeClient, 'getDatabases' | 'cleanup'>> {
const factory = new DefaultMetabaseConnectionClientFactory(
@ -97,7 +97,7 @@ async function createDefaultMetabaseClient(
}
async function createDefaultLookerClient(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
): Promise<Pick<LookerMappingClient, 'listLookerConnections'> & { cleanup?(): Promise<void> }> {
const factory = new DefaultLookerConnectionClientFactory({
@ -110,30 +110,30 @@ async function createDefaultLookerClient(
};
}
function isLookerConnection(project: KloLocalProject, connectionId: string): boolean {
function isLookerConnection(project: KtxLocalProject, connectionId: string): boolean {
return String(project.config.connections[connectionId]?.driver ?? '').toLowerCase() === 'looker';
}
function assertLookerConnection(project: KloLocalProject, connectionId: string): void {
function assertLookerConnection(project: KtxLocalProject, connectionId: string): void {
if (!isLookerConnection(project, connectionId)) {
throw new Error(`Connection "${connectionId}" is not a Looker connection`);
}
}
function assertMetabaseConnection(project: KloLocalProject, connectionId: string): void {
function assertMetabaseConnection(project: KtxLocalProject, connectionId: string): void {
const connection = project.config.connections[connectionId];
if (!connection || String(connection.driver).toLowerCase() !== 'metabase') {
throw new Error(`Connection "${connectionId}" is not a Metabase connection`);
}
}
function assertTargetConnection(project: KloLocalProject, connectionId: string): void {
function assertTargetConnection(project: KtxLocalProject, connectionId: string): void {
if (!project.config.connections[connectionId]) {
throw new Error(`Target connection "${connectionId}" does not exist`);
}
}
function targetPhysicalInfo(project: KloLocalProject, connectionId: string) {
function targetPhysicalInfo(project: KtxLocalProject, connectionId: string) {
const descriptor = localConnectionToWarehouseDescriptor(connectionId, project.config.connections[connectionId]);
if (!descriptor) {
return { connection_type: 'UNKNOWN' };
@ -160,22 +160,22 @@ function renderMapping(
}
function renderLookerMapping(row: Awaited<ReturnType<LocalLookerRuntimeStore['listConnectionMappings']>>[number]): string {
const target = row.kloConnectionId ?? '[unmapped]';
const target = row.ktxConnectionId ?? '[unmapped]';
const metadata = [row.lookerDialect, row.lookerHost, row.lookerDatabase].filter(Boolean).join(', ');
return `${row.lookerConnectionName} -> ${target}${metadata ? ` (${metadata}, source: ${row.source})` : ` (source: ${row.source})`}`;
}
export async function runKloConnectionMapping(
args: KloConnectionMappingArgs,
io: KloCliIo = process,
deps: KloConnectionMappingDeps = {},
export async function runKtxConnectionMapping(
args: KtxConnectionMappingArgs,
io: KtxCliIo = process,
deps: KtxConnectionMappingDeps = {},
): Promise<number> {
try {
const project = await loadKloProject({ projectDir: args.projectDir });
await seedLocalMappingStateFromKloYaml(project, args.connectionId);
const project = await loadKtxProject({ projectDir: args.projectDir });
await seedLocalMappingStateFromKtxYaml(project, args.connectionId);
if (isLookerConnection(project, args.connectionId)) {
assertLookerConnection(project, args.connectionId);
const store = new LocalLookerRuntimeStore({ dbPath: kloLocalStateDbPath(project) });
const store = new LocalLookerRuntimeStore({ dbPath: ktxLocalStateDbPath(project) });
if (args.command === 'list') {
const rows = await store.listConnectionMappings(args.connectionId);
@ -191,7 +191,7 @@ export async function runKloConnectionMapping(
await store.upsertConnectionMapping({
lookerConnectionId: args.connectionId,
lookerConnectionName: args.key,
kloConnectionId: args.value,
ktxConnectionId: args.value,
source: 'cli',
});
io.stdout.write(`Set connectionMappings.${args.key} = ${args.value}\n`);
@ -219,13 +219,13 @@ export async function runKloConnectionMapping(
}
if (args.command === 'validate') {
const knownKloConnectionIds = new Set(Object.keys(project.config.connections));
const knownKtxConnectionIds = new Set(Object.keys(project.config.connections));
const knownConnectionTypes = new Map(
Object.entries(project.config.connections).map(([id, _config]) => [id, targetPhysicalInfo(project, id).connection_type]),
);
const validation = validateLookerMappings({
mappings: await store.readMappings(args.connectionId),
knownKloConnectionIds,
knownKtxConnectionIds,
knownConnectionTypes,
});
if (!validation.ok) {
@ -255,7 +255,7 @@ export async function runKloConnectionMapping(
}
assertMetabaseConnection(project, args.connectionId);
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
if (args.command === 'list') {
const rows = await store.listDatabaseMappings(args.connectionId);

View file

@ -1,17 +1,17 @@
import { type Command, Option } from '@commander-js/extra-typings';
import {
type KloCliCommandContext,
type KtxCliCommandContext,
parseNonEmptyAssignmentOption,
parsePositiveIntegerOption,
parseSafeConnectionIdOption,
resolveCommandProjectDir,
} from '../cli-program.js';
import {
type KloConnectionMetabaseSetupArgs,
type KtxConnectionMetabaseSetupArgs,
type MetabaseSetupMappingAssignment,
type MetabaseSetupSyncMode,
runKloConnectionMetabaseSetup,
runKtxConnectionMetabaseSetup,
} from './connection-metabase-setup.js';
const SYNC_MODE_CHOICES = ['ALL', 'ONLY', 'EXCEPT'] as const satisfies readonly MetabaseSetupSyncMode[];
@ -51,21 +51,21 @@ function collectMappingOption(
}
async function runMetabaseSetupArgs(
context: KloCliCommandContext,
args: KloConnectionMetabaseSetupArgs,
context: KtxCliCommandContext,
args: KtxConnectionMetabaseSetupArgs,
): Promise<void> {
const runner = context.deps.connectionMetabaseSetup ?? runKloConnectionMetabaseSetup;
const runner = context.deps.connectionMetabaseSetup ?? runKtxConnectionMetabaseSetup;
context.setExitCode(await runner(args, context.io));
}
export function registerConnectionMetabaseCommands(connection: Command, context: KloCliCommandContext): void {
export function registerConnectionMetabaseCommands(connection: Command, context: KtxCliCommandContext): void {
const metabase = connection
.command('metabase')
.description('Configure Metabase connections')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
metabase.action(() => {
@ -76,7 +76,7 @@ export function registerConnectionMetabaseCommands(connection: Command, context:
metabase
.command('setup')
.description('Guided setup for a Metabase connection')
.option('--id <connectionId>', 'KLO connection id to write', parseSafeConnectionIdOption)
.option('--id <connectionId>', 'KTX connection id to write', parseSafeConnectionIdOption)
.option('--url <url>', 'Metabase API URL')
.addOption(new Option('--api-key <key>', 'Metabase API key').conflicts('mintApiKey'))
.option('--mint-api-key', 'Mint a Metabase API key with credentials', false)
@ -85,10 +85,10 @@ export function registerConnectionMetabaseCommands(connection: Command, context:
.addHelpText(
'after',
'\nGuided equivalent of:\n' +
' klo connection mapping refresh <connectionId> --auto-accept\n' +
' klo connection mapping set <connectionId> databaseMappings <id>=<target>\n' +
' klo connection mapping set-sync-enabled <connectionId> <id> --enabled true\n' +
' klo ingest <connectionId>\n',
' ktx connection mapping refresh <connectionId> --auto-accept\n' +
' ktx connection mapping set <connectionId> databaseMappings <id>=<target>\n' +
' ktx connection mapping set-sync-enabled <connectionId> <id> --enabled true\n' +
' ktx ingest <connectionId>\n',
)
.option(
'--map <metabaseDatabaseId=targetConnectionId>',

View file

@ -1,11 +1,11 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { LocalMetabaseSourceStateReader } from '@klo/context/ingest';
import { initKloProject, kloLocalStateDbPath, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
import { LocalMetabaseSourceStateReader } from '@ktx/context/ingest';
import { initKtxProject, ktxLocalStateDbPath, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloConnectionMetabaseSetup } from './connection-metabase-setup.js';
import { runKtxConnectionMetabaseSetup } from './connection-metabase-setup.js';
const CANCEL_PROMPT = Symbol('cancel');
@ -135,7 +135,7 @@ function makeIo(options: { isTTY?: boolean; stdinIsTTY?: boolean } = {}) {
};
}
describe('runKloConnectionMetabaseSetup', () => {
describe('runKtxConnectionMetabaseSetup', () => {
const fakeMetabaseCredential = 'mb_example';
const existingMetabaseCredential = 'mb_existing';
const fakeAdminCredential = 'pw';
@ -144,9 +144,9 @@ describe('runKloConnectionMetabaseSetup', () => {
let projectDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-metabase-setup-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-metabase-setup-'));
projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'metabase-setup' });
await initKtxProject({ projectDir, projectName: 'metabase-setup' });
});
afterEach(async () => {
@ -154,15 +154,15 @@ describe('runKloConnectionMetabaseSetup', () => {
});
async function writeConnections(connections: Record<string, { driver: string; [key: string]: unknown }>) {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections,
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Seed Metabase setup test connections',
);
}
@ -208,7 +208,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -230,17 +230,17 @@ describe('runKloConnectionMetabaseSetup', () => {
expect(io.stdout()).toContain('Connection: metabase');
expect(io.stdout()).toContain('Discovered 1 database');
expect(io.stdout()).toContain(`klo ingest metabase --project-dir ${projectDir}`);
expect(io.stdout()).toContain(`ktx ingest metabase --project-dir ${projectDir}`);
expect(io.stdout()).not.toContain('mb_example');
expect(io.stderr()).not.toContain('mb_example');
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
expect(config).toContain('api_url: http://metabase.example.test:3000');
expect(config).toContain('api_key: mb_example');
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{
metabaseDatabaseId: 2,
@ -275,7 +275,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -295,8 +295,8 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
]);
@ -314,7 +314,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -350,7 +350,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -370,8 +370,8 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
]);
@ -384,7 +384,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -412,7 +412,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -440,7 +440,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const missingUsernameIo = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -462,7 +462,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const missingPasswordIo = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -500,7 +500,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const mintingIo = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -537,7 +537,7 @@ describe('runKloConnectionMetabaseSetup', () => {
expect(mintingIo.stdout()).not.toContain(fakeAdminCredential);
expect(mintingIo.stderr()).not.toContain(fakeAdminCredential);
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
expect(config).toContain('api_url: http://metabase.example.test:3000');
expect(config).toContain(`api_key: ${mintedMetabaseCredential}`);
@ -548,7 +548,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -590,7 +590,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -640,7 +640,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -660,8 +660,8 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 1, targetConnectionId: 'orbit', syncEnabled: true },
{ metabaseDatabaseId: 2, targetConnectionId: null, syncEnabled: false },
@ -676,7 +676,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -712,7 +712,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -759,7 +759,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -782,12 +782,12 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(1);
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
expect(io.stderr()).toContain(`klo ingest metabase --project-dir ${projectDir}`);
expect(io.stderr()).toContain(`ktx ingest metabase --project-dir ${projectDir}`);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit' },
]);
@ -810,7 +810,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -857,7 +857,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const interactiveMetabaseCredential = 'mb_interactive_fixture';
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -882,13 +882,13 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
expect(config).toContain('api_url: http://metabase.example.test:3000');
expect(config).toContain(`api_key: ${interactiveMetabaseCredential}`);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{
metabaseDatabaseId: 2,
@ -931,7 +931,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const events: string[] = [];
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -958,8 +958,8 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
{ metabaseDatabaseId: 3, targetConnectionId: 'warehouse2', syncEnabled: false },
@ -997,7 +997,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const events: string[] = [];
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -1023,7 +1023,7 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
expect(events).toContain('intro:KLO Metabase setup');
expect(events).toContain('intro:KTX Metabase setup');
expect(events.some((event) => event.startsWith('spinner.start:Testing Metabase connection'))).toBe(true);
expect(events.some((event) => event.startsWith('spinner.stop:Metabase reachable'))).toBe(true);
expect(events.some((event) => event.startsWith('spinner.start:Discovering Metabase databases'))).toBe(true);
@ -1053,7 +1053,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -1081,7 +1081,7 @@ describe('runKloConnectionMetabaseSetup', () => {
},
});
const beforeConfig = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const beforeConfig = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
const metabaseClient = makeMetabaseClient({
testConnectionSuccess: true,
databases: [
@ -1098,7 +1098,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const cancelMetabaseCredential = 'mb_cancel_fixture';
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -1126,11 +1126,11 @@ describe('runKloConnectionMetabaseSetup', () => {
expect(io.stderr()).toContain('Setup cancelled.');
expect(io.stderr()).not.toContain(cancelMetabaseCredential);
const afterConfig = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const afterConfig = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(afterConfig).toBe(beforeConfig);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toEqual([]);
});
});

View file

@ -12,7 +12,7 @@ import {
select,
text,
} from '@clack/prompts';
import { localConnectionToWarehouseDescriptor } from '@klo/context/connections';
import { localConnectionToWarehouseDescriptor } from '@ktx/context/connections';
import {
DEFAULT_METABASE_CLIENT_CONFIG,
DefaultMetabaseConnectionClientFactory,
@ -23,21 +23,21 @@ import {
type MetabaseSyncMode,
metabaseRuntimeConfigFromLocalConnection,
validateMappingPhysicalMatch,
} from '@klo/context/ingest';
} from '@ktx/context/ingest';
import {
type KloLocalProject,
type KloProjectConnectionConfig,
kloLocalStateDbPath,
loadKloProject,
serializeKloProjectConfig,
} from '@klo/context/project';
type KtxLocalProject,
type KtxProjectConnectionConfig,
ktxLocalStateDbPath,
loadKtxProject,
serializeKtxProjectConfig,
} from '@ktx/context/project';
import { createClackSpinner, type KloCliSpinner } from '../clack.js';
import type { KloCliIo } from '../cli-runtime.js';
import { createClackSpinner, type KtxCliSpinner } from '../clack.js';
import type { KtxCliIo } from '../cli-runtime.js';
import { withMenuOptionsSpacing, withMultiselectNavigation } from '../prompt-navigation.js';
import { type KloPublicIngestArgs, runKloPublicIngest } from '../public-ingest.js';
import { type KtxPublicIngestArgs, runKtxPublicIngest } from '../public-ingest.js';
export type KloMetabaseSetupInputMode = 'auto' | 'disabled';
export type KtxMetabaseSetupInputMode = 'auto' | 'disabled';
export type MetabaseSetupSyncMode = MetabaseSyncMode;
@ -56,7 +56,7 @@ export interface MetabaseSetupPromptAdapter {
outro(message?: string): void;
note(message: string, title: string): void;
log: MetabaseSetupLogger;
spinner(): KloCliSpinner;
spinner(): KtxCliSpinner;
select<T extends string>(options: { message: string; options: Array<MetabaseSetupPromptOption<T>> }): Promise<T>;
multiselect<Value extends number | string>(options: {
message: string;
@ -71,7 +71,7 @@ export interface MetabaseSetupPromptAdapter {
cancel(message: string): void;
}
type KloMetabaseSetupInteractiveIo = KloCliIo & {
type KtxMetabaseSetupInteractiveIo = KtxCliIo & {
stdin?: { isTTY?: boolean };
};
@ -86,9 +86,9 @@ export interface MintMetabaseApiKeyArgs {
password: string;
}
export type MintMetabaseApiKey = (args: MintMetabaseApiKeyArgs, io: KloCliIo) => Promise<string>;
export type MintMetabaseApiKey = (args: MintMetabaseApiKeyArgs, io: KtxCliIo) => Promise<string>;
export interface KloConnectionMetabaseSetupArgs {
export interface KtxConnectionMetabaseSetupArgs {
command: 'setup';
projectDir: string;
connectionId?: string;
@ -102,20 +102,20 @@ export interface KloConnectionMetabaseSetupArgs {
syncMode: MetabaseSetupSyncMode;
runIngest: boolean;
yes: boolean;
inputMode: KloMetabaseSetupInputMode;
inputMode: KtxMetabaseSetupInputMode;
}
export interface KloConnectionMetabaseSetupDeps {
export interface KtxConnectionMetabaseSetupDeps {
createMetabaseClient?: (
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
) => Promise<Pick<MetabaseRuntimeClient, 'testConnection' | 'getDatabases' | 'cleanup'>>;
mintMetabaseApiKey?: MintMetabaseApiKey;
prompts?: MetabaseSetupPromptAdapter;
runPublicIngest?: (args: Extract<KloPublicIngestArgs, { command: 'run' }>, io: KloCliIo) => Promise<number>;
runPublicIngest?: (args: Extract<KtxPublicIngestArgs, { command: 'run' }>, io: KtxCliIo) => Promise<number>;
}
function isMetabaseConnection(connection: KloProjectConnectionConfig | undefined): boolean {
function isMetabaseConnection(connection: KtxProjectConnectionConfig | undefined): boolean {
return (
String(connection?.driver ?? '')
.trim()
@ -131,22 +131,22 @@ function uniqueSorted(values: number[]): number[] {
return [...new Set(values)].sort((a, b) => a - b);
}
function resolveMetabaseUrl(connection: KloProjectConnectionConfig | undefined): string | undefined {
function resolveMetabaseUrl(connection: KtxProjectConnectionConfig | undefined): string | undefined {
return stringField(connection?.api_url) ?? stringField(connection?.apiUrl) ?? stringField(connection?.url);
}
function resolveLiteralMetabaseApiKey(connection: KloProjectConnectionConfig | undefined): string | undefined {
function resolveLiteralMetabaseApiKey(connection: KtxProjectConnectionConfig | undefined): string | undefined {
return stringField(connection?.api_key) ?? stringField(connection?.apiKey);
}
function listMetabaseConnectionIds(project: KloLocalProject): string[] {
function listMetabaseConnectionIds(project: KtxLocalProject): string[] {
return Object.entries(project.config.connections)
.filter(([_connectionId, connection]) => isMetabaseConnection(connection))
.map(([connectionId]) => connectionId)
.sort();
}
function listWarehouseConnectionIds(project: KloLocalProject): string[] {
function listWarehouseConnectionIds(project: KtxLocalProject): string[] {
return Object.entries(project.config.connections)
.filter(([connectionId, connection]) => localConnectionToWarehouseDescriptor(connectionId, connection) != null)
.map(([connectionId]) => connectionId)
@ -165,7 +165,7 @@ function redactSecrets(message: string, secrets: string[]): string {
}
async function createDefaultMetabaseClient(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
): Promise<Pick<MetabaseRuntimeClient, 'testConnection' | 'getDatabases' | 'cleanup'>> {
const factory = new DefaultMetabaseConnectionClientFactory(
@ -192,7 +192,7 @@ async function defaultMintMetabaseApiKey(args: MintMetabaseApiKeyArgs): Promise<
const mintedKey = await sessionClient.createApiKey({
groupId: adminGroup.id,
name: `KLO CLI ${new Date().toISOString()}`,
name: `KTX CLI ${new Date().toISOString()}`,
});
const trimmedKey = stringField(mintedKey);
if (!trimmedKey) {
@ -237,7 +237,7 @@ export function createClackMetabaseSetupPromptAdapter(): MetabaseSetupPromptAdap
log.error(message);
},
},
spinner(): KloCliSpinner {
spinner(): KtxCliSpinner {
return createClackSpinner();
},
async select<T extends string>(options: {
@ -271,8 +271,8 @@ export function createClackMetabaseSetupPromptAdapter(): MetabaseSetupPromptAdap
}
function isInteractiveMetabaseSetupIo(
args: Pick<KloConnectionMetabaseSetupArgs, 'inputMode'>,
io: KloMetabaseSetupInteractiveIo,
args: Pick<KtxConnectionMetabaseSetupArgs, 'inputMode'>,
io: KtxMetabaseSetupInteractiveIo,
): boolean {
return args.inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
}
@ -295,7 +295,7 @@ function normalizeDiscoveredDatabases(databases: MetabaseDatabase[]): Array<{
}));
}
function targetPhysicalInfo(project: KloLocalProject, connectionId: string) {
function targetPhysicalInfo(project: KtxLocalProject, connectionId: string) {
const descriptor = localConnectionToWarehouseDescriptor(connectionId, project.config.connections[connectionId]);
if (!descriptor) {
return { connection_type: 'UNKNOWN' };
@ -338,23 +338,23 @@ function noteMetabaseSetupSummary(options: {
);
}
export async function runKloConnectionMetabaseSetup(
args: KloConnectionMetabaseSetupArgs,
io: KloCliIo,
deps: KloConnectionMetabaseSetupDeps = {},
export async function runKtxConnectionMetabaseSetup(
args: KtxConnectionMetabaseSetupArgs,
io: KtxCliIo,
deps: KtxConnectionMetabaseSetupDeps = {},
): Promise<number> {
let apiKeyForRedaction = args.apiKey;
let passwordForRedaction = args.metabasePassword;
const interactiveIo = io as KloMetabaseSetupInteractiveIo;
const interactiveIo = io as KtxMetabaseSetupInteractiveIo;
const isInteractive = isInteractiveMetabaseSetupIo(args, interactiveIo);
const prompts = deps.prompts ?? (isInteractive ? createClackMetabaseSetupPromptAdapter() : undefined);
try {
if (isInteractive && prompts) {
prompts.intro('KLO Metabase setup');
prompts.intro('KTX Metabase setup');
}
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
const existingMetabaseConnectionIds = listMetabaseConnectionIds(project);
let connectionId: string;
@ -491,7 +491,7 @@ export async function runKloConnectionMetabaseSetup(
throw new Error('Metabase API key is required (use --api-key)');
}
const transientConnectionConfig: KloProjectConnectionConfig = {
const transientConnectionConfig: KtxProjectConnectionConfig = {
...(existingConnection ?? {}),
driver: 'metabase',
api_url: url,
@ -504,7 +504,7 @@ export async function runKloConnectionMetabaseSetup(
[connectionId]: transientConnectionConfig,
},
};
const discoveryProject: KloLocalProject = { ...project, config: configWithTransient };
const discoveryProject: KtxLocalProject = { ...project, config: configWithTransient };
for (const mapping of args.mappings) {
if (!configWithTransient.connections[mapping.targetConnectionId]) {
@ -618,7 +618,7 @@ export async function runKloConnectionMetabaseSetup(
}
const targetConnectionId = await prompts.select({
message: `Map Metabase database ${database.id} ("${database.name}") to which KLO connection?`,
message: `Map Metabase database ${database.id} ("${database.name}") to which KTX connection?`,
options: warehouseConnectionIds.map((warehouseId) => ({ value: warehouseId, label: warehouseId })),
});
resolvedMappings.push({ metabaseDatabaseId: databaseId, targetConnectionId });
@ -641,7 +641,7 @@ export async function runKloConnectionMetabaseSetup(
syncEnabledDatabaseIds: resolvedSyncEnabledDatabaseIds,
});
const confirmed = await prompts.confirm({
message: 'Write changes to klo.yaml and enable sync?',
message: 'Write changes to ktx.yaml and enable sync?',
initialValue: true,
});
if (!confirmed) {
@ -675,15 +675,15 @@ export async function runKloConnectionMetabaseSetup(
}
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(configWithTransient),
'klo',
'klo@example.com',
'ktx.yaml',
serializeKtxProjectConfig(configWithTransient),
'ktx',
'ktx@example.com',
`Setup Metabase connection ${connectionId}`,
);
const updatedProject = await loadKloProject({ projectDir: args.projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir: args.projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await store.refreshDiscoveredDatabases({ connectionId, discovered });
@ -716,7 +716,7 @@ export async function runKloConnectionMetabaseSetup(
const unhydrated = await store.getUnhydratedSyncEnabledMappingIds(connectionId);
if (unhydrated.length > 0) {
io.stderr.write(
`Sync-enabled mappings are missing discovery metadata; run klo connection mapping refresh ${connectionId} --auto-accept\n`,
`Sync-enabled mappings are missing discovery metadata; run ktx connection mapping refresh ${connectionId} --auto-accept\n`,
);
return 1;
}
@ -743,10 +743,10 @@ export async function runKloConnectionMetabaseSetup(
io.stdout.write(`Connection: ${connectionId}\n`);
io.stdout.write(`Discovered ${discovered.length} ${discovered.length === 1 ? 'database' : 'databases'}\n`);
io.stdout.write(`Next: klo ingest ${connectionId} --project-dir ${args.projectDir}\n`);
io.stdout.write(`Next: ktx ingest ${connectionId} --project-dir ${args.projectDir}\n`);
if (args.runIngest) {
const ingestRunner = deps.runPublicIngest ?? runKloPublicIngest;
const ingestRunner = deps.runPublicIngest ?? runKtxPublicIngest;
const exitCode = await ingestRunner(
{
command: 'run',
@ -759,7 +759,7 @@ export async function runKloConnectionMetabaseSetup(
io,
);
if (exitCode !== 0) {
io.stderr.write(`Ingest failed; re-run: klo ingest ${connectionId} --project-dir ${args.projectDir}\n`);
io.stderr.write(`Ingest failed; re-run: ktx ingest ${connectionId} --project-dir ${args.projectDir}\n`);
return 1;
}
}

View file

@ -1,6 +1,6 @@
import { type Command, InvalidArgumentError } from '@commander-js/extra-typings';
import { collectOption, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KloConnectionNotionArgs } from './connection-notion.js';
import { collectOption, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxConnectionNotionArgs } from './connection-notion.js';
interface NotionPickOptions {
input?: boolean;
@ -36,7 +36,7 @@ function normalizeNotionPageId(value: string): string {
return `${lower.slice(0, 8)}-${lower.slice(8, 12)}-${lower.slice(12, 16)}-${lower.slice(16, 20)}-${lower.slice(20)}`;
}
function buildPickArgs(connectionId: string, projectDir: string, options: NotionPickOptions): KloConnectionNotionArgs {
function buildPickArgs(connectionId: string, projectDir: string, options: NotionPickOptions): KtxConnectionNotionArgs {
if (options.input !== false) {
return {
command: 'pick',
@ -59,19 +59,19 @@ function buildPickArgs(connectionId: string, projectDir: string, options: Notion
};
}
async function runConnectionNotionArgs(context: KloCliCommandContext, args: KloConnectionNotionArgs): Promise<void> {
const runner = context.deps.connectionNotion ?? (await import('./connection-notion.js')).runKloConnectionNotion;
async function runConnectionNotionArgs(context: KtxCliCommandContext, args: KtxConnectionNotionArgs): Promise<void> {
const runner = context.deps.connectionNotion ?? (await import('./connection-notion.js')).runKtxConnectionNotion;
context.setExitCode(await runner(args, context.io));
}
export function registerConnectionNotionCommands(connect: Command, context: KloCliCommandContext): void {
export function registerConnectionNotionCommands(connect: Command, context: KtxCliCommandContext): void {
const notion = connect
.command('notion')
.description('Configure Notion source selection')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
notion.action(() => {

View file

@ -10,7 +10,7 @@ import {
type PickerCommand,
type PickerState,
} from './connection-notion-tree.js';
import type { KloCliIo } from '../index.js';
import type { KtxCliIo } from '../index.js';
const COLOR_THEME = {
text: 'white',
@ -28,9 +28,9 @@ const NO_COLOR_THEME = {
type NotionPickerTheme = Record<keyof typeof COLOR_THEME, string>;
export interface NotionPickerTuiIo extends KloCliIo {
export interface NotionPickerTuiIo extends KtxCliIo {
stdin?: { isTTY?: boolean; setRawMode?(value: boolean): void };
stdout: KloCliIo['stdout'] & { isTTY?: boolean; columns?: number; rows?: number };
stdout: KtxCliIo['stdout'] & { isTTY?: boolean; columns?: number; rows?: number };
}
interface InkKey {

View file

@ -2,11 +2,11 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
initKloProject,
loadKloProject,
serializeKloProjectConfig,
type KloProjectConfig,
} from '@klo/context/project';
initKtxProject,
loadKtxProject,
serializeKtxProjectConfig,
type KtxProjectConfig,
} from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
applyNotionPickerWriteback,
@ -14,7 +14,7 @@ import {
notionPickerPageFromSearchResult,
normalizeNotionPageId,
resolveNotionWorkspaceLabel,
runKloConnectionNotion,
runKtxConnectionNotion,
type NotionPickerApi,
type PickerRenderInput,
type PickerRenderResult,
@ -91,24 +91,24 @@ describe('normalizeNotionPageId', () => {
});
});
describe('runKloConnectionNotion', () => {
describe('runKtxConnectionNotion', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-notion-pick-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-notion-pick-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
async function writeProjectConfig(projectDir: string, config: KloProjectConfig): Promise<void> {
const project = await loadKloProject({ projectDir });
async function writeProjectConfig(projectDir: string, config: KtxProjectConfig): Promise<void> {
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(config),
'klo',
'klo@example.com',
'ktx.yaml',
serializeKtxProjectConfig(config),
'ktx',
'ktx@example.com',
'seed test config',
);
}
@ -120,7 +120,7 @@ describe('runKloConnectionNotion', () => {
});
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir: '/tmp/project',
@ -138,7 +138,7 @@ describe('runKloConnectionNotion', () => {
it('writes selected root_page_ids while preserving every other Notion connection field', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -160,7 +160,7 @@ describe('runKloConnectionNotion', () => {
const io = makeIo();
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir,
@ -175,7 +175,7 @@ describe('runKloConnectionNotion', () => {
),
).resolves.toBe(0);
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(yaml).toContain('crawl_mode: selected_roots');
expect(yaml).toContain('root_page_ids:');
expect(yaml).toContain('11111111-2222-3333-4444-555555555555');
@ -193,7 +193,7 @@ describe('runKloConnectionNotion', () => {
it('rejects empty writeback, missing connections, and non-Notion connections', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -204,7 +204,7 @@ describe('runKloConnectionNotion', () => {
},
},
});
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
await expect(applyNotionPickerWriteback(project, 'warehouse', [])).rejects.toThrow(
'connection notion pick requires at least one root page id',
@ -297,7 +297,7 @@ describe('runKloConnectionNotion', () => {
it('runs interactive discovery, warns about stale roots, renders the TUI, and saves selected roots', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -330,7 +330,7 @@ describe('runKloConnectionNotion', () => {
const io = makeIo();
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir,
@ -346,7 +346,7 @@ describe('runKloConnectionNotion', () => {
),
).resolves.toBe(0);
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(yaml).toContain('crawl_mode: selected_roots');
expect(yaml).toContain(PAGE_IDS.engineering);
expect(yaml).not.toContain(PAGE_IDS.stale);
@ -357,7 +357,7 @@ describe('runKloConnectionNotion', () => {
it('passes partial-discovery warnings into the TUI banner state', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -394,7 +394,7 @@ describe('runKloConnectionNotion', () => {
const io = makeIo();
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir,
@ -422,7 +422,7 @@ describe('runKloConnectionNotion', () => {
it('quits interactive mode without writing when the TUI returns quit', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -440,11 +440,11 @@ describe('runKloConnectionNotion', () => {
},
},
});
const before = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const before = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
const io = makeIo();
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir,
@ -460,7 +460,7 @@ describe('runKloConnectionNotion', () => {
),
).resolves.toBe(0);
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toBe(before);
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toBe(before);
expect(io.stdout()).toContain('No changes saved.');
});
});

View file

@ -1,12 +1,12 @@
import { parseNotionConnectionConfig, resolveNotionAuthToken } from '@klo/context/connections';
import { type NotionApi, type NotionBotInfo, NotionClient } from '@klo/context/ingest';
import { parseNotionConnectionConfig, resolveNotionAuthToken } from '@ktx/context/connections';
import { type NotionApi, type NotionBotInfo, NotionClient } from '@ktx/context/ingest';
import {
type KloLocalProject,
type KloProjectConnectionConfig,
loadKloProject,
serializeKloProjectConfig,
} from '@klo/context/project';
import type { KloCliIo } from '../index.js';
type KtxLocalProject,
type KtxProjectConnectionConfig,
loadKtxProject,
serializeKtxProjectConfig,
} from '@ktx/context/project';
import type { KtxCliIo } from '../index.js';
import { profileMark } from '../startup-profile.js';
import { buildInitialState, buildPickerTree, type NotionPickerPageInput } from './connection-notion-tree.js';
import {
@ -18,7 +18,7 @@ import {
profileMark('module:commands/connection-notion');
export type KloConnectionNotionArgs =
export type KtxConnectionNotionArgs =
| {
command: 'pick';
projectDir: string;
@ -36,9 +36,9 @@ export type KloConnectionNotionArgs =
export type NotionPickerApi = Pick<NotionApi, 'search' | 'retrieveBotUser'>;
export type { PickerRenderInput, PickerRenderResult };
interface KloConnectionNotionDeps {
interface KtxConnectionNotionDeps {
env?: Record<string, string | undefined>;
loadProject?: typeof loadKloProject;
loadProject?: typeof loadKtxProject;
createNotionApi?: (authToken: string) => NotionPickerApi;
renderPicker?: (input: PickerRenderInput, io: NotionPickerTuiIo) => Promise<PickerRenderResult>;
}
@ -168,7 +168,7 @@ export async function resolveNotionWorkspaceLabel(api: NotionPickerApi, connecti
}
}
function notionConnection(project: KloLocalProject, connectionId: string): KloProjectConnectionConfig {
function notionConnection(project: KtxLocalProject, connectionId: string): KtxProjectConnectionConfig {
const connection = project.config.connections[connectionId];
if (!connection) {
throw new Error(`Connection "${connectionId}" not found`);
@ -180,7 +180,7 @@ function notionConnection(project: KloLocalProject, connectionId: string): KloPr
}
export async function applyNotionPickerWriteback(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
rootPageIds: string[],
): Promise<void> {
@ -202,22 +202,22 @@ export async function applyNotionPickerWriteback(
};
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(nextConfig),
'klo',
'klo@example.com',
'ktx.yaml',
serializeKtxProjectConfig(nextConfig),
'ktx',
'ktx@example.com',
`Pick Notion roots: ${connectionId} (${rootPageIds.length} pages)`,
);
}
export async function runKloConnectionNotion(
args: KloConnectionNotionArgs,
io: KloCliIo = process,
deps: KloConnectionNotionDeps = {},
export async function runKtxConnectionNotion(
args: KtxConnectionNotionArgs,
io: KtxCliIo = process,
deps: KtxConnectionNotionDeps = {},
): Promise<number> {
try {
assertSafeConnectionId(args.connectionId);
const loadProject = deps.loadProject ?? loadKloProject;
const loadProject = deps.loadProject ?? loadKtxProject;
if (args.mode === 'interactive') {
const project = await loadProject({ projectDir: args.projectDir });

View file

@ -1,14 +1,14 @@
import { type Command, Option } from '@commander-js/extra-typings';
import {
type CommandWithGlobalOptions,
type KloCliCommandContext,
type KtxCliCommandContext,
resolveCommandProjectDirOverride,
} from '../cli-program.js';
import {
type KloDemoArgs,
type KloDemoInputMode,
type KloDemoMode,
type KloDemoOutputMode,
type KtxDemoArgs,
type KtxDemoInputMode,
type KtxDemoMode,
type KtxDemoOutputMode,
} from '../demo.js';
import { defaultDemoProjectDir } from '../demo-assets.js';
import { resolveProjectDir } from '../project-dir.js';
@ -23,7 +23,7 @@ interface DemoOptions {
projectDir?: string;
}
function demoOutputMode(options: { plain?: boolean; json?: boolean }): KloDemoOutputMode {
function demoOutputMode(options: { plain?: boolean; json?: boolean }): KtxDemoOutputMode {
if (options.json === true) {
return 'json';
}
@ -37,14 +37,14 @@ function demoDoctorOutputMode(options: { json?: boolean }): 'plain' | 'json' {
return options.json === true ? 'json' : 'plain';
}
function demoInspectOutputMode(options: { plain?: boolean; json?: boolean }): KloDemoOutputMode {
function demoInspectOutputMode(options: { plain?: boolean; json?: boolean }): KtxDemoOutputMode {
if (options.json === true) {
return 'json';
}
return 'plain';
}
function demoInputMode(options: { input?: boolean }): { inputMode?: KloDemoInputMode } {
function demoInputMode(options: { input?: boolean }): { inputMode?: KtxDemoInputMode } {
return options.input === false ? { inputMode: 'disabled' } : {};
}
@ -118,19 +118,19 @@ export function resolveDemoCommandOptions<T>(command: { opts: () => T; optsWithG
return { ...inherited, ...definedOptions(command.opts() as Record<string, unknown>, inherited, command) } as T;
}
async function runDemoArgs(context: KloCliCommandContext, args: KloDemoArgs): Promise<void> {
const runner = context.deps.demo ?? (await import('../demo.js')).runKloDemo;
async function runDemoArgs(context: KtxCliCommandContext, args: KtxDemoArgs): Promise<void> {
const runner = context.deps.demo ?? (await import('../demo.js')).runKtxDemo;
context.setExitCode(await runner(args, context.io));
}
export function registerDemoCommands(
program: Command,
context: KloCliCommandContext,
context: KtxCliCommandContext,
options: { description?: string } = {},
): void {
const demo = program
.command('demo')
.description(options.description ?? 'Run the pre-seeded KLO demo or a full LLM-backed demo')
.description(options.description ?? 'Run the pre-seeded KTX demo or a full LLM-backed demo')
.addOption(
new Option('--mode <mode>', 'Demo mode: seeded (default), replay, or full')
.choices(['seeded', 'replay', 'full'])
@ -260,7 +260,7 @@ export function registerDemoCommands(
.addOption(new Option('--plain', 'Print plain text output instead of the visual demo').conflicts('json'))
.addOption(new Option('--json', 'Print JSON output').conflicts('plain'))
.option('--no-input', 'Disable interactive terminal input')
.action(async (_options, command: { opts: () => { mode: KloDemoMode } & DemoOptions }) => {
.action(async (_options, command: { opts: () => { mode: KtxDemoMode } & DemoOptions }) => {
const options = resolveDemoCommandOptions(command);
await runDemoArgs(context, {
command: 'ingest',

View file

@ -1,6 +1,6 @@
import type { Command } from '@commander-js/extra-typings';
import { type CommandWithGlobalOptions, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KloDoctorArgs } from '../doctor.js';
import { type CommandWithGlobalOptions, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxDoctorArgs } from '../doctor.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/doctor-commands');
@ -13,15 +13,15 @@ function inputMode(options: { input?: boolean }): { inputMode?: 'disabled' } {
return options.input === false ? { inputMode: 'disabled' } : {};
}
async function runDoctorArgs(context: KloCliCommandContext, args: KloDoctorArgs): Promise<void> {
const runner = context.deps.doctor ?? (await import('../doctor.js')).runKloDoctor;
async function runDoctorArgs(context: KtxCliCommandContext, args: KtxDoctorArgs): Promise<void> {
const runner = context.deps.doctor ?? (await import('../doctor.js')).runKtxDoctor;
context.setExitCode(await runner(args, context.io));
}
export function registerDoctorCommands(program: Command, context: KloCliCommandContext): void {
export function registerDoctorCommands(program: Command, context: KtxCliCommandContext): void {
const doctor = program
.command('doctor')
.description('Check KLO setup, project, and demo readiness')
.description('Check KTX setup, project, and demo readiness')
.option('--json', 'Print JSON output', false)
.option('--no-input', 'Disable interactive terminal input')
.action(async (options: { json?: boolean; input?: boolean }, command) => {
@ -35,7 +35,7 @@ export function registerDoctorCommands(program: Command, context: KloCliCommandC
doctor
.command('setup')
.description('Check KLO install, build, and local runtime readiness')
.description('Check KTX install, build, and local runtime readiness')
.option('--json', 'Print JSON output', false)
.option('--no-input', 'Disable interactive terminal input')
.action(

View file

@ -1,22 +1,22 @@
import { resolve } from 'node:path';
import { type Command, Option } from '@commander-js/extra-typings';
import { type KloCliCommandContext, type OutputModeOptions, resolveCommandProjectDir } from '../cli-program.js';
import type { KloCliDeps, KloCliIo } from '../index.js';
import type { KloIngestArgs, KloIngestOutputMode } from '../ingest.js';
import { type KtxCliCommandContext, type OutputModeOptions, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxCliDeps, KtxCliIo } from '../index.js';
import type { KtxIngestArgs, KtxIngestOutputMode } from '../ingest.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/ingest-commands');
interface IngestCommandOptions {
runIngestWithProgress: (
args: KloIngestArgs,
io: KloCliIo,
deps: KloCliDeps,
defaultRunIngest: (args: KloIngestArgs, io: KloCliIo) => Promise<number>,
args: KtxIngestArgs,
io: KtxCliIo,
deps: KtxCliDeps,
defaultRunIngest: (args: KtxIngestArgs, io: KtxCliIo) => Promise<number>,
) => Promise<number>;
}
function outputMode(options: OutputModeOptions): KloIngestOutputMode {
function outputMode(options: OutputModeOptions): KtxIngestOutputMode {
if (options.json === true) {
return 'json';
}
@ -26,7 +26,7 @@ function outputMode(options: OutputModeOptions): KloIngestOutputMode {
return 'plain';
}
function watchOutputMode(options: OutputModeOptions): KloIngestOutputMode {
function watchOutputMode(options: OutputModeOptions): KtxIngestOutputMode {
if (options.json === true) {
return 'json';
}
@ -36,22 +36,22 @@ function watchOutputMode(options: OutputModeOptions): KloIngestOutputMode {
return 'viz';
}
function inputMode(options: OutputModeOptions): Pick<KloIngestArgs, 'inputMode'> {
function inputMode(options: OutputModeOptions): Pick<KtxIngestArgs, 'inputMode'> {
return options.input === false ? { inputMode: 'disabled' } : {};
}
async function runIngestArgs(
context: KloCliCommandContext,
args: KloIngestArgs,
context: KtxCliCommandContext,
args: KtxIngestArgs,
options: IngestCommandOptions,
): Promise<void> {
const { runKloIngest } = await import('../ingest.js');
context.setExitCode(await options.runIngestWithProgress(args, context.io, context.deps, runKloIngest));
const { runKtxIngest } = await import('../ingest.js');
context.setExitCode(await options.runIngestWithProgress(args, context.io, context.deps, runKtxIngest));
}
export function registerIngestCommands(
program: Command,
context: KloCliCommandContext,
context: KtxCliCommandContext,
commandOptions: IngestCommandOptions,
): void {
const ingest = program
@ -66,7 +66,7 @@ export function registerIngestCommands(
ingest
.command('run')
.description('Run local ingest for one configured connection and source adapter')
.requiredOption('--connection-id <connectionId>', 'KLO connection id')
.requiredOption('--connection-id <connectionId>', 'KTX connection id')
.requiredOption('--adapter <adapter>', 'Ingest source adapter name')
.option('--source-dir <path>', 'Directory containing source files')
.option('--database-introspection-url <url>', 'Daemon URL for live-database introspection')

View file

@ -1,24 +1,24 @@
import { type Command, Option } from '@commander-js/extra-typings';
import { collectOption, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { collectOption, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { wikiWriteCommandSchema } from '../command-schemas.js';
import type { KloKnowledgeArgs } from '../knowledge.js';
import type { KtxKnowledgeArgs } from '../knowledge.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/knowledge-commands');
async function runKnowledgeArgs(context: KloCliCommandContext, args: KloKnowledgeArgs): Promise<void> {
const runner = context.deps.knowledge ?? (await import('../knowledge.js')).runKloKnowledge;
async function runKnowledgeArgs(context: KtxCliCommandContext, args: KtxKnowledgeArgs): Promise<void> {
const runner = context.deps.knowledge ?? (await import('../knowledge.js')).runKtxKnowledge;
context.setExitCode(await runner(args, context.io));
}
export function registerWikiCommands(program: Command, context: KloCliCommandContext): void {
export function registerWikiCommands(program: Command, context: KtxCliCommandContext): void {
const wiki = program
.command('wiki')
.description('List, read, search, or write local wiki pages')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
wiki

View file

@ -1,7 +1,7 @@
import { InvalidArgumentError, type Command } from '@commander-js/extra-typings';
import { type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { publicIngestReadCommandSchema, publicIngestRunCommandSchema } from '../command-schemas.js';
import type { KloPublicIngestArgs, KloPublicIngestInputMode } from '../public-ingest.js';
import type { KtxPublicIngestArgs, KtxPublicIngestInputMode } from '../public-ingest.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/public-ingest-commands');
@ -12,26 +12,26 @@ interface PublicIngestOptions {
input?: boolean;
}
function inputMode(options: { input?: boolean }): KloPublicIngestInputMode {
function inputMode(options: { input?: boolean }): KtxPublicIngestInputMode {
return options.input === false ? 'disabled' : 'auto';
}
async function runPublicIngestArgs(context: KloCliCommandContext, args: KloPublicIngestArgs): Promise<void> {
const runner = context.deps.publicIngest ?? (await import('../public-ingest.js')).runKloPublicIngest;
async function runPublicIngestArgs(context: KtxCliCommandContext, args: KtxPublicIngestArgs): Promise<void> {
const runner = context.deps.publicIngest ?? (await import('../public-ingest.js')).runKtxPublicIngest;
context.setExitCode(await runner(args, context.io));
}
function parsePublicIngestConnectionId(value: string): string {
if (value === 'run') {
throw new InvalidArgumentError('run is reserved; use klo dev ingest run for low-level adapter syntax');
throw new InvalidArgumentError('run is reserved; use ktx dev ingest run for low-level adapter syntax');
}
return value;
}
export function registerPublicIngestCommands(program: Command, context: KloCliCommandContext): void {
export function registerPublicIngestCommands(program: Command, context: KtxCliCommandContext): void {
const ingest = program
.command('ingest')
.description('Build and refresh KLO context from configured sources')
.description('Build and refresh KTX context from configured sources')
.usage('[options] [connectionId]')
.argument('[connectionId]', 'Connection id to ingest', parsePublicIngestConnectionId)
.option('--all', 'Ingest every eligible configured source', false)
@ -42,12 +42,12 @@ export function registerPublicIngestCommands(program: Command, context: KloCliCo
[
'',
'Examples:',
' klo ingest <connectionId> [options]',
' klo ingest --all [options]',
' klo ingest status [runId] [options]',
' klo ingest watch [runId] [options]',
' ktx ingest <connectionId> [options]',
' ktx ingest --all [options]',
' ktx ingest status [runId] [options]',
' ktx ingest watch [runId] [options]',
'',
'Project directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.',
'Project directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.',
'',
].join('\n'),
)
@ -58,7 +58,7 @@ export function registerPublicIngestCommands(program: Command, context: KloCliCo
.action(async (connectionId: string | undefined, _options: PublicIngestOptions, command) => {
const options = command.opts();
if (options.all === true && connectionId) {
throw new Error('klo ingest accepts either --all or <connectionId>, not both');
throw new Error('ktx ingest accepts either --all or <connectionId>, not both');
}
const args = publicIngestRunCommandSchema.parse({
command: 'run',

View file

@ -1,35 +1,35 @@
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
import { type KloCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
import type { KloScanArgs } from '../scan.js';
import { type KtxCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxScanArgs } from '../scan.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/scan-commands');
async function runScanArgs(context: KloCliCommandContext, args: KloScanArgs): Promise<void> {
const runner = context.deps.scan ?? (await import('../scan.js')).runKloScan;
async function runScanArgs(context: KtxCliCommandContext, args: KtxScanArgs): Promise<void> {
const runner = context.deps.scan ?? (await import('../scan.js')).runKtxScan;
context.setExitCode(await runner(args, context.io));
}
type KloScanModeOption = Extract<KloScanArgs, { command: 'run' }>['mode'];
type KtxScanModeOption = Extract<KtxScanArgs, { command: 'run' }>['mode'];
function parseScanModeOption(value: string): KloScanModeOption {
function parseScanModeOption(value: string): KtxScanModeOption {
if (value === 'structural' || value === 'enriched' || value === 'relationships') {
return value;
}
throw new InvalidArgumentError('Allowed choices are structural, enriched, relationships');
}
type KloRelationshipStatusOption = Extract<KloScanArgs, { command: 'relationships' }>['status'];
type KloRelationshipFeedbackDecisionOption = Extract<KloScanArgs, { command: 'relationshipFeedback' }>['decision'];
type KtxRelationshipStatusOption = Extract<KtxScanArgs, { command: 'relationships' }>['status'];
type KtxRelationshipFeedbackDecisionOption = Extract<KtxScanArgs, { command: 'relationshipFeedback' }>['decision'];
function parseRelationshipStatusOption(value: string): KloRelationshipStatusOption {
function parseRelationshipStatusOption(value: string): KtxRelationshipStatusOption {
if (value === 'accepted' || value === 'review' || value === 'rejected' || value === 'skipped' || value === 'all') {
return value;
}
throw new InvalidArgumentError('Allowed choices are accepted, review, rejected, skipped, all');
}
function parseRelationshipFeedbackDecisionOption(value: string): KloRelationshipFeedbackDecisionOption {
function parseRelationshipFeedbackDecisionOption(value: string): KtxRelationshipFeedbackDecisionOption {
if (value === 'accepted' || value === 'rejected' || value === 'all') {
return value;
}
@ -58,7 +58,7 @@ function relationshipDecisionArgs(options: {
note?: string;
json?: boolean;
}): Pick<
Extract<KloScanArgs, { command: 'relationshipDecision' }>,
Extract<KtxScanArgs, { command: 'relationshipDecision' }>,
'candidateId' | 'decision' | 'reviewer' | 'note' | 'json'
> | null {
const decisionCount = [options.accept !== undefined, options.reject !== undefined].filter(Boolean).length;
@ -69,7 +69,7 @@ function relationshipDecisionArgs(options: {
return {
candidateId: options.accept,
decision: 'accepted',
reviewer: options.reviewer ?? 'klo',
reviewer: options.reviewer ?? 'ktx',
note: options.note ?? null,
json: options.json === true,
};
@ -78,7 +78,7 @@ function relationshipDecisionArgs(options: {
return {
candidateId: options.reject,
decision: 'rejected',
reviewer: options.reviewer ?? 'klo',
reviewer: options.reviewer ?? 'ktx',
note: options.note ?? null,
json: options.json === true,
};
@ -90,11 +90,11 @@ function collectRelationshipCandidateOption(value: string, previous: string[]):
return [...previous, parseNonEmptyOption(value)];
}
export function registerScanCommands(program: Command, context: KloCliCommandContext): void {
export function registerScanCommands(program: Command, context: KtxCliCommandContext): void {
const scan = program
.command('scan')
.description('Run or inspect standalone connection scans')
.argument('[connectionId]', 'KLO connection id to scan')
.argument('[connectionId]', 'KTX connection id to scan')
.option(
'--mode <mode>',
'Scan mode: structural, enriched, relationships (default: structural)',
@ -105,7 +105,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
)
.hook('preAction', (_thisCommand, actionCommand) => {
context.writeDebug?.('scan', actionCommand);
@ -113,7 +113,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.action(async (connectionId: string | undefined, options, command) => {
if (!connectionId) {
scan.outputHelp();
context.io.stderr.write('klo dev scan requires <connectionId> or a subcommand\n');
context.io.stderr.write('ktx dev scan requires <connectionId> or a subcommand\n');
context.setExitCode(1);
return;
}
@ -135,7 +135,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.argument('<runId>', 'Local scan run id')
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (runId: string, _options: unknown, command) => {
await runScanArgs(context, {
@ -152,7 +152,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print the raw scan report JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (runId: string, options, command) => {
await runScanArgs(context, {
@ -189,7 +189,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print relationship artifacts as JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (runId: string, options, command) => {
const decision = relationshipDecisionArgs(options);
@ -231,7 +231,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print the apply result as JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (runId: string, options, command) => {
const parentOptions = command.parent?.opts() as { dryRun?: boolean } | undefined;
@ -249,7 +249,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
scan
.command('relationship-feedback')
.description('Export persisted relationship review decisions as calibration labels')
.option('--connection <connectionId>', 'Only export labels for one KLO connection')
.option('--connection <connectionId>', 'Only export labels for one KTX connection')
.option(
'--decision <decision>',
'Relationship feedback decision: accepted, rejected, all',
@ -260,7 +260,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.addOption(new Option('--jsonl', 'Print labels as newline-delimited JSON').default(false).conflicts('json'))
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (options, command) => {
await runScanArgs(context, {
@ -276,7 +276,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
scan
.command('relationship-calibration')
.description('Summarize relationship feedback labels against current score thresholds')
.option('--connection <connectionId>', 'Only calibrate labels for one KLO connection')
.option('--connection <connectionId>', 'Only calibrate labels for one KTX connection')
.option(
'--decision <decision>',
'Relationship feedback decision: accepted, rejected, all',
@ -298,7 +298,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print the calibration report as JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (options, command) => {
await runScanArgs(context, {
@ -315,7 +315,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
scan
.command('relationship-thresholds')
.description('Evaluate relationship feedback labels for offline threshold advice')
.option('--connection <connectionId>', 'Only evaluate labels for one KLO connection')
.option('--connection <connectionId>', 'Only evaluate labels for one KTX connection')
.option(
'--min-total-labels <count>',
'Minimum scored labels before advice can be ready',
@ -337,7 +337,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print the threshold advice report as JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (options, command) => {
await runScanArgs(context, {

View file

@ -1,6 +1,6 @@
import { type Command, InvalidArgumentError } from '@commander-js/extra-typings';
import { type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KloServeArgs } from '../serve.js';
import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxServeArgs } from '../serve.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/serve-commands');
@ -12,10 +12,10 @@ function parseMcp(value: string): 'stdio' {
throw new InvalidArgumentError('Only stdio is supported in this phase');
}
export function registerServeCommands(program: Command, context: KloCliCommandContext): void {
export function registerServeCommands(program: Command, context: KtxCliCommandContext): void {
program
.command('serve')
.description('Run standalone KLO services such as MCP stdio')
.description('Run standalone KTX services such as MCP stdio')
.requiredOption('--mcp <mode>', 'MCP transport mode', parseMcp)
.option('--user-id <id>', 'Local user id', 'local')
.option('--semantic-compute', 'Enable semantic-layer compute', false)
@ -30,7 +30,7 @@ export function registerServeCommands(program: Command, context: KloCliCommandCo
if (options.executeQueries === true && !semanticCompute) {
throw new Error('--execute-queries requires --semantic-compute');
}
const args: KloServeArgs = {
const args: KtxServeArgs = {
mcp: options.mcp,
projectDir: resolveCommandProjectDir(command),
userId: options.userId,
@ -41,7 +41,7 @@ export function registerServeCommands(program: Command, context: KloCliCommandCo
memoryCapture: options.memoryCapture === true,
memoryModel: options.memoryModel,
};
const runner = context.deps.serveStdio ?? (await import('../serve.js')).runKloServeStdio;
const runner = context.deps.serveStdio ?? (await import('../serve.js')).runKtxServeStdio;
context.setExitCode(await runner(args));
});
}

View file

@ -1,15 +1,15 @@
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
import type { KloCliCommandContext } from '../cli-program.js';
import type { KtxCliCommandContext } from '../cli-program.js';
import { resolveCommandProjectDir } from '../cli-program.js';
import type { KloSetupDatabaseDriver } from '../setup-databases.js';
import type { KloSetupSourceType } from '../setup-sources.js';
import type { KtxSetupDatabaseDriver } from '../setup-databases.js';
import type { KtxSetupSourceType } from '../setup-sources.js';
import { registerDemoCommands } from './demo-commands.js';
async function runSetupArgs(
context: KloCliCommandContext,
context: KtxCliCommandContext,
args: Parameters<NonNullable<typeof context.deps.setup>>[0],
) {
const runner = context.deps.setup ?? (await import('../setup.js')).runKloSetup;
const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
context.setExitCode(await runner(args, context.io));
}
@ -28,7 +28,7 @@ function embeddingBackend(value: string): 'openai' | 'sentence-transformers' {
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function databaseDriver(value: string): KloSetupDatabaseDriver {
function databaseDriver(value: string): KtxSetupDatabaseDriver {
if (
value === 'sqlite' ||
value === 'postgres' ||
@ -43,7 +43,7 @@ function databaseDriver(value: string): KloSetupDatabaseDriver {
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function sourceType(value: string): KloSetupSourceType {
function sourceType(value: string): KtxSetupSourceType {
if (
value === 'dbt' ||
value === 'metricflow' ||
@ -109,7 +109,7 @@ function shouldShowSetupEntryMenu(
embeddingApiKeyEnv?: string;
embeddingApiKeyFile?: string;
skipEmbeddings?: boolean;
database?: KloSetupDatabaseDriver[];
database?: KtxSetupDatabaseDriver[];
databaseConnectionId?: string[];
newDatabaseConnectionId?: string;
databaseUrl?: string;
@ -121,7 +121,7 @@ function shouldShowSetupEntryMenu(
historicSqlServiceAccountPattern?: string[];
historicSqlRedactionPattern?: string[];
skipDatabases?: boolean;
source?: KloSetupSourceType;
source?: KtxSetupSourceType;
sourceConnectionId?: string;
sourcePath?: string;
sourceGitUrl?: string;
@ -210,13 +210,13 @@ function shouldShowSetupEntryMenu(
].some((optionName) => optionWasSpecified(command, optionName));
}
export function registerSetupCommands(program: Command, context: KloCliCommandContext): void {
export function registerSetupCommands(program: Command, context: KtxCliCommandContext): void {
const setup = program
.command('setup')
.description('Set up or resume a local KLO project')
.option('--project-dir <path>', 'KLO project directory')
.option('--new', 'Create a new KLO project before setup', false)
.option('--existing', 'Use an existing KLO project', false)
.description('Set up or resume a local KTX project')
.option('--project-dir <path>', 'KTX project directory')
.option('--new', 'Create a new KTX project before setup', false)
.option('--existing', 'Use an existing KTX project', false)
.option('--agents', 'Install agent integration only', false)
.addOption(
new Option('--target <target>', 'Agent target').choices([
@ -247,10 +247,10 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
.option(
'--database <driver>',
'Database driver to configure; repeatable',
(value, previous: KloSetupDatabaseDriver[]) => {
(value, previous: KtxSetupDatabaseDriver[]) => {
return [...previous, databaseDriver(value)];
},
[] as KloSetupDatabaseDriver[],
[] as KtxSetupDatabaseDriver[],
)
.option(
'--database-connection-id <id>',
@ -291,7 +291,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
(value, previous: string[]) => [...previous, value],
[],
)
.option('--skip-databases', 'Leave database setup incomplete; KLO cannot work until a primary source is added', false)
.option('--skip-databases', 'Leave database setup incomplete; KTX cannot work until a primary source is added', false)
.addOption(new Option('--source <type>', 'Source connector type').argParser(sourceType))
.option('--source-connection-id <id>', 'Connection id for source setup')
.option('--source-path <path>', 'Local source path for dbt, MetricFlow, or LookML')
@ -421,9 +421,9 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
});
});
registerDemoCommands(setup, context, { description: 'Run the packaged KLO demo from setup' });
registerDemoCommands(setup, context, { description: 'Run the packaged KTX demo from setup' });
const setupContext = setup.command('context').description('Build, inspect, and recover setup-managed KLO context');
const setupContext = setup.command('context').description('Build, inspect, and recover setup-managed KTX context');
function setupContextInputMode(command: {
optsWithGlobals?: () => unknown;
@ -435,7 +435,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
setupContext
.command('build')
.description('Build agent-ready KLO context for setup')
.description('Build agent-ready KTX context for setup')
.option('--no-input', 'Disable interactive terminal input')
.action(async (options: { input?: boolean }, command) => {
await runSetupArgs(context, {
@ -505,7 +505,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
setup
.command('status')
.description('Show setup readiness for the resolved KLO project')
.description('Show setup readiness for the resolved KTX project')
.option('--json', 'Print JSON output', false)
.action(async (options: { json?: boolean }, command) => {
await runSetupArgs(context, {

View file

@ -1,12 +1,12 @@
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
import {
collectOption,
type KloCliCommandContext,
type KtxCliCommandContext,
parsePositiveIntegerOption,
resolveCommandProjectDir,
} from '../cli-program.js';
import { slQueryCommandSchema } from '../command-schemas.js';
import type { KloSlArgs } from '../sl.js';
import type { KtxSlArgs } from '../sl.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/sl-commands');
@ -32,24 +32,24 @@ function collectOrderBy(
return [...previous, parseOrderBy(value)];
}
async function runSlArgs(context: KloCliCommandContext, args: KloSlArgs): Promise<void> {
const runner = context.deps.sl ?? (await import('../sl.js')).runKloSl;
async function runSlArgs(context: KtxCliCommandContext, args: KtxSlArgs): Promise<void> {
const runner = context.deps.sl ?? (await import('../sl.js')).runKtxSl;
context.setExitCode(await runner(args, context.io));
}
export function registerSlCommands(program: Command, context: KloCliCommandContext, commandName = 'sl'): void {
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')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
sl.command('list')
.description('List semantic-layer sources')
.option('--connection-id <id>', 'KLO connection id')
.option('--connection-id <id>', 'KTX connection id')
.addOption(
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
'pretty',
@ -71,7 +71,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
sl.command('read')
.description('Read a semantic-layer source')
.argument('<sourceName>', 'Semantic-layer source name')
.requiredOption('--connection-id <id>', 'KLO connection id')
.requiredOption('--connection-id <id>', 'KTX connection id')
.action(async (sourceName: string, options: { connectionId: string }, command) => {
await runSlArgs(context, {
command: 'read',
@ -84,7 +84,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
sl.command('validate')
.description('Validate a semantic-layer source')
.argument('<sourceName>', 'Semantic-layer source name')
.requiredOption('--connection-id <id>', 'KLO connection id')
.requiredOption('--connection-id <id>', 'KTX connection id')
.action(async (sourceName: string, options: { connectionId: string }, command) => {
await runSlArgs(context, {
command: 'validate',
@ -97,7 +97,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
sl.command('write')
.description('Write a semantic-layer source')
.argument('<sourceName>', 'Semantic-layer source name')
.requiredOption('--connection-id <id>', 'KLO connection id')
.requiredOption('--connection-id <id>', 'KTX connection id')
.requiredOption('--yaml <yaml>', 'Semantic-layer source YAML')
.action(async (sourceName: string, options: { connectionId: string; yaml: string }, command) => {
await runSlArgs(context, {
@ -111,7 +111,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
sl.command('query')
.description('Compile or execute a semantic-layer query')
.option('--connection-id <id>', 'KLO connection id')
.option('--connection-id <id>', 'KTX connection id')
.option('--measure <measure>', 'Measure to query; repeatable', collectOption, [])
.option('--dimension <dimension>', 'Dimension to include; repeatable', collectOption, [])
.option('--filter <filter>', 'Filter expression; repeatable', collectOption, [])

View file

@ -1,14 +1,14 @@
import type { Command } from '@commander-js/extra-typings';
import type { KloCliCommandContext } from '../cli-program.js';
import type { KtxCliCommandContext } from '../cli-program.js';
import { resolveCommandProjectDir } from '../cli-program.js';
export function registerStatusCommands(program: Command, context: KloCliCommandContext): void {
export function registerStatusCommands(program: Command, context: KtxCliCommandContext): void {
program
.command('status')
.description('Show current KLO project setup status')
.description('Show current KTX project setup status')
.option('--json', 'Print JSON output', false)
.action(async (options: { json?: boolean }, command) => {
const runner = context.deps.setup ?? (await import('../setup.js')).runKloSetup;
const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
context.setExitCode(
await runner(
{

View file

@ -28,10 +28,10 @@ export interface ZshCompletionInstallResult {
zshrcPath: string;
}
const KLO_COMPLETION_BLOCK_START = '# >>> klo completion >>>';
const KLO_COMPLETION_BLOCK_END = '# <<< klo completion <<<';
const KLO_COMPLETION_BLOCK_PATTERN = new RegExp(
`\\n?${escapeRegExp(KLO_COMPLETION_BLOCK_START)}[\\s\\S]*?${escapeRegExp(KLO_COMPLETION_BLOCK_END)}\\n?`,
const KTX_COMPLETION_BLOCK_START = '# >>> ktx completion >>>';
const KTX_COMPLETION_BLOCK_END = '# <<< ktx completion <<<';
const KTX_COMPLETION_BLOCK_PATTERN = new RegExp(
`\\n?${escapeRegExp(KTX_COMPLETION_BLOCK_START)}[\\s\\S]*?${escapeRegExp(KTX_COMPLETION_BLOCK_END)}\\n?`,
'g',
);
@ -39,27 +39,27 @@ export function zshCompletionScript(): string {
const zshWords = '$' + '{words[@]}';
const zshCompletionCapture = [
'$',
`{(@f)$("${'$'}{klo_completion_command[@]}" dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}" 2>/dev/null)}`,
`{(@f)$("${'$'}{ktx_completion_command[@]}" dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}" 2>/dev/null)}`,
].join('');
const zshCompletionsCount = '$' + '{#completions[@]}';
const zshCompletionCommand = '$' + '(eval "print -r -- $' + '{KLO_COMPLETION_COMMAND:-klo}")';
const zshCompletionCommand = '$' + '(eval "print -r -- $' + '{KTX_COMPLETION_COMMAND:-ktx}")';
return [
'#compdef klo',
'#compdef ktx',
'',
'_klo() {',
'_ktx() {',
' local -a completions',
' local -a klo_completion_command',
` klo_completion_command=("\${(@z)${zshCompletionCommand}}")`,
' local -a ktx_completion_command',
` ktx_completion_command=("\${(@z)${zshCompletionCommand}}")`,
` completions=("${zshCompletionCapture}")`,
` if (( ${zshCompletionsCount} )); then`,
" _describe 'klo completions' completions",
" _describe 'ktx completions' completions",
' else',
' _files',
' fi',
'}',
'',
'compdef _klo klo',
'compdef _ktx ktx',
'',
].join('\n');
}
@ -68,7 +68,7 @@ export async function installZshCompletion(): Promise<ZshCompletionInstallResult
const homeDir = process.env.HOME || homedir();
const zshConfigDir = process.env.ZDOTDIR || homeDir;
const completionDir = join(homeDir, '.zfunc');
const completionPath = join(completionDir, '_klo');
const completionPath = join(completionDir, '_ktx');
const zshrcPath = join(zshConfigDir, '.zshrc');
await mkdir(completionDir, { recursive: true });
@ -290,7 +290,7 @@ async function readOptionalTextFile(path: string): Promise<string> {
}
function updateZshrcCompletionBlock(contents: string): string {
const withoutManagedBlock = contents.replace(KLO_COMPLETION_BLOCK_PATTERN, normalizeTrailingNewline);
const withoutManagedBlock = contents.replace(KTX_COMPLETION_BLOCK_PATTERN, normalizeTrailingNewline);
const hasCompinit = /^.*\bcompinit\b.*$/m.test(withoutManagedBlock);
const block = zshrcCompletionBlock({ includeCompinit: !hasCompinit });
@ -313,23 +313,23 @@ function updateZshrcCompletionBlock(contents: string): string {
function zshrcCompletionBlock(options: { includeCompinit: boolean }): string {
return [
KLO_COMPLETION_BLOCK_START,
'_klo_completion_command() {',
KTX_COMPLETION_BLOCK_START,
'_ktx_completion_command() {',
' local dir="$PWD"',
' while [[ "$dir" != "/" ]]; do',
` if [[ -f "$dir/package.json" ]] && command grep -q '"name": "klo-workspace"' "$dir/package.json" 2>/dev/null; then`,
' print -r -- "node $dir/scripts/run-klo.mjs --"',
` if [[ -f "$dir/package.json" ]] && command grep -q '"name": "ktx-workspace"' "$dir/package.json" 2>/dev/null; then`,
' print -r -- "node $dir/scripts/run-ktx.mjs --"',
' return',
' fi',
' dir="' + '$' + '{dir:h}"',
' done',
' print -r -- "klo"',
' print -r -- "ktx"',
'}',
"export KLO_COMPLETION_COMMAND='$(_klo_completion_command)'",
"export KTX_COMPLETION_COMMAND='$(_ktx_completion_command)'",
'setopt complete_aliases',
'fpath=("$HOME/.zfunc" $fpath)',
...(options.includeCompinit ? ['autoload -Uz compinit', 'compinit'] : []),
KLO_COMPLETION_BLOCK_END,
KTX_COMPLETION_BLOCK_END,
].join('\n');
}

View file

@ -1,11 +1,11 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initKloProject, parseKloProjectConfig } from '@klo/context/project';
import type { KloConnectionDriver, KloScanConnector, KloSchemaSnapshot } from '@klo/context/scan';
import { initKtxProject, parseKtxProjectConfig } from '@ktx/context/project';
import type { KtxConnectionDriver, KtxScanConnector, KtxSchemaSnapshot } from '@ktx/context/scan';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloConnection } from './connection.js';
import { runKloCli, type KloCliIo } from './index.js';
import { runKtxConnection } from './connection.js';
import { runKtxCli, type KtxCliIo } from './index.js';
function makeIo(options: { stdoutIsTty?: boolean; stdinIsTty?: boolean } = {}) {
let stdout = '';
@ -32,7 +32,7 @@ function makeIo(options: { stdoutIsTty?: boolean; stdinIsTty?: boolean } = {}) {
};
}
function snapshotFor(driver: KloConnectionDriver, tableNames: string[]): KloSchemaSnapshot {
function snapshotFor(driver: KtxConnectionDriver, tableNames: string[]): KtxSchemaSnapshot {
return {
connectionId: 'warehouse',
driver,
@ -52,10 +52,10 @@ function snapshotFor(driver: KloConnectionDriver, tableNames: string[]): KloSche
};
}
function nativeConnector(driver: KloConnectionDriver, tableNames: string[]) {
function nativeConnector(driver: KtxConnectionDriver, tableNames: string[]) {
const introspect = vi.fn(async () => snapshotFor(driver, tableNames));
const cleanup = vi.fn(async () => undefined);
const connector: KloScanConnector = {
const connector: KtxScanConnector = {
id: `${driver}:warehouse`,
driver,
capabilities: {
@ -75,11 +75,11 @@ function nativeConnector(driver: KloConnectionDriver, tableNames: string[]) {
return { connector, introspect, cleanup };
}
describe('runKloConnection', () => {
describe('runKtxConnection', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-connection-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-connection-'));
});
afterEach(async () => {
@ -88,11 +88,11 @@ describe('runKloConnection', () => {
it('adds and lists env-referenced connections without resolving secrets', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'add',
projectDir,
@ -109,18 +109,18 @@ describe('runKloConnection', () => {
).resolves.toBe(0);
expect(io.stdout()).toContain('Connection: warehouse');
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain('url: env:DATABASE_URL');
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain('url: env:DATABASE_URL');
const listIo = makeIo();
await expect(runKloConnection({ command: 'list', projectDir }, listIo.io)).resolves.toBe(0);
await expect(runKtxConnection({ command: 'list', projectDir }, listIo.io)).resolves.toBe(0);
expect(listIo.stdout()).toContain('warehouse');
expect(listIo.stdout()).toContain('postgres');
});
it('removes a configured connection from klo.yaml without deleting local artifacts when forced', async () => {
it('removes a configured connection from ktx.yaml without deleting local artifacts when forced', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -134,14 +134,14 @@ describe('runKloConnection', () => {
},
makeIo().io,
);
const artifactPath = join(projectDir, '.klo', 'artifacts', 'warehouse.txt');
await mkdir(join(projectDir, '.klo', 'artifacts'), { recursive: true });
const artifactPath = join(projectDir, '.ktx', 'artifacts', 'warehouse.txt');
await mkdir(join(projectDir, '.ktx', 'artifacts'), { recursive: true });
await writeFile(artifactPath, 'keep me', 'utf-8');
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'remove',
projectDir,
@ -153,20 +153,20 @@ describe('runKloConnection', () => {
),
).resolves.toBe(0);
const parsed = parseKloProjectConfig(await readFile(join(projectDir, 'klo.yaml'), 'utf-8'));
const parsed = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
expect(parsed.connections.warehouse).toBeUndefined();
await expect(readFile(artifactPath, 'utf-8')).resolves.toBe('keep me');
expect(io.stdout()).toContain('Connection removed from klo.yaml.');
expect(io.stdout()).toContain('Connection removed from ktx.yaml.');
expect(io.stdout()).toContain(
'Ingested artifacts from this connection remain in .klo/. Run klo dev artifacts to inspect.',
'Ingested artifacts from this connection remain in .ktx/. Run ktx dev artifacts to inspect.',
);
expect(io.stderr()).toBe('');
});
it('requires --force when removing in non-interactive mode', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -183,7 +183,7 @@ describe('runKloConnection', () => {
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'remove',
projectDir,
@ -200,11 +200,11 @@ describe('runKloConnection', () => {
it('returns a clear error when removing an unknown connection', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'remove',
projectDir,
@ -216,13 +216,13 @@ describe('runKloConnection', () => {
),
).resolves.toBe(1);
expect(io.stderr()).toContain('Connection "missing" is not configured in klo.yaml');
expect(io.stderr()).toContain('Connection "missing" is not configured in ktx.yaml');
});
it('asks for confirmation before removing in an interactive terminal', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -243,7 +243,7 @@ describe('runKloConnection', () => {
};
await expect(
runKloConnection(
runKtxConnection(
{
command: 'remove',
projectDir,
@ -256,14 +256,14 @@ describe('runKloConnection', () => {
).resolves.toBe(0);
expect(prompts.confirm).toHaveBeenCalledWith({
message: 'Remove connection "warehouse" from klo.yaml? Ingested artifacts will remain in .klo/.',
message: 'Remove connection "warehouse" from ktx.yaml? Ingested artifacts will remain in .ktx/.',
initialValue: false,
});
});
it('runs public connect map as refresh, validate, and list over the low-level mapping runner', async () => {
const io = makeIo();
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
if (argv[0] === 'refresh') {
mappingIo.stdout.write('Discovery: 1 database\n');
mappingIo.stdout.write('Unmapped discovered: 1\n');
@ -282,7 +282,7 @@ describe('runKloConnection', () => {
});
await expect(
runKloConnection(
runKtxConnection(
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-metabase', json: false },
io.io,
{ runMapping },
@ -309,14 +309,14 @@ describe('runKloConnection', () => {
expect(io.stdout()).toContain('Mappings:');
expect(io.stdout()).toContain('1 -> [unmapped]');
expect(io.stdout()).toContain('Next:');
expect(io.stdout()).toContain('klo ingest prod-metabase');
expect(io.stdout()).toContain('klo dev mapping');
expect(io.stdout()).toContain('ktx ingest prod-metabase');
expect(io.stdout()).toContain('ktx dev mapping');
expect(io.stderr()).toBe('');
});
it('prints stable JSON for public connect map without leaking low-level stdout', async () => {
const io = makeIo();
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
if (argv[0] === 'refresh') {
mappingIo.stdout.write('Discovery: 1 connection\nUnmapped discovered: 0\nStale mappings: 0\n');
return 0;
@ -332,8 +332,8 @@ describe('runKloConnection', () => {
[
{
lookerConnectionName: 'analytics',
kloConnectionId: 'prod-warehouse',
source: 'klo.yaml',
ktxConnectionId: 'prod-warehouse',
source: 'ktx.yaml',
},
],
null,
@ -346,7 +346,7 @@ describe('runKloConnection', () => {
});
await expect(
runKloConnection(
runKtxConnection(
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-looker', json: true },
io.io,
{ runMapping },
@ -357,7 +357,7 @@ describe('runKloConnection', () => {
connectionId: string;
refresh: { ok: boolean; output: string[] };
validation: { ok: boolean; output: string[] };
mappings: Array<{ lookerConnectionName: string; kloConnectionId: string; source: string }>;
mappings: Array<{ lookerConnectionName: string; ktxConnectionId: string; source: string }>;
};
expect(parsed).toEqual({
connectionId: 'prod-looker',
@ -372,8 +372,8 @@ describe('runKloConnection', () => {
mappings: [
{
lookerConnectionName: 'analytics',
kloConnectionId: 'prod-warehouse',
source: 'klo.yaml',
ktxConnectionId: 'prod-warehouse',
source: 'ktx.yaml',
},
],
});
@ -382,7 +382,7 @@ describe('runKloConnection', () => {
it('returns the refresh failure when public connect map cannot discover source metadata', async () => {
const io = makeIo();
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
if (argv[0] === 'refresh') {
mappingIo.stderr.write('Metabase API key is not configured\n');
return 1;
@ -391,7 +391,7 @@ describe('runKloConnection', () => {
});
await expect(
runKloConnection(
runKtxConnection(
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-metabase', json: false },
io.io,
{ runMapping },
@ -405,11 +405,11 @@ describe('runKloConnection', () => {
it('rejects literal credential URLs unless explicitly allowed', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'add',
projectDir,
@ -430,12 +430,12 @@ describe('runKloConnection', () => {
it('warns before writing explicitly allowed literal credential URLs without echoing the URL', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
const literalUrl = 'postgres://localhost:5432/warehouse';
await expect(
runKloConnection(
runKtxConnection(
{
command: 'add',
projectDir,
@ -452,19 +452,19 @@ describe('runKloConnection', () => {
).resolves.toBe(0);
expect(io.stderr()).toContain(
'Warning: writing a literal credential URL to klo.yaml for connection "warehouse". Prefer env:NAME or file:/path references.',
'Warning: writing a literal credential URL to ktx.yaml for connection "warehouse". Prefer env:NAME or file:/path references.',
);
expect(io.stderr()).not.toContain(literalUrl);
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain(literalUrl);
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain(literalUrl);
});
it('adds a Notion connection without writing token values', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'add',
projectDir,
@ -490,7 +490,7 @@ describe('runKloConnection', () => {
),
).resolves.toBe(0);
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(yaml).toContain('driver: notion');
expect(yaml).toContain('auth_token_ref: env:NOTION_AUTH_TOKEN');
expect(yaml).toContain('crawl_mode: all_accessible');
@ -502,8 +502,8 @@ describe('runKloConnection', () => {
it('runs connection notion pick --no-input through the public connection entrypoint', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -530,7 +530,7 @@ describe('runKloConnection', () => {
const io = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'connection',
'notion',
@ -546,7 +546,7 @@ describe('runKloConnection', () => {
),
).resolves.toBe(0);
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(yaml).toContain('crawl_mode: selected_roots');
expect(yaml).toContain('11111111-2222-3333-4444-555555555555');
expect(yaml).toContain('database-1');
@ -556,8 +556,8 @@ describe('runKloConnection', () => {
it('tests a configured connection through the native scan connector', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -576,7 +576,7 @@ describe('runKloConnection', () => {
const io = makeIo();
await expect(
runKloConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
runKtxConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
createScanConnector,
}),
).resolves.toBe(0);
@ -600,8 +600,8 @@ describe('runKloConnection', () => {
it('cleans up the native scan connector when connection testing fails', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -616,7 +616,7 @@ describe('runKloConnection', () => {
makeIo().io,
);
const cleanup = vi.fn(async () => undefined);
const connector: KloScanConnector = {
const connector: KtxScanConnector = {
id: 'sqlite:warehouse',
driver: 'sqlite',
capabilities: {
@ -638,7 +638,7 @@ describe('runKloConnection', () => {
const io = makeIo();
await expect(
runKloConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
runKtxConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
createScanConnector: vi.fn(async () => connector),
}),
).resolves.toBe(1);

View file

@ -1,14 +1,14 @@
import { cancel, confirm, isCancel } from '@clack/prompts';
import { type KloLocalProject, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
import type { KloScanConnector } from '@klo/context/scan';
import type { KloConnectionMappingArgs } from './commands/connection-mapping.js';
import type { KloCliIo } from './index.js';
import { createKloCliScanConnector } from './local-scan-connectors.js';
import { type KtxLocalProject, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
import type { KtxScanConnector } from '@ktx/context/scan';
import type { KtxConnectionMappingArgs } from './commands/connection-mapping.js';
import type { KtxCliIo } from './index.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js';
import { profileMark } from './startup-profile.js';
profileMark('module:connection');
interface KloNotionConnectionCliConfig {
interface KtxNotionConnectionCliConfig {
authTokenRef: string;
crawlMode: 'all_accessible' | 'selected_roots';
rootPageIds: string[];
@ -19,9 +19,9 @@ interface KloNotionConnectionCliConfig {
maxKnowledgeUpdatesPerRun?: number;
}
type KloConnectionInputMode = 'disabled';
type KtxConnectionInputMode = 'disabled';
export type KloConnectionArgs =
export type KtxConnectionArgs =
| { command: 'list'; projectDir: string }
| {
command: 'add';
@ -33,7 +33,7 @@ export type KloConnectionArgs =
readonly: boolean;
force: boolean;
allowLiteralCredentials: boolean;
notion?: KloNotionConnectionCliConfig;
notion?: KtxNotionConnectionCliConfig;
}
| { command: 'test'; projectDir: string; connectionId: string }
| {
@ -41,7 +41,7 @@ export type KloConnectionArgs =
projectDir: string;
connectionId: string;
force: boolean;
inputMode?: KloConnectionInputMode;
inputMode?: KtxConnectionInputMode;
}
| {
command: 'map';
@ -50,19 +50,19 @@ export type KloConnectionArgs =
json: boolean;
};
interface KloConnectionPromptAdapter {
interface KtxConnectionPromptAdapter {
confirm(options: { message: string; initialValue?: boolean }): Promise<boolean>;
cancel(message: string): void;
}
interface KloConnectionIo extends KloCliIo {
interface KtxConnectionIo extends KtxCliIo {
stdin?: { isTTY?: boolean };
}
interface KloConnectionDeps {
createScanConnector?: typeof createKloCliScanConnector;
runMapping?: (argv: string[], io: KloCliIo) => Promise<number>;
prompts?: KloConnectionPromptAdapter;
interface KtxConnectionDeps {
createScanConnector?: typeof createKtxCliScanConnector;
runMapping?: (argv: string[], io: KtxCliIo) => Promise<number>;
prompts?: KtxConnectionPromptAdapter;
}
function assertSafeConnectionId(connectionId: string): void {
@ -76,10 +76,10 @@ function isCredentialReference(value: string): boolean {
}
function literalCredentialWarning(connectionId: string): string {
return `Warning: writing a literal credential URL to klo.yaml for connection "${connectionId}". Prefer env:NAME or file:/path references.`;
return `Warning: writing a literal credential URL to ktx.yaml for connection "${connectionId}". Prefer env:NAME or file:/path references.`;
}
function createClackConnectionPromptAdapter(): KloConnectionPromptAdapter {
function createClackConnectionPromptAdapter(): KtxConnectionPromptAdapter {
return {
async confirm(options: { message: string; initialValue?: boolean }): Promise<boolean> {
const value = await confirm(options);
@ -92,24 +92,24 @@ function createClackConnectionPromptAdapter(): KloConnectionPromptAdapter {
}
function isInteractiveConnectionIo(
args: Extract<KloConnectionArgs, { command: 'remove' }>,
io: KloConnectionIo,
args: Extract<KtxConnectionArgs, { command: 'remove' }>,
io: KtxConnectionIo,
): boolean {
return args.inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
}
async function cleanupConnector(connector: KloScanConnector | null): Promise<void> {
async function cleanupConnector(connector: KtxScanConnector | null): Promise<void> {
if (connector?.cleanup) {
await connector.cleanup();
}
}
async function testNativeConnection(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
createScanConnector: typeof createKloCliScanConnector,
createScanConnector: typeof createKtxCliScanConnector,
): Promise<{ driver: string; tableCount: number }> {
let connector: KloScanConnector | null = null;
let connector: KtxScanConnector | null = null;
try {
connector = await createScanConnector(project, connectionId);
const snapshot = await connector.introspect(
@ -131,7 +131,7 @@ async function testNativeConnection(
}
}
interface BufferedIo extends KloCliIo {
interface BufferedIo extends KtxCliIo {
stdoutText(): string;
stderrText(): string;
}
@ -167,17 +167,17 @@ function splitOutputLines(output: string): string[] {
}
async function runLowLevelMapping(
args: KloConnectionMappingArgs,
args: KtxConnectionMappingArgs,
argv: string[],
io: KloCliIo,
deps: KloConnectionDeps,
io: KtxCliIo,
deps: KtxConnectionDeps,
): Promise<number> {
if (deps.runMapping) {
return await deps.runMapping(argv, io);
}
const { runKloConnectionMapping } = await import('./commands/connection-mapping.js');
return await runKloConnectionMapping(args, io);
const { runKtxConnectionMapping } = await import('./commands/connection-mapping.js');
return await runKtxConnectionMapping(args, io);
}
function parseMappingListJson(output: string): unknown[] {
@ -190,12 +190,12 @@ function parseMappingListJson(output: string): unknown[] {
}
async function runPublicConnectionMap(
args: Extract<KloConnectionArgs, { command: 'map' }>,
io: KloCliIo,
deps: KloConnectionDeps,
args: Extract<KtxConnectionArgs, { command: 'map' }>,
io: KtxCliIo,
deps: KtxConnectionDeps,
): Promise<number> {
const refreshIo = createBufferedIo();
const refreshArgs: KloConnectionMappingArgs = {
const refreshArgs: KtxConnectionMappingArgs = {
command: 'refresh',
projectDir: args.projectDir,
connectionId: args.sourceConnectionId,
@ -217,7 +217,7 @@ async function runPublicConnectionMap(
}
const validationIo = createBufferedIo();
const validationArgs: KloConnectionMappingArgs = {
const validationArgs: KtxConnectionMappingArgs = {
command: 'validate',
projectDir: args.projectDir,
connectionId: args.sourceConnectionId,
@ -237,7 +237,7 @@ async function runPublicConnectionMap(
const listIo = createBufferedIo();
const listArgv = ['list', args.sourceConnectionId, '--project-dir', args.projectDir];
const listArgs: KloConnectionMappingArgs = {
const listArgs: KtxConnectionMappingArgs = {
command: 'list',
projectDir: args.projectDir,
connectionId: args.sourceConnectionId,
@ -271,26 +271,26 @@ async function runPublicConnectionMap(
io.stdout.write('\nMappings:\n');
io.stdout.write(listIo.stdoutText().trim() ? listIo.stdoutText() : 'No mappings found.\n');
io.stdout.write('\nNext:\n');
io.stdout.write(` klo ingest ${args.sourceConnectionId}\n`);
io.stdout.write(` klo dev mapping list ${args.sourceConnectionId}\n`);
io.stdout.write(` ktx ingest ${args.sourceConnectionId}\n`);
io.stdout.write(` ktx dev mapping list ${args.sourceConnectionId}\n`);
return 0;
}
export async function runKloConnection(
args: KloConnectionArgs,
io: KloConnectionIo = process,
deps: KloConnectionDeps = {},
export async function runKtxConnection(
args: KtxConnectionArgs,
io: KtxConnectionIo = process,
deps: KtxConnectionDeps = {},
): Promise<number> {
try {
if (args.command === 'map') {
return await runPublicConnectionMap(args, io, deps);
}
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
if (args.command === 'list') {
const entries = Object.entries(project.config.connections).sort(([a], [b]) => a.localeCompare(b));
if (entries.length === 0) {
io.stdout.write('No connections configured. Run `klo connection add <id> --driver <driver>` to add one.\n');
io.stdout.write('No connections configured. Run `ktx connection add <id> --driver <driver>` to add one.\n');
return 0;
}
const idWidth = Math.max('ID'.length, ...entries.map(([id]) => id.length));
@ -348,11 +348,11 @@ export async function runKloConnection(
},
};
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(nextConfig),
'klo',
'klo@example.com',
`Update KLO connection: ${args.connectionId}`,
'ktx.yaml',
serializeKtxProjectConfig(nextConfig),
'ktx',
'ktx@example.com',
`Update KTX connection: ${args.connectionId}`,
);
io.stdout.write(`Connection: ${args.connectionId}\n`);
io.stdout.write(`Driver: ${args.driver}\n`);
@ -361,7 +361,7 @@ export async function runKloConnection(
if (args.command === 'remove') {
if (!project.config.connections[args.connectionId]) {
throw new Error(`Connection "${args.connectionId}" is not configured in klo.yaml`);
throw new Error(`Connection "${args.connectionId}" is not configured in ktx.yaml`);
}
if (!args.force) {
@ -373,7 +373,7 @@ export async function runKloConnection(
const prompts = deps.prompts ?? createClackConnectionPromptAdapter();
const confirmed = await prompts.confirm({
message: `Remove connection "${args.connectionId}" from klo.yaml? Ingested artifacts will remain in .klo/.`,
message: `Remove connection "${args.connectionId}" from ktx.yaml? Ingested artifacts will remain in .ktx/.`,
initialValue: false,
});
if (!confirmed) {
@ -388,21 +388,21 @@ export async function runKloConnection(
connections,
};
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(nextConfig),
'klo',
'klo@example.com',
`Remove KLO connection: ${args.connectionId}`,
'ktx.yaml',
serializeKtxProjectConfig(nextConfig),
'ktx',
'ktx@example.com',
`Remove KTX connection: ${args.connectionId}`,
);
io.stdout.write('Connection removed from klo.yaml.\n');
io.stdout.write('Ingested artifacts from this connection remain in .klo/. Run klo dev artifacts to inspect.\n');
io.stdout.write('Connection removed from ktx.yaml.\n');
io.stdout.write('Ingested artifacts from this connection remain in .ktx/. Run ktx dev artifacts to inspect.\n');
return 0;
}
const result = await testNativeConnection(
project,
args.connectionId,
deps.createScanConnector ?? createKloCliScanConnector,
deps.createScanConnector ?? createKtxCliScanConnector,
);
io.stdout.write(`Connection test passed: ${args.connectionId}\n`);
io.stdout.write(`Driver: ${result.driver}\n`);

View file

@ -1,6 +1,6 @@
import { buildDefaultKloProjectConfig, type KloProjectConfig } from '@klo/context/project';
import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '@ktx/context/project';
import { describe, expect, it, vi } from 'vitest';
import type { KloPublicIngestProject, KloPublicIngestTargetResult } from './public-ingest.js';
import type { KtxPublicIngestProject, KtxPublicIngestTargetResult } from './public-ingest.js';
import {
extractProgressMessage,
initViewState,
@ -32,17 +32,17 @@ function makeIo(options: { isTTY?: boolean } = {}) {
};
}
function projectWithConnections(connections: KloProjectConfig['connections']): KloPublicIngestProject {
function projectWithConnections(connections: KtxProjectConfig['connections']): KtxPublicIngestProject {
return {
projectDir: '/tmp/project',
config: {
...buildDefaultKloProjectConfig('warehouse'),
...buildDefaultKtxProjectConfig('warehouse'),
connections,
},
};
}
function successResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KloPublicIngestTargetResult {
function successResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KtxPublicIngestTargetResult {
return {
connectionId,
driver,
@ -55,7 +55,7 @@ function successResult(connectionId: string, driver: string, operation: 'scan' |
};
}
function failedResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KloPublicIngestTargetResult {
function failedResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KtxPublicIngestTargetResult {
return {
connectionId,
driver,
@ -78,7 +78,7 @@ describe('extractProgressMessage', () => {
});
it('returns null for non-progress output', () => {
expect(extractProgressMessage('KLO scan completed\n')).toBeNull();
expect(extractProgressMessage('KTX scan completed\n')).toBeNull();
});
});
@ -137,7 +137,7 @@ describe('renderContextBuildView', () => {
]);
const output = renderContextBuildView(state, { styled: false });
expect(output).toContain('Building KLO context');
expect(output).toContain('Building KTX context');
expect(output).toContain('Primary sources:');
expect(output).toContain('warehouse');
expect(output).toContain('queued');
@ -237,7 +237,7 @@ describe('runContextBuild', () => {
);
const output = io.stdout();
expect(output).toContain('Building KLO context');
expect(output).toContain('Building KTX context');
expect(output).toContain('Primary sources:');
expect(output).toContain('warehouse');
expect(output).toContain('Context sources:');
@ -297,7 +297,7 @@ describe('runContextBuild', () => {
expect(mockExit).toHaveBeenCalledWith(0);
expect(io.stdout()).toContain('Context build continuing in the background.');
expect(io.stdout()).toContain('Resume: klo setup --project-dir /tmp/project');
expect(io.stdout()).toContain('Resume: ktx setup --project-dir /tmp/project');
mockExit.mockRestore();
});
});

View file

@ -1,12 +1,12 @@
import { spawn } from 'node:child_process';
import { mkdirSync, openSync } from 'node:fs';
import { join, resolve } from 'node:path';
import type { KloCliIo } from './index.js';
import type { KtxCliIo } from './index.js';
import type {
KloPublicIngestArgs,
KloPublicIngestPlanTarget,
KloPublicIngestProject,
KloPublicIngestTargetResult,
KtxPublicIngestArgs,
KtxPublicIngestPlanTarget,
KtxPublicIngestProject,
KtxPublicIngestTargetResult,
} from './public-ingest.js';
import { buildPublicIngestPlan, executePublicIngestTarget } from './public-ingest.js';
import { formatDuration } from './demo-metrics.js';
@ -18,7 +18,7 @@ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧',
const ESC = String.fromCharCode(0x1b);
export interface ContextBuildTargetState {
target: KloPublicIngestPlanTarget;
target: KtxPublicIngestPlanTarget;
status: 'queued' | 'running' | 'done' | 'failed';
detailLine: string | null;
summaryText: string | null;
@ -131,7 +131,7 @@ function renderTargetGroup(
}
function resumeCommand(projectDir?: string): string {
return projectDir ? `klo setup --project-dir ${projectDir}` : 'klo setup';
return projectDir ? `ktx setup --project-dir ${projectDir}` : 'ktx setup';
}
export function renderContextBuildView(
@ -142,7 +142,7 @@ export function renderContextBuildView(
const width = columnWidth(state);
const lines: string[] = [
'',
'Building KLO context',
'Building KTX context',
'─────────────────────',
...renderTargetGroup('Primary sources', state.primarySources, state.frame, styled, width),
...renderTargetGroup('Context sources', state.contextSources, state.frame, styled, width),
@ -184,7 +184,7 @@ export function parseIngestSummary(output: string): string | null {
}
interface CapturedIo {
io: KloCliIo;
io: KtxCliIo;
captured(): string;
}
@ -212,7 +212,7 @@ function createCaptureIo(onProgress: (message: string) => void, isTTY: boolean):
// --- Repaint ---
function createRepainter(io: KloCliIo) {
function createRepainter(io: KtxCliIo) {
let lastLineCount = 0;
return {
@ -229,7 +229,7 @@ function createRepainter(io: KloCliIo) {
// --- Background build ---
function resolveKloEntryScript(): string | null {
function resolveKtxEntryScript(): string | null {
const argv1 = process.argv[1];
if (argv1 && (argv1.endsWith('.js') || argv1.endsWith('.ts') || argv1.endsWith('.mjs'))) {
return argv1;
@ -238,11 +238,11 @@ function resolveKloEntryScript(): string | null {
}
function spawnBackgroundBuild(projectDir: string): { logPath: string } | null {
const entryScript = resolveKloEntryScript();
const entryScript = resolveKtxEntryScript();
if (!entryScript) return null;
const resolvedDir = resolve(projectDir);
const logDir = join(resolvedDir, '.klo', 'setup');
const logDir = join(resolvedDir, '.ktx', 'setup');
mkdirSync(logDir, { recursive: true });
const logPath = join(logDir, 'context-build.log');
const logFd = openSync(logPath, 'w');
@ -280,11 +280,11 @@ function defaultSetupKeystroke(onDetach: () => void, onCtrlC: () => void): (() =
// --- Orchestration ---
function makeTargetState(target: KloPublicIngestPlanTarget): ContextBuildTargetState {
function makeTargetState(target: KtxPublicIngestPlanTarget): ContextBuildTargetState {
return { target, status: 'queued', detailLine: null, summaryText: null, startedAt: null, elapsedMs: 0 };
}
export function initViewState(targets: KloPublicIngestPlanTarget[]): ContextBuildViewState {
export function initViewState(targets: KtxPublicIngestPlanTarget[]): ContextBuildViewState {
return {
primarySources: targets.filter((t) => t.operation === 'scan').map(makeTargetState),
contextSources: targets.filter((t) => t.operation === 'source-ingest').map(makeTargetState),
@ -293,9 +293,9 @@ export function initViewState(targets: KloPublicIngestPlanTarget[]): ContextBuil
}
export async function runContextBuild(
project: KloPublicIngestProject,
project: KtxPublicIngestProject,
args: ContextBuildArgs,
io: KloCliIo,
io: KtxCliIo,
deps: ContextBuildDeps = {},
): Promise<ContextBuildResult> {
const plan = buildPublicIngestPlan(project, { projectDir: args.projectDir, all: true });
@ -339,7 +339,7 @@ export async function runContextBuild(
const bg = spawnBackgroundBuild(args.projectDir);
io.stdout.write('\n\nContext build continuing in the background.\n');
if (bg) io.stdout.write(`Log: ${bg.logPath}\n`);
io.stdout.write(`Status: klo setup context status --project-dir ${resolve(args.projectDir)}\n`);
io.stdout.write(`Status: ktx setup context status --project-dir ${resolve(args.projectDir)}\n`);
io.stdout.write(`Resume: ${resumeCommand(args.projectDir)}\n`);
process.exit(0);
},
@ -351,7 +351,7 @@ export async function runContextBuild(
},
);
}
const runArgs: Extract<KloPublicIngestArgs, { command: 'run' }> = {
const runArgs: Extract<KtxPublicIngestArgs, { command: 'run' }> = {
command: 'run',
projectDir: args.projectDir,
all: true,

View file

@ -28,7 +28,7 @@ async function readPackagedJson<T>(relativePath: string): Promise<T> {
}
describe('demo assets', () => {
const projectDir = join(tmpdir(), `klo-demo-assets-${process.pid}`);
const projectDir = join(tmpdir(), `ktx-demo-assets-${process.pid}`);
afterEach(async () => {
await rm(projectDir, { recursive: true, force: true });
@ -36,8 +36,8 @@ describe('demo assets', () => {
it('resolves the default demo root under the OS temp directory', () => {
const dir = defaultDemoProjectDir();
expect(dir.startsWith(join(tmpdir(), 'klo-demo-'))).toBe(true);
expect(dir).toMatch(/klo-demo-[a-f0-9]{8}$/);
expect(dir.startsWith(join(tmpdir(), 'ktx-demo-'))).toBe(true);
expect(dir).toMatch(/ktx-demo-[a-f0-9]{8}$/);
});
it('exports the packaged Orbit demo identity', () => {
@ -124,7 +124,7 @@ describe('demo assets', () => {
await expect(access(join(projectDir, 'raw-sources'))).resolves.toBeUndefined();
await expect(access(join(projectDir, '_schema'))).rejects.toMatchObject({ code: 'ENOENT' });
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('backend: anthropic');
expect(config).toContain('api_key: env:ANTHROPIC_API_KEY');
expect(config).not.toContain('sk-ant-');
@ -187,7 +187,7 @@ describe('demo assets', () => {
await expect(inspectDemoProjectState(projectDir)).resolves.toEqual({
status: 'missing',
projectDir,
missing: ['klo.yaml', 'demo.db', 'state.sqlite', 'replays/replay.memory-flow.v1.json'],
missing: ['ktx.yaml', 'demo.db', 'state.sqlite', 'replays/replay.memory-flow.v1.json'],
});
await ensureDemoProject({ projectDir, force: false });
@ -210,7 +210,7 @@ describe('demo assets', () => {
await rm(join(projectDir, 'demo.db'), { force: true });
await expect(resetDemoProject({ projectDir, force: false })).rejects.toThrow(
`klo setup demo reset is destructive; pass --force to recreate ${projectDir}`,
`ktx setup demo reset is destructive; pass --force to recreate ${projectDir}`,
);
await expect(resetDemoProject({ projectDir, force: true })).resolves.toMatchObject({ projectDir });
@ -218,10 +218,10 @@ describe('demo assets', () => {
await expect(inspectDemoProjectState(projectDir)).resolves.toMatchObject({ status: 'ready' });
});
it('preserves a user-edited klo.yaml across reset --force', async () => {
it('preserves a user-edited ktx.yaml across reset --force', async () => {
await ensureDemoProject({ projectDir, force: false });
const customConfig = [
'project: klo-demo-orbit',
'project: ktx-demo-orbit',
'connections:',
` ${DEMO_CONNECTION_ID}:`,
' driver: sqlite',
@ -232,7 +232,7 @@ describe('demo assets', () => {
' search: sqlite-fts5',
' git:',
' auto_commit: true',
' author: klo <klo@example.com>',
' author: ktx <ktx@example.com>',
'llm:',
' provider:',
' backend: vertex',
@ -253,20 +253,20 @@ describe('demo assets', () => {
' failureMode: continue',
'',
].join('\n');
await writeFile(join(projectDir, 'klo.yaml'), customConfig, 'utf-8');
await writeFile(join(projectDir, 'ktx.yaml'), customConfig, 'utf-8');
await resetDemoProject({ projectDir, force: true });
const preserved = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const preserved = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(preserved).toBe(customConfig);
expect(preserved).toContain('backend: vertex');
expect(preserved).not.toContain('backend: anthropic');
await expect(inspectDemoProjectState(projectDir)).resolves.toMatchObject({ status: 'ready' });
});
it('still writes the default klo.yaml on reset when none exists', async () => {
it('still writes the default ktx.yaml on reset when none exists', async () => {
await resetDemoProject({ projectDir, force: true });
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('backend: anthropic');
});
});

View file

@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { randomBytes } from 'node:crypto';
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { loadDemoReplayFile, loadLatestDemoReplay } from './demo-replay-store.js';
interface DemoProjectResult {
@ -33,7 +33,7 @@ export const DEMO_REPLAY_FILE = 'replay.memory-flow.v1.json';
export const DEMO_FULL_JOB_ID = 'demo-full-ingest';
const REQUIRED_BASE_PROJECT_PATHS = [
'klo.yaml',
'ktx.yaml',
'demo.db',
'state.sqlite',
join('replays', DEMO_REPLAY_FILE),
@ -70,7 +70,7 @@ async function exists(path: string): Promise<boolean> {
export function defaultDemoProjectDir(): string {
const suffix = randomBytes(4).toString('hex');
return join(tmpdir(), `klo-demo-${suffix}`);
return join(tmpdir(), `ktx-demo-${suffix}`);
}
export async function inspectDemoProjectState(projectDir: string): Promise<DemoProjectState> {
@ -97,10 +97,10 @@ export async function inspectDemoProjectState(projectDir: string): Promise<DemoP
export async function resetDemoProject(options: EnsureDemoProjectOptions): Promise<DemoProjectResult> {
const projectDir = resolve(options.projectDir);
if (!options.force) {
throw new Error(`klo setup demo reset is destructive; pass --force to recreate ${projectDir}`);
throw new Error(`ktx setup demo reset is destructive; pass --force to recreate ${projectDir}`);
}
const preservedConfig = await readExistingConfig(join(projectDir, 'klo.yaml'));
const preservedConfig = await readExistingConfig(join(projectDir, 'ktx.yaml'));
const result = await ensureDemoProject({ projectDir, force: true });
if (preservedConfig !== null) {
await writeFile(result.configPath, preservedConfig, 'utf-8');
@ -118,7 +118,7 @@ async function readExistingConfig(configPath: string): Promise<string | null> {
function demoConfig(databasePath: string): string {
return [
'project: klo-demo-orbit',
'project: ktx-demo-orbit',
'connections:',
` ${DEMO_CONNECTION_ID}:`,
' driver: sqlite',
@ -129,7 +129,7 @@ function demoConfig(databasePath: string): string {
' search: sqlite-fts5',
' git:',
' auto_commit: true',
' author: klo <klo@example.com>',
' author: ktx <ktx@example.com>',
'llm:',
' provider:',
' backend: anthropic',
@ -185,7 +185,7 @@ async function assertPackagedSeededAssetsPresent(): Promise<void> {
export async function ensureDemoProject(options: EnsureDemoProjectOptions): Promise<DemoProjectResult> {
const projectDir = resolve(options.projectDir);
const configPath = join(projectDir, 'klo.yaml');
const configPath = join(projectDir, 'ktx.yaml');
if (!options.force && (await exists(configPath))) {
throw new Error(`Demo project already exists at ${projectDir}; pass --force to recreate it`);
}
@ -237,7 +237,7 @@ export async function ensureSeededDemoProject(options: EnsureDemoProjectOptions)
if (!options.force && error instanceof Error && error.message.includes('Demo project already exists')) {
return {
projectDir,
configPath: join(projectDir, 'klo.yaml'),
configPath: join(projectDir, 'ktx.yaml'),
databasePath: join(projectDir, 'demo.db'),
replayPath: join(projectDir, 'replays', DEMO_REPLAY_FILE),
};

View file

@ -1,7 +1,7 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { IngestReportSnapshot, LocalIngestResult, RunLocalIngestOptions } from '@klo/context/ingest';
import type { IngestReportSnapshot, LocalIngestResult, RunLocalIngestOptions } from '@ktx/context/ingest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
import {
@ -69,7 +69,7 @@ describe('full demo helpers', () => {
let projectDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-full-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-full-'));
projectDir = join(tempDir, 'demo');
await ensureDemoProject({ projectDir, force: false });
});
@ -79,18 +79,18 @@ describe('full demo helpers', () => {
});
it('fails full mode with exact Anthropic env guidance when the key is missing', async () => {
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
expect(() => assertFullDemoCredentials(project, {})).toThrow(
'klo setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `klo setup demo --mode full --no-input`, or run `klo setup demo --mode seeded --no-input` without credentials.',
'ktx setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `ktx setup demo --mode full --no-input`, or run `ktx setup demo --mode seeded --no-input` without credentials.',
);
});
it('respects an existing gateway provider project for full mode', async () => {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: klo-demo-orbit',
'project: ktx-demo-orbit',
'connections:',
' orbit_demo:',
' driver: sqlite',
@ -109,22 +109,22 @@ describe('full demo helpers', () => {
].join('\n'),
'utf-8',
);
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
expect(() => assertFullDemoCredentials(project, {})).not.toThrow();
expect(fullDemoCredentialStatus(project, {})).toEqual({ status: 'ready' });
});
it('reports full-demo credential status without throwing', async () => {
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
expect(fullDemoCredentialStatus(project, {})).toEqual({ status: 'missing-anthropic-key' });
expect(fullDemoCredentialStatus(project, { ANTHROPIC_API_KEY: 'sk-ant-test' })).toEqual({ status: 'ready' }); // pragma: allowlist secret
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: klo-demo-orbit',
'project: ktx-demo-orbit',
'connections:',
' orbit_demo:',
' driver: sqlite',
@ -136,7 +136,7 @@ describe('full demo helpers', () => {
].join('\n'),
'utf-8',
);
const disabledProject = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
const disabledProject = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
expect(fullDemoCredentialStatus(disabledProject, {})).toEqual({ status: 'unsupported-provider', provider: 'none' });
});
@ -192,9 +192,9 @@ describe('full demo helpers', () => {
expect(summary).toContain('Full demo ingest: done');
expect(summary).toContain('Saved memory: 1 wiki, 1 semantic layer');
expect(summary).toContain('Provenance rows: 2');
expect(summary).toContain('Next: klo setup demo inspect');
expect(summary).toContain('Shows the files, semantic-layer sources, and memory KLO just produced.');
expect(summary).toContain('Next: klo setup demo replay');
expect(summary).toContain('Next: ktx setup demo inspect');
expect(summary).toContain('Shows the files, semantic-layer sources, and memory KTX just produced.');
expect(summary).toContain('Next: ktx setup demo replay');
expect(summary).toContain('Replays the same visual story without calling the LLM again.');
expect(summary).not.toContain('--viz');
});

View file

@ -1,4 +1,4 @@
import { resolveKloConfigReference } from '@klo/context/core';
import { resolveKtxConfigReference } from '@ktx/context/core';
import {
createMemoryFlowLiveBuffer,
ingestReportToMemoryFlowReplay,
@ -7,12 +7,12 @@ import {
type LocalIngestResult,
type MemoryFlowReplayInput,
type RunLocalIngestOptions,
} from '@klo/context/ingest';
import { loadKloProject, type KloLocalProject } from '@klo/context/project';
import { runLocalScan, type LocalScanRunResult } from '@klo/context/scan';
} from '@ktx/context/ingest';
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import { runLocalScan, type LocalScanRunResult } from '@ktx/context/scan';
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
import { runDemoScan } from './demo-scan.js';
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
import { formatNextStepLines } from './next-steps.js';
interface DemoFullOptions {
@ -24,7 +24,7 @@ interface DemoFullOptions {
}
export interface DemoFullResult {
project: KloLocalProject;
project: KtxLocalProject;
scan: LocalScanRunResult;
ingest: LocalIngestResult;
report: IngestReportSnapshot;
@ -54,7 +54,7 @@ function savedCounts(report: IngestReportSnapshot): { wikiCount: number; slCount
}
export function fullDemoCredentialStatus(
project: KloLocalProject,
project: KtxLocalProject,
env: NodeJS.ProcessEnv = process.env,
): FullDemoCredentialStatus {
const llm = project.config.llm;
@ -62,14 +62,14 @@ export function fullDemoCredentialStatus(
return { status: 'unsupported-provider', provider: llm.provider.backend };
}
if (llm.provider.backend === 'anthropic' && !resolveKloConfigReference(llm.provider.anthropic?.api_key, env)) {
if (llm.provider.backend === 'anthropic' && !resolveKtxConfigReference(llm.provider.anthropic?.api_key, env)) {
return { status: 'missing-anthropic-key' };
}
return { status: 'ready' };
}
export function assertFullDemoCredentials(project: KloLocalProject, env: NodeJS.ProcessEnv = process.env): void {
export function assertFullDemoCredentials(project: KtxLocalProject, env: NodeJS.ProcessEnv = process.env): void {
const llm = project.config.llm;
const status = fullDemoCredentialStatus(project, env);
if (status.status === 'ready') {
@ -78,13 +78,13 @@ export function assertFullDemoCredentials(project: KloLocalProject, env: NodeJS.
if (status.status === 'unsupported-provider') {
throw new Error(
'klo setup demo --mode full requires llm.provider.backend: anthropic, vertex, or gateway. Run `klo setup demo init --force --no-input` to recreate the demo config, or run `klo setup demo --mode seeded --no-input` without credentials.',
'ktx setup demo --mode full requires llm.provider.backend: anthropic, vertex, or gateway. Run `ktx setup demo init --force --no-input` to recreate the demo config, or run `ktx setup demo --mode seeded --no-input` without credentials.',
);
}
if (llm.provider.backend === 'anthropic') {
throw new Error(
'klo setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `klo setup demo --mode full --no-input`, or run `klo setup demo --mode seeded --no-input` without credentials.',
'ktx setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `ktx setup demo --mode full --no-input`, or run `ktx setup demo --mode seeded --no-input` without credentials.',
);
}
}
@ -110,7 +110,7 @@ function initialFullReplay(projectDir: string): MemoryFlowReplayInput {
export async function runDemoFull(options: DemoFullOptions): Promise<DemoFullResult> {
await ensureDemoProjectForReuse(options.projectDir);
const project = await loadKloProject({ projectDir: options.projectDir });
const project = await loadKtxProject({ projectDir: options.projectDir });
assertFullDemoCredentials(project, options.env);
const { result: scan } = await runDemoScan({
@ -125,7 +125,7 @@ export async function runDemoFull(options: DemoFullOptions): Promise<DemoFullRes
const executeLocalIngest = options.runLocalIngest ?? runLocalIngest;
const ingest = await executeLocalIngest({
project,
adapters: createKloCliLocalIngestAdapters(project),
adapters: createKtxCliLocalIngestAdapters(project),
adapter: DEMO_ADAPTER,
connectionId: DEMO_CONNECTION_ID,
trigger: 'manual_resync',
@ -152,9 +152,9 @@ export function formatFullDemoSummary(report: IngestReportSnapshot): string {
`Sync: ${report.body.syncId}`,
`Saved memory: ${counts.wikiCount} wiki, ${counts.slCount} semantic layer`,
`Provenance rows: ${report.body.provenanceRows.length}`,
'Next: klo setup demo inspect',
' Shows the files, semantic-layer sources, and memory KLO just produced.',
'Next: klo setup demo replay',
'Next: ktx setup demo inspect',
' Shows the files, semantic-layer sources, and memory KTX just produced.',
'Next: ktx setup demo replay',
' Replays the same visual story without calling the LLM again.',
'',
].join('\n');
@ -176,7 +176,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
const conflictCount = report.body.conflictsResolved.length;
const areasAnalyzed = workUnits.filter((wu) => wu.actions.length > 0).length;
const lines: string[] = ['', '★ KLO finished ingesting your data', ''];
const lines: string[] = ['', '★ KTX finished ingesting your data', ''];
if (areasAnalyzed > 0) {
lines.push(` ✓ Analyzed ${areasAnalyzed} business area${areasAnalyzed === 1 ? '' : 's'}`);
@ -187,7 +187,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
lines.push('');
if (counts.slCount > 0 || counts.wikiCount > 0) {
lines.push(' KLO created:');
lines.push(' KTX created:');
if (counts.slCount > 0) lines.push(` 📊 ${counts.slCount} query definition${counts.slCount === 1 ? '' : 's'} — so agents can write accurate SQL for your data`);
if (counts.wikiCount > 0) lines.push(` 📝 ${counts.wikiCount} knowledge page${counts.wikiCount === 1 ? '' : 's'} — so agents understand your business context`);
lines.push('');
@ -206,7 +206,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
lines.push(' What to do next:');
lines.push(...formatNextStepLines());
lines.push('');
lines.push(` Your KLO project files are at: ${projectDir}`);
lines.push(` Your KTX project files are at: ${projectDir}`);
lines.push('');
return lines.join('\n');

View file

@ -21,7 +21,7 @@ describe('demo interaction decisions', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-interaction-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-interaction-'));
});
afterEach(async () => {
@ -53,7 +53,7 @@ describe('demo interaction decisions', () => {
prompts: createTestDemoPromptAdapter({ choices: [] }),
}),
).rejects.toThrow(
`Demo project is not ready at ${tempDir}: missing demo.db. Run klo setup demo reset --project-dir ${tempDir} --force --no-input`,
`Demo project is not ready at ${tempDir}: missing demo.db. Run ktx setup demo reset --project-dir ${tempDir} --force --no-input`,
);
});

View file

@ -2,7 +2,7 @@ import { cancel, confirm, isCancel, password, select, text } from '@clack/prompt
import type { Option as ClackOption } from '@clack/prompts';
import { resolve } from 'node:path';
import { inspectDemoProjectState } from './demo-assets.js';
import type { KloDemoInputMode } from './demo.js';
import type { KtxDemoInputMode } from './demo.js';
import { withMenuOptionsSpacing } from './prompt-navigation.js';
type DemoPromptOption<T extends string> = ClackOption<T>;
@ -29,7 +29,7 @@ type FullCredentialDecision =
| { action: 'run-mode'; mode: 'seeded' | 'replay' }
| { action: 'cancel' };
function isInteractive(inputMode: KloDemoInputMode | undefined, io: DemoInteractiveIo): boolean {
function isInteractive(inputMode: KtxDemoInputMode | undefined, io: DemoInteractiveIo): boolean {
return inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
}
@ -97,7 +97,7 @@ export function createTestDemoPromptAdapter(options: {
export async function chooseDemoProjectForInteractiveRun(options: {
projectDir: string;
inputMode?: KloDemoInputMode;
inputMode?: KtxDemoInputMode;
io: DemoInteractiveIo;
prompts?: DemoPromptAdapter;
}): Promise<DemoProjectDecision> {
@ -108,7 +108,7 @@ export async function chooseDemoProjectForInteractiveRun(options: {
if (!isInteractive(options.inputMode, options.io)) {
if (state.status === 'corrupt') {
throw new Error(
`Demo project is not ready at ${projectDir}: missing ${state.missing.join(', ')}. Run klo setup demo reset --project-dir ${projectDir} --force --no-input`,
`Demo project is not ready at ${projectDir}: missing ${state.missing.join(', ')}. Run ktx setup demo reset --project-dir ${projectDir} --force --no-input`,
);
}
return { action: 'use', projectDir, reset: false };
@ -163,7 +163,7 @@ export async function chooseDemoProjectForInteractiveRun(options: {
export async function resolveFullCredentialDecision(options: {
needsAnthropicKey: boolean;
inputMode?: KloDemoInputMode;
inputMode?: KtxDemoInputMode;
io: DemoInteractiveIo;
env: NodeJS.ProcessEnv;
prompts?: DemoPromptAdapter;

View file

@ -1,4 +1,4 @@
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { describe, expect, it } from 'vitest';
import {
buildDemoMetrics,

View file

@ -1,4 +1,4 @@
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
const DEFAULT_INPUT_TOKENS_PER_STEP = 4500;
const DEFAULT_OUTPUT_TOKENS_PER_STEP = 700;

View file

@ -1,4 +1,4 @@
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { describe, expect, it } from 'vitest';
import { createPlainProgressEmitter, formatMemoryFlowEventLine } from './demo-progress.js';

View file

@ -1,5 +1,5 @@
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { KloDemoIo } from './demo.js';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import type { KtxDemoIo } from './demo.js';
function plural(n: number, one: string, many = `${one}s`): string {
return `${n} ${n === 1 ? one : many}`;
@ -62,7 +62,7 @@ export function formatMemoryFlowEventLine(event: MemoryFlowEvent): string | null
}
}
export function createPlainProgressEmitter(io: KloDemoIo): (snapshot: MemoryFlowReplayInput) => void {
export function createPlainProgressEmitter(io: KtxDemoIo): (snapshot: MemoryFlowReplayInput) => void {
let printed = 0;
return (snapshot) => {
while (printed < snapshot.events.length) {

View file

@ -1,7 +1,7 @@
import { mkdtemp, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { type MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import { type MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { describe, expect, it } from 'vitest';
import { DEMO_LATEST_REPLAY_FILE, loadLatestDemoReplay, writeDemoReplay } from './demo-replay-store.js';
@ -35,7 +35,7 @@ function replay(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowRepla
describe('demo replay store', () => {
it('writes a versioned replay file and updates latest', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'klo-demo-replay-store-'));
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-demo-replay-store-'));
const saved = await writeDemoReplay(projectDir, replay(), { label: 'full' });
@ -53,7 +53,7 @@ describe('demo replay store', () => {
});
it('returns null when no latest local replay exists', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'klo-demo-replay-store-empty-'));
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-demo-replay-store-empty-'));
await expect(loadLatestDemoReplay(projectDir)).resolves.toBeNull();
});

View file

@ -1,7 +1,7 @@
import { constants as fsConstants } from 'node:fs';
import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { parseMemoryFlowReplayInput, type MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import { parseMemoryFlowReplayInput, type MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
interface StoredMemoryFlowReplayFile {
memoryFlowReplaySchemaVersion: 1;

View file

@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest';
import { findLatestDemoScanReport, runDemoScan } from './demo-scan.js';
describe('demo scan helpers', () => {
const projectDir = join(tmpdir(), `klo-demo-scan-${process.pid}`);
const projectDir = join(tmpdir(), `ktx-demo-scan-${process.pid}`);
afterEach(async () => {
await rm(projectDir, { recursive: true, force: true });

View file

@ -1,9 +1,9 @@
import { getLocalIngestStatus, type IngestReportSnapshot, type MemoryFlowReplayInput } from '@klo/context/ingest';
import { loadKloProject, type KloLocalProject } from '@klo/context/project';
import { runLocalScan, type KloScanReport, type LocalScanRunResult } from '@klo/context/scan';
import { getLocalIngestStatus, type IngestReportSnapshot, type MemoryFlowReplayInput } from '@ktx/context/ingest';
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import { runLocalScan, type KtxScanReport, type LocalScanRunResult } from '@ktx/context/scan';
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
import { loadLatestDemoReplay } from './demo-replay-store.js';
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
interface DemoScanOptions {
projectDir: string;
@ -13,13 +13,13 @@ interface DemoScanOptions {
}
interface DemoScanResult {
project: KloLocalProject;
project: KtxLocalProject;
result: LocalScanRunResult;
}
interface DemoInspectSummary {
projectDir: string;
scanReport: KloScanReport | null;
scanReport: KtxScanReport | null;
fullReport: IngestReportSnapshot | null;
semanticLayerFileCount: number;
knowledgeFileCount: number;
@ -28,7 +28,7 @@ interface DemoInspectSummary {
}
interface DemoInspectDeps {
findFullReport?: (project: KloLocalProject) => Promise<IngestReportSnapshot | null>;
findFullReport?: (project: KtxLocalProject) => Promise<IngestReportSnapshot | null>;
}
async function ensureDemoProjectForReuse(projectDir: string): Promise<void> {
@ -40,36 +40,36 @@ async function ensureDemoProjectForReuse(projectDir: string): Promise<void> {
});
}
async function loadReadyDemoProject(projectDir: string): Promise<KloLocalProject> {
async function loadReadyDemoProject(projectDir: string): Promise<KtxLocalProject> {
try {
return await loadKloProject({ projectDir });
return await loadKtxProject({ projectDir });
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(
`Demo project is not ready at ${projectDir}: ${reason}. Run klo setup demo init --project-dir ${projectDir} --force --no-input to recreate it.`,
`Demo project is not ready at ${projectDir}: ${reason}. Run ktx setup demo init --project-dir ${projectDir} --force --no-input to recreate it.`,
);
}
}
function reportDiff(report: KloScanReport): string {
function reportDiff(report: KtxScanReport): string {
return `+${report.diffSummary.tablesAdded}/~${report.diffSummary.tablesModified}/-${report.diffSummary.tablesDeleted}/=${report.diffSummary.tablesUnchanged}`;
}
function jsonReport(raw: string, path: string): KloScanReport {
function jsonReport(raw: string, path: string): KtxScanReport {
try {
return JSON.parse(raw) as KloScanReport;
return JSON.parse(raw) as KtxScanReport;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(`Invalid demo scan report at ${path}: ${reason}`);
}
}
async function countFiles(project: KloLocalProject, root: string, predicate: (path: string) => boolean): Promise<number> {
async function countFiles(project: KtxLocalProject, root: string, predicate: (path: string) => boolean): Promise<number> {
const { files } = await project.fileStore.listFiles(root, true);
return files.filter(predicate).length;
}
async function findFullDemoReport(project: KloLocalProject): Promise<IngestReportSnapshot | null> {
async function findFullDemoReport(project: KtxLocalProject): Promise<IngestReportSnapshot | null> {
return getLocalIngestStatus(project, DEMO_FULL_JOB_ID);
}
@ -92,13 +92,13 @@ export async function runDemoScan(options: DemoScanOptions): Promise<DemoScanRes
trigger: 'cli',
jobId: options.jobId ?? 'demo-scan',
now: options.now,
adapters: createKloCliLocalIngestAdapters(project),
adapters: createKtxCliLocalIngestAdapters(project),
});
return { project, result };
}
export async function findLatestDemoScanReport(projectDir: string): Promise<KloScanReport | null> {
export async function findLatestDemoScanReport(projectDir: string): Promise<KtxScanReport | null> {
const project = await loadReadyDemoProject(projectDir);
const root = `raw-sources/${DEMO_CONNECTION_ID}/${DEMO_ADAPTER}`;
const { files } = await project.fileStore.listFiles(root, true);
@ -117,7 +117,7 @@ export async function findLatestDemoScanReport(projectDir: string): Promise<KloS
export async function inspectDemoProject(
projectDir: string,
projectOverride?: KloLocalProject,
projectOverride?: KtxLocalProject,
deps: DemoInspectDeps = {},
): Promise<DemoInspectSummary> {
const project = projectOverride ?? (await loadReadyDemoProject(projectDir));
@ -143,7 +143,7 @@ export async function inspectDemoProject(
};
}
export function formatDemoScanSummary(report: KloScanReport): string {
export function formatDemoScanSummary(report: KtxScanReport): string {
return [
'Demo scan: done',
`Connection: ${report.connectionId}`,
@ -152,7 +152,7 @@ export function formatDemoScanSummary(report: KloScanReport): string {
`Tables: ${reportDiff(report)}`,
`Semantic-layer artifacts: ${report.artifactPaths.manifestShards.length}`,
`Report: ${report.artifactPaths.reportPath ?? 'none'}`,
'Next: klo setup demo inspect',
'Next: ktx setup demo inspect',
' Shows the files and semantic-layer draft created from the database scan.',
'',
].join('\n');
@ -190,22 +190,22 @@ export function formatDemoInspect(summary: DemoInspectSummary): string {
: [report ? 'Memory synthesis: full mode not run' : 'Memory synthesis: not run'];
const next = fullReport
? [
`Next: klo ingest watch ${fullReport.runId} --project-dir ${summary.projectDir}`,
`Next: ktx ingest watch ${fullReport.runId} --project-dir ${summary.projectDir}`,
' Opens the captured run timeline and lets you inspect what happened.',
'Next: klo setup demo replay',
'Next: ktx setup demo replay',
' Replays the same visual story without calling the LLM again.',
]
: report
? [
'Next: klo setup demo --mode full',
'Next: ktx setup demo --mode full',
' Runs the full AI-backed pass with your LLM provider.',
'Next: klo setup demo replay',
'Next: ktx setup demo replay',
' Replays the packaged visual story without calling the LLM.',
]
: [
'Next: klo setup demo --no-input',
'Next: ktx setup demo --no-input',
' Runs the pre-seeded demo without calling the LLM.',
'Next: klo setup demo --mode full',
'Next: ktx setup demo --mode full',
' Runs the full AI-backed pass with your LLM provider.',
];

View file

@ -4,10 +4,10 @@ import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { runDemoSeeded } from './demo-seeded.js';
import { formatSeededInspect, inspectSeededProject } from './demo-seeded-inspect.js';
import { KLO_NEXT_STEP_COMMANDS } from './next-steps.js';
import { KTX_NEXT_STEP_COMMANDS } from './next-steps.js';
describe('seeded demo inspect contract', () => {
const projectDir = join(tmpdir(), `klo-demo-seeded-inspect-${process.pid}`);
const projectDir = join(tmpdir(), `ktx-demo-seeded-inspect-${process.pid}`);
afterEach(async () => {
await rm(projectDir, { recursive: true, force: true });
@ -59,7 +59,7 @@ describe('seeded demo inspect contract', () => {
reports: { primaryPath: 'reports/seeded-demo-report.json', fileCount: 1 },
replays: { primaryPath: 'replays/replay.memory-flow.v1.json', latestPath: 'replays/latest.memory-flow.v1.json' },
},
nextCommands: KLO_NEXT_STEP_COMMANDS,
nextCommands: KTX_NEXT_STEP_COMMANDS,
});
expect(inspect.generatedOutputs.replays.fileCount).toBeGreaterThanOrEqual(3);
@ -89,13 +89,13 @@ describe('seeded demo inspect contract', () => {
expect(output).toContain('Report: reports/seeded-demo-report.json');
expect(output).toContain('Replay: replays/replay.memory-flow.v1.json');
expect(output).toContain('Latest replay: seeded (packaged, prebuilt)');
expect(output).toContain(' $ klo agent tools --json');
expect(output).toContain(' $ klo agent context --json');
expect(output).toContain(' $ klo serve --mcp stdio --user-id local');
expect(output.indexOf('klo agent tools --json')).toBeLessThan(
output.indexOf('klo serve --mcp stdio --user-id local'),
expect(output).toContain(' $ ktx agent tools --json');
expect(output).toContain(' $ ktx agent context --json');
expect(output).toContain(' $ ktx serve --mcp stdio --user-id local');
expect(output.indexOf('ktx agent tools --json')).toBeLessThan(
output.indexOf('ktx serve --mcp stdio --user-id local'),
);
expect(output).not.toContain('klo ask');
expect(output).not.toContain('ktx ask');
expect(output).not.toContain('deterministic mode');
});

View file

@ -1,10 +1,10 @@
import { constants as fsConstants } from 'node:fs';
import { access, readFile, readdir } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { loadPackagedDemoReplay } from './demo-assets.js';
import { DEMO_LATEST_REPLAY_FILE, loadLatestDemoReplay } from './demo-replay-store.js';
import { KLO_NEXT_STEP_COMMANDS, KLO_NEXT_STEP_COMMAND_WIDTH } from './next-steps.js';
import { KTX_NEXT_STEP_COMMANDS, KTX_NEXT_STEP_COMMAND_WIDTH } from './next-steps.js';
type SeededInspectReadiness = 'missing' | 'ready' | 'corrupt';
@ -66,7 +66,7 @@ export interface SeededInspectSummary {
}
const REQUIRED_SEEDED_PROJECT_PATHS = [
'klo.yaml',
'ktx.yaml',
'demo.db',
'state.sqlite',
'manifest.json',
@ -181,7 +181,7 @@ function sourceBundleFromManifest(manifest: DemoSeededManifest): SeededInspectSu
}
function nextCommands(): SeededInspectSummary['nextCommands'] {
return [...KLO_NEXT_STEP_COMMANDS];
return [...KTX_NEXT_STEP_COMMANDS];
}
function modeMetadataFromReplay(replay: MemoryFlowReplayInput | null): SeededInspectSummary['modeMetadata'] {
@ -291,9 +291,9 @@ export function formatSeededInspect(summary: SeededInspectSummary): string {
);
for (const command of summary.nextCommands) {
lines.push(` $ ${command.command.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} ${command.description}`);
lines.push(` $ ${command.command.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} ${command.description}`);
}
lines.push('', `Your KLO project files are at: ${summary.projectDir}`, '');
lines.push('', `Your KTX project files are at: ${summary.projectDir}`, '');
return lines.join('\n');
}

View file

@ -6,7 +6,7 @@ import { ensureSeededDemoProject } from './demo-assets.js';
import { runDemoSeeded } from './demo-seeded.js';
describe('demo seeded mode', () => {
const projectDir = join(tmpdir(), `klo-demo-seeded-${process.pid}`);
const projectDir = join(tmpdir(), `ktx-demo-seeded-${process.pid}`);
afterEach(async () => {
await rm(projectDir, { recursive: true, force: true });
@ -17,7 +17,7 @@ describe('demo seeded mode', () => {
expect(result.projectDir).toBe(projectDir);
await expect(access(join(projectDir, 'demo.db'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'klo.yaml'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'ktx.yaml'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'manifest.json'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'semantic-layer/orbit_demo/accounts.yaml'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'knowledge/global/arr-contract-first.md'))).resolves.toBeUndefined();
@ -35,7 +35,7 @@ describe('demo seeded mode', () => {
expect(result.replay.metadata?.timing).toBe('prebuilt');
expect(result.inspect.mode).toBe('seeded');
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('api_key: env:ANTHROPIC_API_KEY');
expect(config).not.toContain('sk-ant-');
});

View file

@ -1,4 +1,4 @@
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import {
ensureSeededDemoProject,
loadPackagedDemoReplay,

View file

@ -1,14 +1,14 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { IngestReportSnapshot, MemoryFlowReplayInput } from '@klo/context/ingest';
import type { IngestReportSnapshot, MemoryFlowReplayInput } from '@ktx/context/ingest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloDemo } from './demo.js';
import { runKtxDemo } from './demo.js';
import { DEMO_FULL_JOB_ID, defaultDemoProjectDir, ensureDemoProject } from './demo-assets.js';
import type { DemoFullResult } from './demo-full.js';
import { createTestDemoPromptAdapter } from './demo-interaction.js';
import type { renderMemoryFlowTui } from './memory-flow-tui.js';
import { KLO_NEXT_STEP_COMMANDS } from './next-steps.js';
import { KTX_NEXT_STEP_COMMANDS } from './next-steps.js';
import { resetVizFallbackWarningsForTest } from './viz-fallback.js';
function makeIo(options: { isTTY?: boolean; columns?: number; rawMode?: boolean } = {}) {
@ -108,12 +108,12 @@ function fakeFullResult(projectDir: string): DemoFullResult {
};
}
describe('runKloDemo', () => {
describe('runKtxDemo', () => {
let tempDir: string;
beforeEach(async () => {
resetVizFallbackWarningsForTest();
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-command-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-command-'));
});
afterEach(async () => {
@ -123,7 +123,7 @@ describe('runKloDemo', () => {
it('initializes the demo project', async () => {
const io = makeIo();
await expect(
runKloDemo({ command: 'init', projectDir: tempDir, force: false, inputMode: 'disabled' }, io.io),
runKtxDemo({ command: 'init', projectDir: tempDir, force: false, inputMode: 'disabled' }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain(`Demo project: ${tempDir}`);
@ -135,14 +135,14 @@ describe('runKloDemo', () => {
it('renders the packaged replay in no-input viz mode', async () => {
const io = makeIo({ isTTY: true });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' } },
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow Warehouse + dbt + BI + Docs done');
expect(io.stdout()).toContain('KTX memory flow Warehouse + dbt + BI + Docs done');
expect(io.stdout()).toContain('Saved 16 memories');
expect(io.stderr()).toBe('');
});
@ -152,7 +152,7 @@ describe('runKloDemo', () => {
const renderStoredMemoryFlow = vi.fn<typeof renderMemoryFlowTui>(async () => true);
await expect(
runKloDemo(
runKtxDemo(
{ command: 'replay', projectDir: tempDir, outputMode: 'viz' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
@ -166,7 +166,7 @@ describe('runKloDemo', () => {
adapter: 'live-database',
});
expect(renderStoredMemoryFlow.mock.calls[0]?.[2]).toEqual({ speedMultiplier: 0.125 });
expect(io.stdout()).toContain('KLO finished ingesting your data');
expect(io.stdout()).toContain('KTX finished ingesting your data');
expect(io.stderr()).toBe('');
});
@ -175,7 +175,7 @@ describe('runKloDemo', () => {
const renderStoredMemoryFlow = vi.fn<typeof renderMemoryFlowTui>(async () => true);
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: tempDir, outputMode: 'viz' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
@ -184,7 +184,7 @@ describe('runKloDemo', () => {
expect(renderStoredMemoryFlow).toHaveBeenCalledTimes(1);
expect(renderStoredMemoryFlow.mock.calls[0]?.[2]).toEqual({ speedMultiplier: 0.125 });
expect(io.stdout()).toContain('KLO finished ingesting your data');
expect(io.stdout()).toContain('KTX finished ingesting your data');
expect(io.stderr()).toBe('');
});
@ -193,7 +193,7 @@ describe('runKloDemo', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloDemo(
runKtxDemo(
{ command: 'replay', projectDir: tempDir, outputMode: 'viz' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
@ -203,10 +203,10 @@ describe('runKloDemo', () => {
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
expect(io.stdout()).toContain('Memory-flow summary: done');
expect(io.stdout()).toContain('Connection: orbit_demo');
expect(io.stdout()).toContain('klo sl list');
expect(io.stdout()).toContain('klo wiki list');
expect(io.stdout()).toContain('klo serve --mcp stdio --user-id local');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).toContain('ktx sl list');
expect(io.stdout()).toContain('ktx wiki list');
expect(io.stdout()).toContain('ktx serve --mcp stdio --user-id local');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
);
@ -216,15 +216,15 @@ describe('runKloDemo', () => {
const testIo = makeIo({ isTTY: false });
await expect(
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io),
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io),
).resolves.toBe(0);
expect(testIo.stdout()).toContain('Memory-flow summary: done');
expect(testIo.stdout()).toContain('Connection: orbit_demo');
expect(testIo.stdout()).toContain('klo sl list');
expect(testIo.stdout()).toContain('klo wiki list');
expect(testIo.stdout()).toContain('klo serve --mcp stdio --user-id local');
expect(testIo.stdout()).not.toContain('KLO memory flow');
expect(testIo.stdout()).toContain('ktx sl list');
expect(testIo.stdout()).toContain('ktx wiki list');
expect(testIo.stdout()).toContain('ktx serve --mcp stdio --user-id local');
expect(testIo.stdout()).not.toContain('KTX memory flow');
expect(testIo.stderr()).toContain(
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
);
@ -233,7 +233,7 @@ describe('runKloDemo', () => {
it('prints JSON replay output when requested', async () => {
const io = makeIo();
await expect(
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, io.io),
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, io.io),
).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toMatchObject({ runId: 'demo-seeded-orbit', connectionId: 'orbit_demo' });
@ -242,7 +242,7 @@ describe('runKloDemo', () => {
it('runs the packaged SQLite demo scan', async () => {
const io = makeIo();
await expect(runKloDemo({ command: 'scan', projectDir: tempDir, inputMode: 'disabled' }, io.io)).resolves.toBe(0);
await expect(runKtxDemo({ command: 'scan', projectDir: tempDir, inputMode: 'disabled' }, io.io)).resolves.toBe(0);
expect(io.stdout()).toContain('Demo scan: done');
expect(io.stdout()).toContain('Connection: orbit_demo');
@ -254,7 +254,7 @@ describe('runKloDemo', () => {
it('runs seeded mode with pre-seeded assets and inspect summary', async () => {
const io = makeIo({ isTTY: true });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' } },
@ -272,7 +272,7 @@ describe('runKloDemo', () => {
const io = makeIo();
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: defaultDemoProjectDir(), outputMode: 'plain', inputMode: 'disabled' },
io.io,
),
@ -282,10 +282,10 @@ describe('runKloDemo', () => {
expect(io.stdout()).toContain('Source: packaged demo project');
expect(io.stdout()).toContain('Generated context: prebuilt from bundled assets');
expect(io.stdout()).toContain('LLM calls: none');
expect(io.stdout()).toContain('Your KLO project files are at:');
expect(io.stdout()).toContain(join(tmpdir(), 'klo-demo-'));
expect(io.stdout()).toContain('klo serve --mcp stdio');
expect(io.stdout()).not.toContain(['klo', 'mcp'].join(' '));
expect(io.stdout()).toContain('Your KTX project files are at:');
expect(io.stdout()).toContain(join(tmpdir(), 'ktx-demo-'));
expect(io.stdout()).toContain('ktx serve --mcp stdio');
expect(io.stdout()).not.toContain(['ktx', 'mcp'].join(' '));
expect(io.stdout()).not.toContain('deterministic');
});
@ -293,7 +293,7 @@ describe('runKloDemo', () => {
const testIo = makeIo({ isTTY: true, columns: 120 });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' },
testIo.io,
{ env: { ...process.env, TERM: 'dumb' } },
@ -310,19 +310,19 @@ describe('runKloDemo', () => {
it('prints demo inspect as plain text and JSON', async () => {
const seededIo = makeIo();
await expect(
runKloDemo({ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, seededIo.io),
runKtxDemo({ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, seededIo.io),
).resolves.toBe(0);
const plainIo = makeIo();
await expect(
runKloDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, plainIo.io),
runKtxDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, plainIo.io),
).resolves.toBe(0);
expect(plainIo.stdout()).toContain('Mode: seeded');
expect(plainIo.stdout()).toContain('Semantic-layer sources:');
const jsonIo = makeIo();
await expect(
runKloDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, jsonIo.io),
runKtxDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, jsonIo.io),
).resolves.toBe(0);
const parsed = JSON.parse(jsonIo.stdout());
expect(parsed).toMatchObject({
@ -347,7 +347,7 @@ describe('runKloDemo', () => {
generatedContext: 'prebuilt from bundled assets',
llmCalls: 'none',
},
nextCommands: KLO_NEXT_STEP_COMMANDS,
nextCommands: KTX_NEXT_STEP_COMMANDS,
});
expect(parsed.generatedOutputs.replays.fileCount).toBeGreaterThanOrEqual(3);
expect(jsonIo.stderr()).toBe('');
@ -359,7 +359,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io, {
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io, {
env: {},
runFullDemo,
}),
@ -372,10 +372,10 @@ describe('runKloDemo', () => {
onMemoryFlowChange: expect.any(Function),
}),
);
expect(testIo.stdout()).toContain('KLO memory flow orbit_demo/live-database done');
expect(testIo.stdout()).toContain('KTX memory flow orbit_demo/live-database done');
expect(testIo.stdout()).toContain('Full demo ingest: done');
expect(testIo.stdout()).toContain('Next: klo setup demo inspect');
expect(testIo.stdout()).toContain('Shows the files, semantic-layer sources, and memory KLO just produced.');
expect(testIo.stdout()).toContain('Next: ktx setup demo inspect');
expect(testIo.stdout()).toContain('Shows the files, semantic-layer sources, and memory KTX just produced.');
});
it('streams live memory-flow snapshots for full demo viz and then prints final summary', async () => {
@ -399,7 +399,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
prompts: createTestDemoPromptAdapter({ choices: ['reuse'] }),
runFullDemo,
@ -411,12 +411,12 @@ describe('runKloDemo', () => {
expect(liveSession.update).toHaveBeenCalledTimes(1);
expect(liveSession.close).toHaveBeenCalledTimes(1);
expect(testIo.stdout()).not.toContain('Memory-flow summary: done');
expect(testIo.stdout()).toContain('KLO finished ingesting your data');
expect(testIo.stdout()).toContain('klo sl list');
expect(testIo.stdout()).toContain('klo wiki list');
expect(testIo.stdout()).toContain('klo serve --mcp stdio --user-id local');
expect(testIo.stdout()).not.toContain(['klo', 'ask'].join(' '));
expect(testIo.stdout()).not.toContain(['klo', 'mcp'].join(' '));
expect(testIo.stdout()).toContain('KTX finished ingesting your data');
expect(testIo.stdout()).toContain('ktx sl list');
expect(testIo.stdout()).toContain('ktx wiki list');
expect(testIo.stdout()).toContain('ktx serve --mcp stdio --user-id local');
expect(testIo.stdout()).not.toContain(['ktx', 'ask'].join(' '));
expect(testIo.stdout()).not.toContain(['ktx', 'mcp'].join(' '));
});
it('uses plain progress for full demo viz when stdin raw mode is unavailable', async () => {
@ -440,7 +440,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
prompts: createTestDemoPromptAdapter({ choices: ['reuse'] }),
runFullDemo,
@ -456,7 +456,7 @@ describe('runKloDemo', () => {
);
expect(testIo.stdout()).toContain('[connect] Connected live-database - 7 database files (demo_full)');
expect(testIo.stdout()).toContain('Full demo ingest: done');
expect(testIo.stdout()).not.toContain('KLO memory flow');
expect(testIo.stdout()).not.toContain('KTX memory flow');
expect(testIo.stderr()).toContain(
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
);
@ -486,7 +486,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{ env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, runFullDemo }, // pragma: allowlist secret
@ -510,7 +510,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
testIo.io,
{ env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, runFullDemo }, // pragma: allowlist secret
@ -526,7 +526,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'ingest', mode: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{ env: {}, runFullDemo },
@ -537,12 +537,12 @@ describe('runKloDemo', () => {
});
it('saves full-demo replay output for the next demo replay command', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-full-replay-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-full-replay-'));
await ensureDemoProject({ projectDir: tempDir, force: false });
const io = makeIo();
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
io.io,
{
@ -554,7 +554,7 @@ describe('runKloDemo', () => {
const replayIo = makeIo();
await expect(
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, replayIo.io),
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, replayIo.io),
).resolves.toBe(0);
expect(JSON.parse(replayIo.stdout())).toMatchObject({
runId: 'run-full',
@ -566,7 +566,7 @@ describe('runKloDemo', () => {
const testIo = makeIo();
await expect(
runKloDemo(
runKtxDemo(
{ command: 'ingest', mode: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
),
@ -581,7 +581,7 @@ describe('runKloDemo', () => {
const runDoctor = vi.fn().mockResolvedValue(0);
await expect(
runKloDemo(
runKtxDemo(
{
command: 'doctor',
projectDir: tempDir,
@ -610,13 +610,13 @@ describe('runKloDemo', () => {
const rejected = makeIo();
await expect(
runKloDemo({ command: 'reset', projectDir: tempDir, force: false, inputMode: 'disabled' }, rejected.io),
runKtxDemo({ command: 'reset', projectDir: tempDir, force: false, inputMode: 'disabled' }, rejected.io),
).resolves.toBe(1);
expect(rejected.stderr()).toContain(`klo setup demo reset is destructive; pass --force to recreate ${tempDir}`);
expect(rejected.stderr()).toContain(`ktx setup demo reset is destructive; pass --force to recreate ${tempDir}`);
const accepted = makeIo();
await expect(
runKloDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, accepted.io),
runKtxDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, accepted.io),
).resolves.toBe(0);
expect(accepted.stdout()).toContain(`Demo project reset: ${tempDir}`);
});
@ -624,12 +624,12 @@ describe('runKloDemo', () => {
it('rehydrates seeded assets after reset --force', async () => {
const resetIo = makeIo();
await expect(
runKloDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, resetIo.io),
runKtxDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, resetIo.io),
).resolves.toBe(0);
const seededIo = makeIo();
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
seededIo.io,
),
@ -648,11 +648,11 @@ describe('runKloDemo', () => {
const testIo = makeIo();
await expect(
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, testIo.io),
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, testIo.io),
).resolves.toBe(1);
expect(testIo.stderr()).toContain(`Demo project is not ready at ${tempDir}: missing demo.db`);
expect(testIo.stderr()).toContain(`klo setup demo reset --project-dir ${tempDir} --force --no-input`);
expect(testIo.stderr()).toContain(`ktx setup demo reset --project-dir ${tempDir} --force --no-input`);
});
it('uses a process-local Anthropic key from the interactive prompt', async () => {
@ -661,7 +661,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
testIo.io,
{
@ -682,7 +682,7 @@ describe('runKloDemo', () => {
onMemoryFlowChange: expect.any(Function),
}),
);
expect(await readFile(join(tempDir, 'klo.yaml'), 'utf-8')).toContain('api_key: env:ANTHROPIC_API_KEY');
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain('api_key: env:ANTHROPIC_API_KEY');
});
it('routes an interactive missing-key choice to seeded mode', async () => {
@ -691,7 +691,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
testIo.io,
{
@ -712,7 +712,7 @@ describe('runKloDemo', () => {
const testIo = makeIo({ isTTY: true });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
testIo.io,
{
@ -733,7 +733,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'viz' },
testIo.io,
{
@ -745,7 +745,7 @@ describe('runKloDemo', () => {
).resolves.toBe(0);
expect(runFullDemo).not.toHaveBeenCalled();
expect(testIo.stdout()).toContain('KLO memory flow');
expect(testIo.stdout()).toContain('KTX memory flow');
expect(testIo.stdout()).toContain('done');
});
});

View file

@ -3,9 +3,9 @@ import {
formatMemoryFlowFinalSummary,
renderMemoryFlowReplay,
type MemoryFlowReplayInput,
} from '@klo/context/ingest/memory-flow';
import { resolveKloConfigReference } from '@klo/context/core';
import { loadKloProject } from '@klo/context/project';
} from '@ktx/context/ingest/memory-flow';
import { resolveKtxConfigReference } from '@ktx/context/core';
import { loadKtxProject } from '@ktx/context/project';
import {
DEMO_ADAPTER,
DEMO_CONNECTION_ID,
@ -34,11 +34,11 @@ import {
resolveFullCredentialDecision,
type DemoPromptAdapter,
} from './demo-interaction.js';
import type { KloDoctorArgs } from './doctor.js';
import type { KtxDoctorArgs } from './doctor.js';
import {
renderMemoryFlowTui,
startLiveMemoryFlowTui,
type KloMemoryFlowTuiIo,
type KtxMemoryFlowTuiIo,
type MemoryFlowTuiLiveSession,
} from './memory-flow-tui.js';
import {
@ -51,36 +51,36 @@ import { formatNextStepLines } from './next-steps.js';
profileMark('module:demo');
export type KloDemoOutputMode = 'plain' | 'json' | 'viz';
export type KloDemoInputMode = 'auto' | 'disabled';
export type KloDemoMode = 'full' | 'seeded';
export type KtxDemoOutputMode = 'plain' | 'json' | 'viz';
export type KtxDemoInputMode = 'auto' | 'disabled';
export type KtxDemoMode = 'full' | 'seeded';
export type KloDemoArgs =
| { command: 'init'; projectDir: string; force: boolean; inputMode?: KloDemoInputMode }
| { command: 'reset'; projectDir: string; force: boolean; inputMode?: KloDemoInputMode }
| { command: 'replay'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
| { command: 'scan'; projectDir: string; inputMode?: KloDemoInputMode }
| { command: 'inspect'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
| { command: 'doctor'; projectDir: string; outputMode: Exclude<KloDemoOutputMode, 'viz'>; inputMode?: KloDemoInputMode }
| { command: 'seeded'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
| { command: 'full'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
export type KtxDemoArgs =
| { command: 'init'; projectDir: string; force: boolean; inputMode?: KtxDemoInputMode }
| { command: 'reset'; projectDir: string; force: boolean; inputMode?: KtxDemoInputMode }
| { command: 'replay'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
| { command: 'scan'; projectDir: string; inputMode?: KtxDemoInputMode }
| { command: 'inspect'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
| { command: 'doctor'; projectDir: string; outputMode: Exclude<KtxDemoOutputMode, 'viz'>; inputMode?: KtxDemoInputMode }
| { command: 'seeded'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
| { command: 'full'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
| {
command: 'ingest';
mode: KloDemoMode;
mode: KtxDemoMode;
projectDir: string;
outputMode: KloDemoOutputMode;
inputMode?: KloDemoInputMode;
outputMode: KtxDemoOutputMode;
inputMode?: KtxDemoInputMode;
};
export interface KloDemoIo {
stdin?: KloMemoryFlowTuiIo['stdin'];
export interface KtxDemoIo {
stdin?: KtxMemoryFlowTuiIo['stdin'];
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
stderr: { write(chunk: string): void };
}
interface KloDemoDeps {
interface KtxDemoDeps {
runFullDemo?: typeof runDemoFull;
runDoctor?: (args: KloDoctorArgs, io: KloDemoIo) => Promise<number>;
runDoctor?: (args: KtxDoctorArgs, io: KtxDemoIo) => Promise<number>;
renderStoredMemoryFlow?: typeof renderMemoryFlowTui;
startLiveMemoryFlow?: typeof startLiveMemoryFlowTui;
env?: NodeJS.ProcessEnv;
@ -127,7 +127,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
}
}
const lines: string[] = ['', '★ KLO finished ingesting your data', ''];
const lines: string[] = ['', '★ KTX finished ingesting your data', ''];
if (chunkCount > 0) {
lines.push(` ✓ Analyzed ${chunkCount} business area${chunkCount === 1 ? '' : 's'}`);
@ -137,7 +137,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
lines.push('');
if (slCount > 0 || wikiCount > 0) {
lines.push(' KLO created:');
lines.push(' KTX created:');
if (slCount > 0) lines.push(` 📊 ${slCount} query definition${slCount === 1 ? '' : 's'} — so agents can write accurate SQL for your data`);
if (wikiCount > 0) lines.push(` 📝 ${wikiCount} knowledge page${wikiCount === 1 ? '' : 's'} — so agents understand your business context`);
lines.push('');
@ -153,7 +153,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
lines.push(...formatNextStepLines());
if (input.sourceDir) {
lines.push('');
lines.push(` Your KLO project files are at: ${input.sourceDir}`);
lines.push(` Your KTX project files are at: ${input.sourceDir}`);
}
lines.push('');
@ -164,7 +164,7 @@ function formatPlainReplaySummary(input: MemoryFlowReplayInput): string {
return [formatMemoryFlowFinalSummary(input).trimEnd(), '', 'What to do next:', ...formatNextStepLines(), ''].join('\n');
}
function writeReplay(input: MemoryFlowReplayInput, outputMode: KloDemoOutputMode, io: KloDemoIo): void {
function writeReplay(input: MemoryFlowReplayInput, outputMode: KtxDemoOutputMode, io: KtxDemoIo): void {
if (outputMode === 'json') {
io.stdout.write(`${JSON.stringify(input, null, 2)}\n`);
return;
@ -181,10 +181,10 @@ function writeReplay(input: MemoryFlowReplayInput, outputMode: KloDemoOutputMode
async function writeStoredReplay(
input: MemoryFlowReplayInput,
outputMode: KloDemoOutputMode,
inputMode: KloDemoArgs['inputMode'],
io: KloDemoIo,
deps: KloDemoDeps,
outputMode: KtxDemoOutputMode,
inputMode: KtxDemoArgs['inputMode'],
io: KtxDemoIo,
deps: KtxDemoDeps,
env: NodeJS.ProcessEnv,
): Promise<void> {
const resolvedOutputMode = effectiveDemoOutputMode(outputMode, io, env, {
@ -211,8 +211,8 @@ async function writeStoredReplay(
function writeInspect(
summary: Awaited<ReturnType<typeof inspectDemoProject>>,
outputMode: KloDemoOutputMode,
io: KloDemoIo,
outputMode: KtxDemoOutputMode,
io: KtxDemoIo,
): void {
if (outputMode === 'json') {
io.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
@ -224,8 +224,8 @@ function writeInspect(
function writeFullDemo(
result: Awaited<ReturnType<typeof runDemoFull>>,
outputMode: KloDemoOutputMode,
io: KloDemoIo,
outputMode: KtxDemoOutputMode,
io: KtxDemoIo,
options: { liveWasRendered?: boolean; projectDir?: string } = {},
): void {
if (outputMode === 'json') {
@ -274,8 +274,8 @@ function replayWithFullMetadata(result: Awaited<ReturnType<typeof runDemoFull>>)
function pickMemoryFlowProgress(
liveSession: MemoryFlowTuiLiveSession | null,
outputMode: KloDemoOutputMode,
io: KloDemoIo,
outputMode: KtxDemoOutputMode,
io: KtxDemoIo,
): ((snapshot: MemoryFlowReplayInput) => void) | undefined {
if (liveSession) {
return (snapshot: MemoryFlowReplayInput) => {
@ -290,7 +290,7 @@ function pickMemoryFlowProgress(
return createPlainProgressEmitter(io);
}
function isTuiCapableDemoIo(io: KloDemoIo): io is KloDemoIo & KloMemoryFlowTuiIo {
function isTuiCapableDemoIo(io: KtxDemoIo): io is KtxDemoIo & KtxMemoryFlowTuiIo {
return (
io.stdin?.isTTY === true &&
io.stdout.isTTY === true &&
@ -304,11 +304,11 @@ interface EffectiveDemoOutputModeOptions {
}
function effectiveDemoOutputMode(
outputMode: KloDemoOutputMode,
io: KloDemoIo,
outputMode: KtxDemoOutputMode,
io: KtxDemoIo,
env: NodeJS.ProcessEnv,
options: EffectiveDemoOutputModeOptions = {},
): KloDemoOutputMode {
): KtxDemoOutputMode {
if (outputMode !== 'viz') {
return outputMode;
}
@ -346,7 +346,7 @@ async function ensureDemoProjectForCommand(projectDir: string): Promise<void> {
});
}
async function prepareProjectForDemoCommand(args: KloDemoArgs, io: KloDemoIo, deps: KloDemoDeps): Promise<string | null> {
async function prepareProjectForDemoCommand(args: KtxDemoArgs, io: KtxDemoIo, deps: KtxDemoDeps): Promise<string | null> {
if (args.command === 'init' || args.command === 'reset' || args.command === 'doctor') {
return args.projectDir;
}
@ -372,10 +372,10 @@ async function prepareProjectForDemoCommand(args: KloDemoArgs, io: KloDemoIo, de
async function runReplayDemo(
projectDir: string,
outputMode: KloDemoOutputMode,
inputMode: KloDemoArgs['inputMode'],
io: KloDemoIo,
deps: KloDemoDeps,
outputMode: KtxDemoOutputMode,
inputMode: KtxDemoArgs['inputMode'],
io: KtxDemoIo,
deps: KtxDemoDeps,
env: NodeJS.ProcessEnv = process.env,
): Promise<number> {
await ensureDemoProjectForCommand(projectDir);
@ -385,10 +385,10 @@ async function runReplayDemo(
async function runSeededDemo(
projectDir: string,
outputMode: KloDemoOutputMode,
inputMode: KloDemoArgs['inputMode'],
io: KloDemoIo,
deps: KloDemoDeps,
outputMode: KtxDemoOutputMode,
inputMode: KtxDemoArgs['inputMode'],
io: KtxDemoIo,
deps: KtxDemoDeps,
env: NodeJS.ProcessEnv = process.env,
): Promise<number> {
const result = await runDemoSeeded({ projectDir });
@ -411,7 +411,7 @@ async function runSeededDemo(
return 0;
}
export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, deps: KloDemoDeps = {}): Promise<number> {
export async function runKtxDemo(args: KtxDemoArgs, io: KtxDemoIo = process, deps: KtxDemoDeps = {}): Promise<number> {
try {
if (args.command === 'init') {
const result = await ensureDemoProject({ projectDir: args.projectDir, force: args.force });
@ -419,7 +419,7 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
io.stdout.write(`Config: ${result.configPath}\n`);
io.stdout.write(`Database: ${result.databasePath}\n`);
io.stdout.write(`Replay: ${result.replayPath}\n`);
io.stdout.write('Next: klo setup demo --no-input\n');
io.stdout.write('Next: ktx setup demo --no-input\n');
io.stdout.write(' Runs the pre-seeded demo without calling the LLM.\n');
return 0;
}
@ -430,7 +430,7 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
io.stdout.write(`Config: ${result.configPath}\n`);
io.stdout.write(`Database: ${result.databasePath}\n`);
io.stdout.write(`Replay: ${result.replayPath}\n`);
io.stdout.write('Next: klo setup demo --mode full\n');
io.stdout.write('Next: ktx setup demo --mode full\n');
io.stdout.write(' Runs the full AI-backed pass with your LLM provider.\n');
return 0;
}
@ -454,13 +454,13 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
if (args.command === 'full' || (args.command === 'ingest' && args.mode === 'full')) {
const executeFullDemo = deps.runFullDemo ?? runDemoFull;
await ensureDemoProjectForCommand(preparedProjectDir);
const project = await loadKloProject({ projectDir: preparedProjectDir });
const project = await loadKtxProject({ projectDir: preparedProjectDir });
const credentialStatus = fullDemoCredentialStatus(project, env);
const credentialDecision = await resolveFullCredentialDecision({
needsAnthropicKey:
credentialStatus.status === 'missing-anthropic-key' &&
project.config.llm.provider.backend === 'anthropic' &&
!resolveKloConfigReference(project.config.llm.provider.anthropic?.api_key, env),
!resolveKtxConfigReference(project.config.llm.provider.anthropic?.api_key, env),
inputMode: args.inputMode,
io,
env,
@ -523,8 +523,8 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
}
if (args.command === 'doctor') {
const { runKloDoctor } = await import('./doctor.js');
const executeDoctor = deps.runDoctor ?? runKloDoctor;
const { runKtxDoctor } = await import('./doctor.js');
const executeDoctor = deps.runDoctor ?? runKtxDoctor;
return await executeDoctor(
{
command: 'demo',

View file

@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { runKloCli } from './index.js';
import { runKtxCli } from './index.js';
function makeIo() {
let stdout = '';
@ -26,9 +26,9 @@ describe('dev Commander tree', () => {
it('prints visible dev help with only supported low-level command groups', async () => {
const testIo = makeIo();
await expect(runKloCli(['dev', '--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', '--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo dev [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
for (const command of ['init', 'doctor', 'scan', 'ingest', 'mapping']) {
expect(testIo.stdout()).toContain(command);
}
@ -51,10 +51,10 @@ describe('dev Commander tree', () => {
it('keeps dev callable while hiding it from root command rows', async () => {
const testIo = makeIo();
await expect(runKloCli(['--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Advanced:');
expect(testIo.stdout()).toContain('klo dev');
expect(testIo.stdout()).toContain('ktx dev');
expect(testIo.stdout()).not.toContain('dev Low-level diagnostics');
expect(testIo.stderr()).toBe('');
});
@ -63,15 +63,15 @@ describe('dev Commander tree', () => {
const { mkdtemp, readFile, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const tempDir = await mkdtemp(join(tmpdir(), 'klo-dev-init-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-init-'));
const projectDir = join(tempDir, 'warehouse');
const testIo = makeIo();
try {
await expect(runKloCli(['dev', 'init', projectDir, '--name', 'warehouse'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'init', projectDir, '--name', 'warehouse'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain(`Initialized KLO project at ${projectDir}`);
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
expect(testIo.stderr()).toBe('');
} finally {
await rm(tempDir, { recursive: true, force: true });
@ -82,16 +82,16 @@ describe('dev Commander tree', () => {
const { mkdtemp, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const tempDir = await mkdtemp(join(tmpdir(), 'klo-dev-init-global-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-init-global-'));
const projectDir = join(tempDir, 'global-init');
const testIo = makeIo();
try {
await expect(
runKloCli(['--project-dir', projectDir, 'dev', 'init', '--name', 'global-init'], testIo.io),
runKtxCli(['--project-dir', projectDir, 'dev', 'init', '--name', 'global-init'], testIo.io),
).resolves.toBe(0);
expect(testIo.stdout()).toContain(`Initialized KLO project at ${projectDir}`);
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
expect(testIo.stderr()).toBe('');
} finally {
await rm(tempDir, { recursive: true, force: true });
@ -106,7 +106,7 @@ describe('dev Commander tree', () => {
]) {
const testIo = makeIo();
await expect(runKloCli(argv, testIo.io)).resolves.toBe(1);
await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown command|error:/);
}
@ -115,12 +115,12 @@ describe('dev Commander tree', () => {
it.each([
{
argv: ['dev', 'doctor', '--help'],
expected: ['Usage: klo dev doctor', '--json', '--no-input'],
expected: ['Usage: ktx dev doctor', '--json', '--no-input'],
},
{
argv: ['dev', 'scan', '--help'],
expected: [
'Usage: klo dev scan',
'Usage: ktx dev scan',
'--mode <mode>',
'structural',
'relationships',
@ -136,12 +136,12 @@ describe('dev Commander tree', () => {
},
{
argv: ['dev', 'scan', 'report', '--help'],
expected: ['Usage: klo dev scan report [options] <runId>', '<runId>', '--json'],
expected: ['Usage: ktx dev scan report [options] <runId>', '<runId>', '--json'],
},
{
argv: ['dev', 'scan', 'relationships', '--help'],
expected: [
'Usage: klo dev scan relationships [options] <runId>',
'Usage: ktx dev scan relationships [options] <runId>',
'--status <status>',
'--limit <count>',
'--accept <candidateId>',
@ -154,7 +154,7 @@ describe('dev Commander tree', () => {
{
argv: ['dev', 'scan', 'relationship-apply', '--help'],
expected: [
'Usage: klo dev scan relationship-apply [options] <runId>',
'Usage: ktx dev scan relationship-apply [options] <runId>',
'--all-accepted',
'--candidate <candidateId>',
'--dry-run',
@ -163,7 +163,7 @@ describe('dev Commander tree', () => {
{
argv: ['dev', 'scan', 'relationship-thresholds', '--help'],
expected: [
'Usage: klo dev scan relationship-thresholds [options]',
'Usage: ktx dev scan relationship-thresholds [options]',
'--connection <connectionId>',
'--min-total-labels <count>',
'--min-accepted-labels <count>',
@ -174,7 +174,7 @@ describe('dev Commander tree', () => {
{
argv: ['dev', 'scan', 'relationship-feedback', '--help'],
expected: [
'Usage: klo dev scan relationship-feedback [options]',
'Usage: ktx dev scan relationship-feedback [options]',
'--connection <connectionId>',
'--decision <decision>',
'--json',
@ -184,7 +184,7 @@ describe('dev Commander tree', () => {
{
argv: ['dev', 'scan', 'relationship-calibration', '--help'],
expected: [
'Usage: klo dev scan relationship-calibration [options]',
'Usage: ktx dev scan relationship-calibration [options]',
'--connection <connectionId>',
'--decision <decision>',
'--accept-threshold <value>',
@ -194,11 +194,11 @@ describe('dev Commander tree', () => {
},
{
argv: ['dev', 'ingest', 'run', '--help'],
expected: ['Usage: klo dev ingest run [options]', '--connection-id <connectionId>', '--adapter <adapter>'],
expected: ['Usage: ktx dev ingest run [options]', '--connection-id <connectionId>', '--adapter <adapter>'],
},
{
argv: ['dev', 'mapping', 'sync-state', 'set', '--help'],
expected: ['Usage: klo dev mapping sync-state set [options] <connectionId>', '--mode <mode>'],
expected: ['Usage: ktx dev mapping sync-state set [options] <connectionId>', '--mode <mode>'],
},
])('prints generated nested help for $argv', async ({ argv, expected }) => {
const io = makeIo();
@ -206,7 +206,7 @@ describe('dev Commander tree', () => {
const ingest = vi.fn(async () => 0);
const scan = vi.fn(async () => 0);
await expect(runKloCli(argv, io.io, { doctor, ingest, scan })).resolves.toBe(0);
await expect(runKtxCli(argv, io.io, { doctor, ingest, scan })).resolves.toBe(0);
for (const text of expected) {
expect(io.stdout()).toContain(text);
@ -222,7 +222,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--dry-run'], scanIo.io, { scan }),
runKtxCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--dry-run'], scanIo.io, { scan }),
).resolves.toBe(0);
expect(scan).toHaveBeenCalledWith(
@ -245,7 +245,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'], io.io, {
runKtxCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'], io.io, {
scan,
}),
).resolves.toBe(0);
@ -269,7 +269,7 @@ describe('dev Commander tree', () => {
const io = makeIo();
const scan = vi.fn(async () => 0);
await expect(runKloCli(['dev', 'scan', 'warehouse', option], io.io, { scan })).resolves.toBe(1);
await expect(runKtxCli(['dev', 'scan', 'warehouse', option], io.io, { scan })).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
expect(io.stderr()).toContain(`unknown option '${option}'`);
@ -279,18 +279,18 @@ describe('dev Commander tree', () => {
const io = makeIo();
const scan = vi.fn(async () => 0);
await expect(runKloCli(['dev', 'scan', '--dry-run'], io.io, { scan })).resolves.toBe(1);
await expect(runKtxCli(['dev', 'scan', '--dry-run'], io.io, { scan })).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
expect(io.stdout()).toContain('Usage: klo dev scan');
expect(io.stderr()).toContain('klo dev scan requires <connectionId> or a subcommand');
expect(io.stdout()).toContain('Usage: ktx dev scan');
expect(io.stderr()).toContain('ktx dev scan requires <connectionId> or a subcommand');
});
it('rejects invalid scan modes before dispatch', async () => {
const io = makeIo();
const scan = vi.fn(async () => 0);
await expect(runKloCli(['dev', 'scan', 'warehouse', '--mode', 'deep'], io.io, { scan })).resolves.toBe(1);
await expect(runKtxCli(['dev', 'scan', 'warehouse', '--mode', 'deep'], io.io, { scan })).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
expect(io.stderr()).toContain("argument 'deep' is invalid");
@ -301,10 +301,10 @@ describe('dev Commander tree', () => {
const io = makeIo();
const scan = vi.fn(async () => 0);
await expect(runKloCli(['dev', 'scan', 'report', '--help'], io.io, { scan })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'scan', 'report', '--help'], io.io, { scan })).resolves.toBe(0);
expect(io.stdout()).toContain('--project-dir is inherited from `klo dev scan`');
expect(io.stdout()).not.toContain('--project-dir is inherited from `klo scan`');
expect(io.stdout()).toContain('--project-dir is inherited from `ktx dev scan`');
expect(io.stdout()).not.toContain('--project-dir is inherited from `ktx scan`');
expect(scan).not.toHaveBeenCalled();
});
@ -314,10 +314,10 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'report', 'scan-run-1', '--project-dir', '/tmp/project'], humanIo.io, { scan }),
runKtxCli(['dev', 'scan', 'report', 'scan-run-1', '--project-dir', '/tmp/project'], humanIo.io, { scan }),
).resolves.toBe(0);
await expect(
runKloCli(['dev', 'scan', 'report', 'scan-run-2', '--project-dir', '/tmp/project', '--json'], jsonIo.io, {
runKtxCli(['dev', 'scan', 'report', 'scan-run-2', '--project-dir', '/tmp/project', '--json'], jsonIo.io, {
scan,
}),
).resolves.toBe(0);
@ -339,7 +339,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -377,7 +377,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -419,7 +419,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'relationships', 'scan-run-review', option, ''], io.io, { scan }),
runKtxCli(['dev', 'scan', 'relationships', 'scan-run-review', option, ''], io.io, { scan }),
).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
@ -431,7 +431,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'relationship-feedback', '--json', '--jsonl'], io.io, { scan }),
runKtxCli(['dev', 'scan', 'relationship-feedback', '--json', '--jsonl'], io.io, { scan }),
).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
@ -443,7 +443,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -480,7 +480,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -516,7 +516,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -557,7 +557,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -598,7 +598,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'relationship-calibration', '--accept-threshold', '1.5'], io.io, { scan }),
runKtxCli(['dev', 'scan', 'relationship-calibration', '--accept-threshold', '1.5'], io.io, { scan }),
).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
@ -610,7 +610,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -635,7 +635,7 @@ describe('dev Commander tree', () => {
const ingest = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'ingest',

View file

@ -1,6 +1,6 @@
import { resolve } from 'node:path';
import type { Command } from '@commander-js/extra-typings';
import { type CommandWithGlobalOptions, type KloCliCommandContext, resolveCommandProjectDir } from './cli-program.js';
import { type CommandWithGlobalOptions, type KtxCliCommandContext, resolveCommandProjectDir } from './cli-program.js';
import { registerCompletionCommands } from './commands/completion-commands.js';
import { registerConnectionMappingCommands } from './commands/connection-commands.js';
import { registerDoctorCommands } from './commands/doctor-commands.js';
@ -10,7 +10,7 @@ import { profileMark } from './startup-profile.js';
profileMark('module:dev');
export function registerDevCommands(program: Command, context: KloCliCommandContext): void {
export function registerDevCommands(program: Command, context: KtxCliCommandContext): void {
const dev = program
.command('dev', { hidden: true })
.description('Low-level diagnostics, scans, adapter commands, and mapping tools')
@ -27,10 +27,10 @@ export function registerDevCommands(program: Command, context: KloCliCommandCont
dev
.command('init')
.description('Initialize a Git-backed KLO project directory for maintenance scripts')
.description('Initialize a Git-backed KTX project directory for maintenance scripts')
.argument('[directory]', 'Project directory')
.option('--name <name>', 'Project name written to klo.yaml')
.option('--force', 'Rewrite klo.yaml and scaffold files in an existing project', false)
.option('--name <name>', 'Project name written to ktx.yaml')
.option('--force', 'Rewrite ktx.yaml and scaffold files in an existing project', false)
.action(
async (
projectDir: string | undefined,

View file

@ -2,10 +2,10 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { KloEmbeddingConfig, KloEmbeddingHealthCheckOptions, KloEmbeddingHealthCheckResult } from '@klo/llm';
import type { KtxEmbeddingConfig, KtxEmbeddingHealthCheckOptions, KtxEmbeddingHealthCheckResult } from '@ktx/llm';
import {
formatDoctorReport,
runKloDoctor,
runKtxDoctor,
runSetupDoctorChecks,
type DoctorCheck,
} from './doctor.js';
@ -32,13 +32,13 @@ function makeIo() {
}
type EmbeddingHealthCheck = (
config: KloEmbeddingConfig,
options?: KloEmbeddingHealthCheckOptions,
) => Promise<KloEmbeddingHealthCheckResult>;
config: KtxEmbeddingConfig,
options?: KtxEmbeddingHealthCheckOptions,
) => Promise<KtxEmbeddingHealthCheckResult>;
async function writeProjectConfig(projectDir: string, embeddingLines: string[]): Promise<void> {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -69,9 +69,9 @@ describe('formatDoctorReport', () => {
},
];
expect(formatDoctorReport({ title: 'KLO setup doctor', checks })).toBe(
expect(formatDoctorReport({ title: 'KTX setup doctor', checks })).toBe(
[
'KLO setup doctor',
'KTX setup doctor',
'PASS Node 22+: v22.16.0 ABI 127',
'FAIL Native SQLite: Cannot load better-sqlite3',
' Fix: Run: pnpm run native:rebuild',
@ -85,12 +85,12 @@ describe('runSetupDoctorChecks', () => {
it('returns pass checks when injected commands and file checks succeed', async () => {
const checks = await runSetupDoctorChecks({
env: { PATH: '/bin' },
workspaceRoot: '/workspace/klo',
workspaceRoot: '/workspace/ktx',
execText: async (command, args) => {
if (command === 'pnpm' && args[0] === '--version') return '10.28.0';
if (command === 'corepack' && args[0] === '--version') return '0.32.0';
if (command === 'uv' && args[0] === '--version') return 'uv 0.9.5';
if (command === process.execPath && args.includes('--version')) return '@klo/cli 0.0.0-private';
if (command === process.execPath && args.includes('--version')) return '@ktx/cli 0.0.0-private';
throw new Error(`${command} ${args.join(' ')}`);
},
pathExists: async () => true,
@ -111,7 +111,7 @@ describe('runSetupDoctorChecks', () => {
it('returns exact fixes when setup checks fail', async () => {
const checks = await runSetupDoctorChecks({
env: {},
workspaceRoot: '/workspace/klo',
workspaceRoot: '/workspace/ktx',
execText: async (command) => {
throw new Error(`${command} not found`);
},
@ -140,12 +140,12 @@ describe('runSetupDoctorChecks', () => {
it('treats missing corepack as a warning so setup doctor can still pass', async () => {
const checks = await runSetupDoctorChecks({
env: { PATH: '/bin' },
workspaceRoot: '/workspace/klo',
workspaceRoot: '/workspace/ktx',
execText: async (command, args) => {
if (command === 'pnpm' && args[0] === '--version') return '10.28.0';
if (command === 'corepack' && args[0] === '--version') throw new Error('spawn corepack ENOENT');
if (command === 'uv' && args[0] === '--version') return 'uv 0.9.5';
if (command === process.execPath && args.includes('--version')) return '@klo/cli 0.0.0-private';
if (command === process.execPath && args.includes('--version')) return '@ktx/cli 0.0.0-private';
throw new Error(`${command} ${args.join(' ')}`);
},
pathExists: async () => true,
@ -154,7 +154,7 @@ describe('runSetupDoctorChecks', () => {
const testIo = makeIo();
await expect(
runKloDoctor({ command: 'setup', outputMode: 'plain', inputMode: 'disabled' }, testIo.io, {
runKtxDoctor({ command: 'setup', outputMode: 'plain', inputMode: 'disabled' }, testIo.io, {
runSetupChecks: async () => checks,
}),
).resolves.toBe(0);
@ -171,11 +171,11 @@ describe('runSetupDoctorChecks', () => {
});
});
describe('runKloDoctor', () => {
describe('runKtxDoctor', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-doctor-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-doctor-'));
});
afterEach(async () => {
@ -186,7 +186,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'setup', outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -204,7 +204,7 @@ describe('runKloDoctor', () => {
),
).resolves.toBe(1);
expect(testIo.stdout()).toContain('KLO setup doctor');
expect(testIo.stdout()).toContain('KTX setup doctor');
expect(testIo.stdout()).toContain('FAIL TypeScript package build: Missing packages/cli/dist/bin.js');
expect(testIo.stdout()).toContain('Fix: Run: pnpm run build');
expect(testIo.stderr()).toBe('');
@ -214,7 +214,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'setup', outputMode: 'json', inputMode: 'disabled' },
testIo.io,
{
@ -226,14 +226,14 @@ describe('runKloDoctor', () => {
).resolves.toBe(0);
expect(JSON.parse(testIo.stdout())).toEqual({
title: 'KLO setup doctor',
title: 'KTX setup doctor',
checks: [{ id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0 ABI 127' }],
});
});
it('runs project checks against a valid klo.yaml', async () => {
it('runs project checks against a valid ktx.yaml', async () => {
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -250,7 +250,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -261,14 +261,14 @@ describe('runKloDoctor', () => {
),
).resolves.toBe(0);
expect(testIo.stdout()).toContain('KLO project doctor');
expect(testIo.stdout()).toContain('KTX project doctor');
expect(testIo.stdout()).toContain('PASS Project config: warehouse');
expect(testIo.stdout()).toContain('PASS Connections: 1 configured');
});
it('includes Postgres historic-SQL readiness in project doctor output', async () => {
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -295,12 +295,12 @@ describe('runKloDoctor', () => {
status: 'warn' as const,
detail:
'pg_stat_statements ready (PostgreSQL 16.4) with warnings: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
fix: `Update the Postgres parameter group or config, then rerun \`klo dev doctor --project-dir ${tempDir}\``,
fix: `Update the Postgres parameter group or config, then rerun \`ktx dev doctor --project-dir ${tempDir}\``,
},
]);
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -322,7 +322,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -338,7 +338,7 @@ describe('runKloDoctor', () => {
'Semantic lane will be skipped; lexical, dictionary, and token lanes remain available.',
);
expect(testIo.stdout()).toContain(
`Fix: Run: klo setup --project-dir ${tempDir} --no-input`,
`Fix: Run: ktx setup --project-dir ${tempDir} --no-input`,
);
});
@ -355,7 +355,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -375,7 +375,7 @@ describe('runKloDoctor', () => {
dimensions: 384,
sentenceTransformers: { baseURL: 'http://127.0.0.1:8765', pathPrefix: '' },
},
{ text: 'KLO semantic search doctor probe', timeoutMs: 1234 },
{ text: 'KTX semantic search doctor probe', timeoutMs: 1234 },
);
expect(testIo.stdout()).toContain(
'PASS Semantic search embeddings: sentence-transformers/all-MiniLM-L6-v2 (384d) probe succeeded',
@ -395,7 +395,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -413,7 +413,7 @@ describe('runKloDoctor', () => {
model: 'all-MiniLM-L6-v2',
dimensions: 384,
}),
{ text: 'KLO semantic search doctor probe', timeoutMs: 120_000 },
{ text: 'KTX semantic search doctor probe', timeoutMs: 120_000 },
);
});
@ -433,7 +433,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
testIo.io,
{
@ -454,7 +454,7 @@ describe('runKloDoctor', () => {
status: 'warn',
detail:
'sentence-transformers/all-MiniLM-L6-v2 (384d) probe failed: connect ECONNREFUSED 127.0.0.1:8765. Semantic lane will be skipped; lexical, dictionary, and token lanes remain available.',
fix: `Run: klo setup --project-dir ${tempDir} --no-input`,
fix: `Run: ktx setup --project-dir ${tempDir} --no-input`,
});
});
});

View file

@ -4,15 +4,15 @@ import { access } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import type { KloLocalProject, KloProjectEmbeddingConfig } from '@klo/context/project';
import type { KloEmbeddingConfig, KloEmbeddingHealthCheckOptions, KloEmbeddingHealthCheckResult } from '@klo/llm';
import type { KtxLocalProject, KtxProjectEmbeddingConfig } from '@ktx/context/project';
import type { KtxEmbeddingConfig, KtxEmbeddingHealthCheckOptions, KtxEmbeddingHealthCheckResult } from '@ktx/llm';
import type { HistoricSqlDoctorDeps } from './historic-sql-doctor.js';
const execFileAsync = promisify(execFile);
type DoctorStatus = 'pass' | 'warn' | 'fail';
type KloDoctorOutputMode = 'plain' | 'json';
type KloDoctorInputMode = 'auto' | 'disabled';
type KtxDoctorOutputMode = 'plain' | 'json';
type KtxDoctorInputMode = 'auto' | 'disabled';
export interface DoctorCheck {
id: string;
@ -27,12 +27,12 @@ interface DoctorReport {
checks: DoctorCheck[];
}
export type KloDoctorArgs =
| { command: 'setup'; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode }
| { command: 'project'; projectDir: string; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode }
| { command: 'demo'; projectDir: string; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode };
export type KtxDoctorArgs =
| { command: 'setup'; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode }
| { command: 'project'; projectDir: string; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode }
| { command: 'demo'; projectDir: string; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode };
interface KloDoctorIo {
interface KtxDoctorIo {
stdout: { write(chunk: string): void };
stderr: { write(chunk: string): void };
}
@ -46,9 +46,9 @@ interface SetupDoctorDeps {
}
type EmbeddingHealthCheck = (
config: KloEmbeddingConfig,
options?: KloEmbeddingHealthCheckOptions,
) => Promise<KloEmbeddingHealthCheckResult>;
config: KtxEmbeddingConfig,
options?: KtxEmbeddingHealthCheckOptions,
) => Promise<KtxEmbeddingHealthCheckResult>;
interface SemanticSearchDoctorDeps {
env?: NodeJS.ProcessEnv;
@ -56,9 +56,9 @@ interface SemanticSearchDoctorDeps {
embeddingProbeTimeoutMs?: number;
}
interface KloDoctorDeps extends SemanticSearchDoctorDeps, HistoricSqlDoctorDeps {
interface KtxDoctorDeps extends SemanticSearchDoctorDeps, HistoricSqlDoctorDeps {
runSetupChecks?: () => Promise<DoctorCheck[]>;
runHistoricSqlDoctorChecks?: (project: KloLocalProject, deps: HistoricSqlDoctorDeps) => Promise<DoctorCheck[]>;
runHistoricSqlDoctorChecks?: (project: KtxLocalProject, deps: HistoricSqlDoctorDeps) => Promise<DoctorCheck[]>;
}
function workspaceRootDir(): string {
@ -119,18 +119,18 @@ function check(status: DoctorStatus, id: string, label: string, detail: string,
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
}
const SEMANTIC_SEARCH_HEALTH_TEXT = 'KLO semantic search doctor probe';
const SEMANTIC_SEARCH_HEALTH_TEXT = 'KTX semantic search doctor probe';
const SEMANTIC_SEARCH_HEALTH_TIMEOUT_MS = 5_000;
const SEMANTIC_SEARCH_LOCAL_HEALTH_TIMEOUT_MS = 120_000;
function semanticEmbeddingSetupFix(projectDir: string, backend: KloProjectEmbeddingConfig['backend']): string {
function semanticEmbeddingSetupFix(projectDir: string, backend: KtxProjectEmbeddingConfig['backend']): string {
if (backend === 'openai') {
return `Set OPENAI_API_KEY or rerun: klo setup --project-dir ${projectDir} --embedding-backend openai --no-input`;
return `Set OPENAI_API_KEY or rerun: ktx setup --project-dir ${projectDir} --embedding-backend openai --no-input`;
}
return `Run: klo setup --project-dir ${projectDir} --no-input`;
return `Run: ktx setup --project-dir ${projectDir} --no-input`;
}
function embeddingConfigLabel(config: KloProjectEmbeddingConfig | KloEmbeddingConfig): string {
function embeddingConfigLabel(config: KtxProjectEmbeddingConfig | KtxEmbeddingConfig): string {
const model = config.model?.trim() || 'model not configured';
return `${config.backend}/${model} (${config.dimensions}d)`;
}
@ -140,15 +140,15 @@ function semanticLaneFallbackDetail(reason: string): string {
}
async function defaultEmbeddingHealthCheck(
config: KloEmbeddingConfig,
options?: KloEmbeddingHealthCheckOptions,
): Promise<KloEmbeddingHealthCheckResult> {
const { runKloEmbeddingHealthCheck } = await import('@klo/llm');
return runKloEmbeddingHealthCheck(config, options);
config: KtxEmbeddingConfig,
options?: KtxEmbeddingHealthCheckOptions,
): Promise<KtxEmbeddingHealthCheckResult> {
const { runKtxEmbeddingHealthCheck } = await import('@ktx/llm');
return runKtxEmbeddingHealthCheck(config, options);
}
async function runSemanticSearchEmbeddingCheck(
config: KloProjectEmbeddingConfig,
config: KtxProjectEmbeddingConfig,
projectDir: string,
deps: SemanticSearchDoctorDeps = {},
): Promise<DoctorCheck> {
@ -163,8 +163,8 @@ async function runSemanticSearchEmbeddingCheck(
}
try {
const { resolveLocalKloEmbeddingConfig } = await import('@klo/context');
const resolved = resolveLocalKloEmbeddingConfig(config, deps.env ?? process.env);
const { resolveLocalKtxEmbeddingConfig } = await import('@ktx/context');
const resolved = resolveLocalKtxEmbeddingConfig(config, deps.env ?? process.env);
if (!resolved) {
return check(
'warn',
@ -300,7 +300,7 @@ export async function runSetupDoctorChecks(deps: SetupDoctorDeps = {}): Promise<
'workspace-cli',
'Workspace-local CLI',
failureMessage(error),
'Run: pnpm run build && pnpm run klo -- --version',
'Run: pnpm run build && pnpm run ktx -- --version',
),
);
}
@ -308,11 +308,11 @@ export async function runSetupDoctorChecks(deps: SetupDoctorDeps = {}): Promise<
return checks;
}
async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): Promise<DoctorCheck[]> {
const { loadKloProject } = await import('@klo/context/project');
async function runProjectChecks(projectDir: string, deps: KtxDoctorDeps = {}): Promise<DoctorCheck[]> {
const { loadKtxProject } = await import('@ktx/context/project');
const checks: DoctorCheck[] = [];
try {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
checks.push(check('pass', 'project-config', 'Project config', project.config.project));
const connectionCount = Object.keys(project.config.connections).length;
checks.push(
@ -323,7 +323,7 @@ async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): P
'connections',
'Connections',
'0 configured',
'Add a connection to klo.yaml or run `klo setup demo init`',
'Add a connection to ktx.yaml or run `ktx setup demo init`',
),
);
checks.push(check('pass', 'storage', 'Storage', `${project.config.storage.state}/${project.config.storage.search}`));
@ -339,20 +339,20 @@ async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): P
'project-config',
'Project config',
failureMessage(error),
`Run: klo init ${projectDir} --name <project-name>`,
`Run: ktx init ${projectDir} --name <project-name>`,
),
);
}
return checks;
}
async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): Promise<DoctorCheck[]> {
async function runDemoProjectChecks(projectDir: string, deps: KtxDoctorDeps = {}): Promise<DoctorCheck[]> {
const env = deps.env ?? process.env;
const { DEMO_CONNECTION_ID, DEMO_REPLAY_FILE } = await import('./demo-assets.js');
const { loadKloProject } = await import('@klo/context/project');
const { loadKtxProject } = await import('@ktx/context/project');
const checks: DoctorCheck[] = [];
const requiredPaths = [
['demo-config', 'Demo config', 'klo.yaml'],
['demo-config', 'Demo config', 'ktx.yaml'],
['demo-database', 'Demo dataset', 'demo.db'],
['demo-state', 'Demo state database', 'state.sqlite'],
['demo-replay', 'Demo replay', join('replays', DEMO_REPLAY_FILE)],
@ -371,13 +371,13 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
id,
label,
`Missing ${relativePath}`,
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
),
);
}
try {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
const connection = project.config.connections[DEMO_CONNECTION_ID];
checks.push(
connection?.driver === 'sqlite'
@ -387,7 +387,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
'demo-connection',
'Demo connection',
`${DEMO_CONNECTION_ID} is missing or is not sqlite`,
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
),
);
const provider = project.config.llm.provider.backend;
@ -399,7 +399,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
'demo-llm-provider',
'Demo LLM provider',
provider,
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
),
);
if (provider === 'anthropic' && !env.ANTHROPIC_API_KEY) {
@ -409,7 +409,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
'anthropic-credentials',
'Anthropic credentials',
'ANTHROPIC_API_KEY is not set',
'Export ANTHROPIC_API_KEY to run `klo setup demo --mode full --no-input`',
'Export ANTHROPIC_API_KEY to run `ktx setup demo --mode full --no-input`',
),
);
} else {
@ -426,7 +426,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
'demo-config-parse',
'Demo config parse',
failureMessage(error),
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
),
);
}
@ -450,7 +450,7 @@ function hasFailures(report: DoctorReport): boolean {
return report.checks.some((item) => item.status === 'fail');
}
function writeReport(report: DoctorReport, outputMode: KloDoctorOutputMode, io: KloDoctorIo): void {
function writeReport(report: DoctorReport, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void {
if (outputMode === 'json') {
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
return;
@ -458,24 +458,24 @@ function writeReport(report: DoctorReport, outputMode: KloDoctorOutputMode, io:
io.stdout.write(formatDoctorReport(report));
}
export async function runKloDoctor(
args: KloDoctorArgs,
io: KloDoctorIo = process,
deps: KloDoctorDeps = {},
export async function runKtxDoctor(
args: KtxDoctorArgs,
io: KtxDoctorIo = process,
deps: KtxDoctorDeps = {},
): Promise<number> {
try {
const runSetupChecks = deps.runSetupChecks ?? (() => runSetupDoctorChecks());
const setupChecks = await runSetupChecks();
const report: DoctorReport =
args.command === 'setup'
? { title: 'KLO setup doctor', checks: setupChecks }
? { title: 'KTX setup doctor', checks: setupChecks }
: args.command === 'demo'
? {
title: 'KLO demo doctor',
title: 'KTX demo doctor',
checks: [...setupChecks, ...(await runDemoProjectChecks(args.projectDir, deps))],
}
: {
title: 'KLO project doctor',
title: 'KTX project doctor',
checks: [...setupChecks, ...(await runProjectChecks(args.projectDir, deps))],
};

View file

@ -70,7 +70,7 @@ describe('standalone local warehouse example', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-example-smoke-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-example-smoke-'));
});
afterEach(async () => {
@ -128,14 +128,14 @@ describe('standalone local warehouse example', () => {
]);
expect(ingest).toMatchObject({ code: 1, stdout: '' });
expect(ingest.stderr).toContain(
'klo dev ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner',
'ktx dev ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner',
);
}, 30_000);
it('serves local wiki and semantic-layer MCP tools against the copied example project', async () => {
const projectDir = await copyExampleProject(tempDir);
const client = new Client({ name: 'klo-example-client', version: '0.0.0' });
const client = new Client({ name: 'ktx-example-client', version: '0.0.0' });
const transport = new StdioClientTransport({
command: process.execPath,
args: [CLI_BIN, 'serve', '--mcp', 'stdio', '--project-dir', projectDir, '--user-id', 'example-user'],

View file

@ -1,5 +1,5 @@
import { buildDefaultKloProjectConfig, type KloProjectConnectionConfig } from '@klo/context/project';
import { HistoricSqlExtensionMissingError } from '@klo/context/ingest';
import { buildDefaultKtxProjectConfig, type KtxProjectConnectionConfig } from '@ktx/context/project';
import { HistoricSqlExtensionMissingError } from '@ktx/context/ingest';
import { describe, expect, it, vi } from 'vitest';
import {
runPostgresHistoricSqlDoctorChecks,
@ -7,14 +7,14 @@ import {
type PostgresHistoricSqlDoctorProbe,
} from './historic-sql-doctor.js';
function projectWithConnections(connections: Record<string, KloProjectConnectionConfig>): HistoricSqlDoctorProject {
function projectWithConnections(connections: Record<string, KtxProjectConnectionConfig>): HistoricSqlDoctorProject {
return {
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
config: {
...buildDefaultKloProjectConfig('warehouse'),
...buildDefaultKtxProjectConfig('warehouse'),
connections,
ingest: {
...buildDefaultKloProjectConfig('warehouse').ingest,
...buildDefaultKtxProjectConfig('warehouse').ingest,
adapters: ['live-database', 'historic-sql'],
},
},
@ -61,7 +61,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
);
expect(probe).toHaveBeenCalledWith({
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
connectionId: 'warehouse',
connection: {
driver: 'postgres',
@ -108,7 +108,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
status: 'warn',
detail:
'pg_stat_statements ready (PostgreSQL 16.4) with warnings: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
fix: 'Update the Postgres parameter group or config, then rerun `klo dev doctor --project-dir /tmp/klo-project`',
fix: 'Update the Postgres parameter group or config, then rerun `ktx dev doctor --project-dir /tmp/ktx-project`',
},
]);
});

View file

@ -1,15 +1,15 @@
import type { KloProjectConfig, KloProjectConnectionConfig } from '@klo/context/project';
import type { KtxProjectConfig, KtxProjectConnectionConfig } from '@ktx/context/project';
import type { DoctorCheck } from './doctor.js';
export interface HistoricSqlDoctorProject {
projectDir: string;
config: Pick<KloProjectConfig, 'connections' | 'ingest'>;
config: Pick<KtxProjectConfig, 'connections' | 'ingest'>;
}
export interface PostgresHistoricSqlDoctorProbeInput {
projectDir: string;
connectionId: string;
connection: KloProjectConnectionConfig;
connection: KtxProjectConnectionConfig;
env: NodeJS.ProcessEnv;
}
@ -31,19 +31,19 @@ function check(status: DoctorCheck['status'], id: string, label: string, detail:
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
}
function historicSqlRecord(connection: KloProjectConnectionConfig): Record<string, unknown> | null {
function historicSqlRecord(connection: KtxProjectConnectionConfig): Record<string, unknown> | null {
const historicSql = connection.historicSql;
return historicSql && typeof historicSql === 'object' && !Array.isArray(historicSql)
? (historicSql as Record<string, unknown>)
: null;
}
function isEnabledPostgresHistoricSql(connection: KloProjectConnectionConfig): boolean {
function isEnabledPostgresHistoricSql(connection: KtxProjectConnectionConfig): boolean {
const historicSql = historicSqlRecord(connection);
return historicSql?.enabled === true && historicSql.dialect === 'postgres';
}
function isPostgresDriver(connection: KloProjectConnectionConfig): boolean {
function isPostgresDriver(connection: KtxProjectConnectionConfig): boolean {
const driver = String(connection.driver ?? '').toLowerCase();
return driver === 'postgres' || driver === 'postgresql';
}
@ -62,7 +62,7 @@ function capabilityFailureFix(error: unknown, connectionId: string, projectDir:
if (error instanceof Error && error.name === 'HistoricSqlVersionUnsupportedError') {
return 'Use PostgreSQL 14 or newer, or disable historicSql for this connection';
}
return `Fix connections.${connectionId} Postgres settings, then rerun \`klo dev doctor --project-dir ${projectDir}\``;
return `Fix connections.${connectionId} Postgres settings, then rerun \`ktx dev doctor --project-dir ${projectDir}\``;
}
function failureDetail(error: unknown): string {
@ -75,14 +75,14 @@ function failureDetail(error: unknown): string {
async function defaultPostgresHistoricSqlProbe(
input: PostgresHistoricSqlDoctorProbeInput,
): Promise<PostgresHistoricSqlDoctorProbeResult> {
const [{ PostgresPgssQueryHistoryReader }, { KloPostgresHistoricSqlQueryClient, isKloPostgresConnectionConfig }] =
await Promise.all([import('@klo/context/ingest'), import('@klo/connector-postgres')]);
const [{ PostgresPgssQueryHistoryReader }, { KtxPostgresHistoricSqlQueryClient, isKtxPostgresConnectionConfig }] =
await Promise.all([import('@ktx/context/ingest'), import('@ktx/connector-postgres')]);
if (!isKloPostgresConnectionConfig(input.connection)) {
if (!isKtxPostgresConnectionConfig(input.connection)) {
throw new Error(`Native PostgreSQL connector cannot run driver "${input.connection.driver ?? 'unknown'}"`);
}
const client = new KloPostgresHistoricSqlQueryClient({
const client = new KtxPostgresHistoricSqlQueryClient({
connectionId: input.connectionId,
connection: input.connection,
env: input.env,
@ -135,7 +135,7 @@ export async function runPostgresHistoricSqlDoctorChecks(
checkId(connectionId),
label,
`pg_stat_statements ready (${result.pgServerVersion}) with warnings: ${result.warnings.join('; ')}`,
`Update the Postgres parameter group or config, then rerun \`klo dev doctor --project-dir ${project.projectDir}\``,
`Update the Postgres parameter group or config, then rerun \`ktx dev doctor --project-dir ${project.projectDir}\``,
),
);
} else {

View file

@ -5,11 +5,11 @@ import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
getKloCliPackageInfo,
getKtxCliPackageInfo,
rendererUnavailableVizFallback,
renderMemoryFlowTui,
resolveVizFallback,
runKloCli,
runKtxCli,
sanitizeMemoryFlowTuiError,
startLiveMemoryFlowTui,
warnVizFallbackOnce,
@ -39,20 +39,20 @@ function makeIo(options: { stdoutIsTty?: boolean } = {}) {
};
}
describe('getKloCliPackageInfo', () => {
describe('getKtxCliPackageInfo', () => {
it('identifies the CLI package and its context dependency', () => {
expect(getKloCliPackageInfo()).toEqual({
name: '@klo/cli',
expect(getKtxCliPackageInfo()).toEqual({
name: '@ktx/cli',
version: '0.0.0-private',
contextPackageName: '@klo/context',
contextPackageName: '@ktx/context',
});
});
it('exports package metadata for package managers and runtime diagnostics', () => {
const packageJson = require('@klo/cli/package.json') as { name: string; version: string };
const packageJson = require('@ktx/cli/package.json') as { name: string; version: string };
expect(packageJson).toMatchObject({
name: '@klo/cli',
name: '@ktx/cli',
version: '0.0.0-private',
});
});
@ -82,11 +82,11 @@ describe('memory-flow renderer exports', () => {
});
});
describe('runKloCli', () => {
describe('runKtxCli', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-'));
});
afterEach(async () => {
@ -96,18 +96,18 @@ describe('runKloCli', () => {
it('prints version information', async () => {
const testIo = makeIo();
await expect(runKloCli(['--version'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['--version'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toBe('@klo/cli 0.0.0-private\n');
expect(testIo.stdout()).toBe('@ktx/cli 0.0.0-private\n');
expect(testIo.stderr()).toBe('');
});
it('prints the May 6 public command surface in root help', async () => {
const testIo = makeIo();
await expect(runKloCli(['--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'serve', 'status']) {
expect(testIo.stdout()).toContain(`${command}`);
}
@ -116,22 +116,22 @@ describe('runKloCli', () => {
expect(testIo.stdout()).not.toContain(`${removed} `);
}
expect(testIo.stdout()).toContain('--project-dir <path>');
expect(testIo.stdout()).toContain('KLO_PROJECT_DIR');
expect(testIo.stdout()).toContain('KTX_PROJECT_DIR');
expect(testIo.stdout()).toContain('--debug');
expect(testIo.stdout()).not.toContain('--' + 'verbose');
expect(testIo.stdout()).toContain('Advanced:');
expect(testIo.stdout()).toContain('klo dev');
expect(testIo.stdout()).toContain('ktx dev');
expect(testIo.stderr()).toBe('');
});
it('exposes demo under setup help instead of root help', async () => {
const testIo = makeIo();
await expect(runKloCli(['setup', '--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['setup', '--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo setup [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx setup [options] [command]');
expect(testIo.stdout()).toContain('demo');
expect(testIo.stdout()).toContain('Run the packaged KLO demo from setup');
expect(testIo.stdout()).toContain('Run the packaged KTX demo from setup');
expect(testIo.stdout()).not.toContain('--skip-llm');
expect(testIo.stdout()).not.toContain('--skip-embeddings');
expect(testIo.stdout()).not.toContain('--embedding-model');
@ -140,33 +140,33 @@ describe('runKloCli', () => {
expect(testIo.stderr()).toBe('');
});
it('prints help for bare klo outside a TTY', async () => {
it('prints help for bare ktx outside a TTY', async () => {
const setup = vi.fn(async () => 0);
const testIo = makeIo({ stdoutIsTty: false });
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
expect(setup).not.toHaveBeenCalled();
expect(testIo.stderr()).toBe('');
});
it('starts setup for bare klo in a TTY when no project is discoverable', async () => {
it('starts setup for bare ktx in a TTY when no project is discoverable', async () => {
const { mkdtemp, realpath, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const originalCwd = process.cwd();
const tempDir = await mkdtemp(join(tmpdir(), 'klo-bare-setup-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-bare-setup-'));
const setup = vi.fn(async () => 0);
const testIo = makeIo({ stdoutIsTty: true });
const previousProjectDir = process.env.KLO_PROJECT_DIR;
const previousProjectDir = process.env.KTX_PROJECT_DIR;
const expectedProjectDir = await realpath(tempDir);
try {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
process.chdir(tempDir);
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
expect(setup).toHaveBeenCalledWith(
{
@ -187,71 +187,71 @@ describe('runKloCli', () => {
},
testIo.io,
);
expect(testIo.stdout()).not.toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).not.toContain('Usage: ktx [options] [command]');
expect(testIo.stderr()).toBe('');
} finally {
process.chdir(originalCwd);
if (previousProjectDir === undefined) {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
} else {
process.env.KLO_PROJECT_DIR = previousProjectDir;
process.env.KTX_PROJECT_DIR = previousProjectDir;
}
await rm(tempDir, { recursive: true, force: true });
}
});
it('prints help without project status for bare klo in a TTY when a project is discoverable', async () => {
it('prints help without project status for bare ktx in a TTY when a project is discoverable', async () => {
const { mkdtemp, realpath, rm, writeFile } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const originalCwd = process.cwd();
const previousProjectDir = process.env.KLO_PROJECT_DIR;
const tempDir = await mkdtemp(join(tmpdir(), 'klo-bare-existing-'));
const previousProjectDir = process.env.KTX_PROJECT_DIR;
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-bare-existing-'));
const setup = vi.fn(async () => 0);
const testIo = makeIo({ stdoutIsTty: true });
const expectedProjectDir = await realpath(tempDir);
try {
delete process.env.KLO_PROJECT_DIR;
await writeFile(join(tempDir, 'klo.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
delete process.env.KTX_PROJECT_DIR;
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
process.chdir(tempDir);
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
expect(testIo.stdout()).not.toContain(`Project: ${expectedProjectDir}`);
expect(setup).not.toHaveBeenCalled();
} finally {
process.chdir(originalCwd);
if (previousProjectDir === undefined) {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
} else {
process.env.KLO_PROJECT_DIR = previousProjectDir;
process.env.KTX_PROJECT_DIR = previousProjectDir;
}
await rm(tempDir, { recursive: true, force: true });
}
});
it('does not invoke status for bare klo in a TTY when status would fail', async () => {
it('does not invoke status for bare ktx in a TTY when status would fail', async () => {
const setup = vi.fn(async () => {
throw new Error('Unsupported ingest.llm: use top-level llm.provider, llm.models, and ingest.workUnits');
});
const testIo = makeIo({ stdoutIsTty: true });
const previousProjectDir = process.env.KLO_PROJECT_DIR;
const previousProjectDir = process.env.KTX_PROJECT_DIR;
try {
process.env.KLO_PROJECT_DIR = tempDir;
process.env.KTX_PROJECT_DIR = tempDir;
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
expect(setup).not.toHaveBeenCalled();
expect(testIo.stderr()).toBe('');
} finally {
if (previousProjectDir === undefined) {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
} else {
process.env.KLO_PROJECT_DIR = previousProjectDir;
process.env.KTX_PROJECT_DIR = previousProjectDir;
}
}
});
@ -260,7 +260,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
const removedVerboseOption = '--' + 'verbose';
await expect(runKloCli([removedVerboseOption, 'connection', 'list'], testIo.io)).resolves.toBe(1);
await expect(runKtxCli([removedVerboseOption, 'connection', 'list'], testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toContain(`unknown option '${removedVerboseOption}'`);
expect(testIo.stdout()).toBe('');
@ -270,12 +270,12 @@ describe('runKloCli', () => {
const testIo = makeIo();
const zshWords = '$' + '{words[@]}';
await expect(runKloCli(['dev', 'completion', 'zsh'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'completion', 'zsh'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('#compdef klo');
expect(testIo.stdout()).toContain('KLO_COMPLETION_COMMAND:-klo');
expect(testIo.stdout()).toContain('#compdef ktx');
expect(testIo.stdout()).toContain('KTX_COMPLETION_COMMAND:-ktx');
expect(testIo.stdout()).toContain(`dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}"`);
expect(testIo.stdout()).toContain('compdef _klo klo');
expect(testIo.stdout()).toContain('compdef _ktx ktx');
expect(testIo.stderr()).toBe('');
});
@ -283,22 +283,22 @@ describe('runKloCli', () => {
const testIo = makeIo();
const previousHome = process.env.HOME;
const previousZdotdir = process.env.ZDOTDIR;
const tempHome = await mkdtemp(join(tmpdir(), 'klo-completion-home-'));
const tempHome = await mkdtemp(join(tmpdir(), 'ktx-completion-home-'));
try {
process.env.HOME = tempHome;
delete process.env.ZDOTDIR;
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], testIo.io)).resolves.toBe(0);
const completionFile = await readFile(join(tempHome, '.zfunc', '_klo'), 'utf-8');
const completionFile = await readFile(join(tempHome, '.zfunc', '_ktx'), 'utf-8');
const zshrc = await readFile(join(tempHome, '.zshrc'), 'utf-8');
expect(completionFile).toContain('#compdef klo');
expect(zshrc).toContain('# >>> klo completion >>>');
expect(zshrc).toContain('_klo_completion_command()');
expect(zshrc).toContain('"name": "klo-workspace"');
expect(zshrc).toContain('scripts/run-klo.mjs');
expect(zshrc).toContain("export KLO_COMPLETION_COMMAND='$(_klo_completion_command)'");
expect(completionFile).toContain('#compdef ktx');
expect(zshrc).toContain('# >>> ktx completion >>>');
expect(zshrc).toContain('_ktx_completion_command()');
expect(zshrc).toContain('"name": "ktx-workspace"');
expect(zshrc).toContain('scripts/run-ktx.mjs');
expect(zshrc).toContain("export KTX_COMPLETION_COMMAND='$(_ktx_completion_command)'");
expect(zshrc).toContain('setopt complete_aliases');
expect(zshrc).toContain('fpath=("$HOME/.zfunc" $fpath)');
expect(zshrc).toContain('autoload -Uz compinit');
@ -326,20 +326,20 @@ describe('runKloCli', () => {
const secondIo = makeIo();
const previousHome = process.env.HOME;
const previousZdotdir = process.env.ZDOTDIR;
const tempHome = await mkdtemp(join(tmpdir(), 'klo-completion-home-'));
const tempHome = await mkdtemp(join(tmpdir(), 'ktx-completion-home-'));
try {
process.env.HOME = tempHome;
delete process.env.ZDOTDIR;
await writeFile(join(tempHome, '.zshrc'), 'export EDITOR=vim\nautoload -Uz compinit\ncompinit\n', 'utf-8');
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], firstIo.io)).resolves.toBe(0);
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], secondIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], firstIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], secondIo.io)).resolves.toBe(0);
const zshrc = await readFile(join(tempHome, '.zshrc'), 'utf-8');
expect(zshrc.match(/# >>> klo completion >>>/g)).toHaveLength(1);
expect(zshrc.match(/# >>> ktx completion >>>/g)).toHaveLength(1);
expect(zshrc.indexOf('fpath=("$HOME/.zfunc" $fpath)')).toBeLessThan(zshrc.indexOf('autoload -Uz compinit'));
expect(zshrc.match(/_klo_completion_command\(\)/g)).toHaveLength(1);
expect(zshrc.match(/_ktx_completion_command\(\)/g)).toHaveLength(1);
expect(zshrc.match(/^compinit$/gm)).toHaveLength(1);
expect(secondIo.stdout()).toContain('Updated zsh config:');
expect(firstIo.stderr()).toBe('');
@ -364,11 +364,11 @@ describe('runKloCli', () => {
const connectionIo = makeIo();
await expect(
runKloCli(['dev', '__complete', '--shell', 'zsh', '--position', '2', '--', 'klo', 'co'], rootIo.io),
runKtxCli(['dev', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', 'co'], rootIo.io),
).resolves.toBe(0);
await expect(
runKloCli(
['dev', '__complete', '--shell', 'zsh', '--position', '3', '--', 'klo', 'connection', 'm'],
runKtxCli(
['dev', '__complete', '--shell', 'zsh', '--position', '3', '--', 'ktx', 'connection', 'm'],
connectionIo.io,
),
).resolves.toBe(0);
@ -386,13 +386,13 @@ describe('runKloCli', () => {
const choiceIo = makeIo();
await expect(
runKloCli(
['dev', '__complete', '--shell', 'zsh', '--position', '4', '--', 'klo', 'connection', 'add', '--cr'],
runKtxCli(
['dev', '__complete', '--shell', 'zsh', '--position', '4', '--', 'ktx', 'connection', 'add', '--cr'],
optionIo.io,
),
).resolves.toBe(0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'__complete',
@ -401,7 +401,7 @@ describe('runKloCli', () => {
'--position',
'7',
'--',
'klo',
'ktx',
'connection',
'add',
'notion',
@ -425,7 +425,7 @@ describe('runKloCli', () => {
const serveStdio = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', tempDir, 'serve', '--mcp', 'stdio', '--user-id', 'agent'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'serve', '--mcp', 'stdio', '--user-id', 'agent'], testIo.io, {
serveStdio,
}),
).resolves.toBe(0);
@ -447,7 +447,7 @@ describe('runKloCli', () => {
const ingest = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', '/tmp/project', 'ingest', 'warehouse'], testIo.io, { publicIngest: ingest }),
runKtxCli(['--project-dir', '/tmp/project', 'ingest', 'warehouse'], testIo.io, { publicIngest: ingest }),
).resolves.toBe(0);
expect(ingest).toHaveBeenCalledWith(
@ -469,10 +469,10 @@ describe('runKloCli', () => {
const lowLevelIngest = vi.fn(async () => 0);
await expect(
runKloCli(['ingest', 'watch', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest }),
runKtxCli(['ingest', 'watch', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest }),
).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo ingest watch [options] [runId]');
expect(testIo.stdout()).toContain('Usage: ktx ingest watch [options] [runId]');
expect(testIo.stdout()).toContain('[runId]');
expect(testIo.stdout()).toContain('--project-dir <path>');
expect(testIo.stdout()).toContain('--json');
@ -488,12 +488,12 @@ describe('runKloCli', () => {
const publicIngest = vi.fn(async () => 0);
await expect(
runKloCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json', '--no-input'], statusIo.io, {
runKtxCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json', '--no-input'], statusIo.io, {
publicIngest,
}),
).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'ingest', 'watch', '--no-input'], watchIo.io, {
runKtxCli(['--project-dir', tempDir, 'ingest', 'watch', '--no-input'], watchIo.io, {
publicIngest,
}),
).resolves.toBe(0);
@ -527,7 +527,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
const demo = vi.fn().mockResolvedValue(0);
await expect(runKloCli(['demo', '--mode', 'replay', '--no-input'], testIo.io, { demo })).resolves.toBe(1);
await expect(runKtxCli(['demo', '--mode', 'replay', '--no-input'], testIo.io, { demo })).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown command|error:/i);
expect(demo).not.toHaveBeenCalled();
@ -538,7 +538,7 @@ describe('runKloCli', () => {
const demo = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'replay', '--no-input'], testIo.io, { demo }),
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'replay', '--no-input'], testIo.io, { demo }),
).resolves.toBe(0);
expect(demo).toHaveBeenCalledWith(
@ -553,7 +553,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'seeded', '--no-input'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'seeded', '--no-input'], testIo.io, {
demo,
}),
).resolves.toBe(0);
@ -569,7 +569,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', '--mode', 'seeded'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', '--mode', 'seeded'], testIo.io, {
demo,
}),
).resolves.toBe(0);
@ -585,7 +585,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'demo', 'inspect', '--no-input'], testIo.io, { demo }),
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', 'inspect', '--no-input'], testIo.io, { demo }),
).resolves.toBe(0);
expect(demo).toHaveBeenCalledWith(
{
@ -603,7 +603,7 @@ describe('runKloCli', () => {
const demo = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input'], testIo.io, {
demo,
}),
).resolves.toBe(0);
@ -621,7 +621,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', 'ingest', '--mode', 'seeded'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', 'ingest', '--mode', 'seeded'], testIo.io, {
demo,
}),
).resolves.toBe(0);
@ -639,7 +639,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(
runKtxCli(
['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input', '--plain'],
testIo.io,
{
@ -665,16 +665,16 @@ describe('runKloCli', () => {
const publicIngest = vi.fn();
const lowLevelIngest = vi.fn();
await expect(runKloCli(['ingest', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(0);
await expect(runKtxCli(['ingest', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo ingest [options] [connectionId]');
expect(testIo.stdout()).toContain('Build and refresh KLO context from configured sources');
expect(testIo.stdout()).toContain('Usage: ktx ingest [options] [connectionId]');
expect(testIo.stdout()).toContain('Build and refresh KTX context from configured sources');
expect(testIo.stdout()).toContain('status');
expect(testIo.stdout()).toContain('watch');
expect(testIo.stdout()).toContain('klo ingest --all [options]');
expect(testIo.stdout()).toContain('klo ingest status [runId] [options]');
expect(testIo.stdout()).toContain('klo ingest watch [runId] [options]');
expect(testIo.stdout()).not.toContain('klo ingest replay <runId> [options]');
expect(testIo.stdout()).toContain('ktx ingest --all [options]');
expect(testIo.stdout()).toContain('ktx ingest status [runId] [options]');
expect(testIo.stdout()).toContain('ktx ingest watch [runId] [options]');
expect(testIo.stdout()).not.toContain('ktx ingest replay <runId> [options]');
expect(testIo.stdout()).toContain('--no-input');
expect(testIo.stdout()).not.toContain('--adapter');
expect(testIo.stderr()).toBe('');
@ -689,20 +689,20 @@ describe('runKloCli', () => {
const publicIngest = vi.fn(async () => 0);
const lowLevelIngest = vi.fn(async () => 0);
await expect(runKloCli(['ingest', 'run'], publicRunIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(
await expect(runKtxCli(['ingest', 'run'], publicRunIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(
1,
);
expect(publicRunIo.stderr()).toMatch(/invalid argument|reserved|run/i);
expect(publicIngest).not.toHaveBeenCalled();
await expect(
runKloCli(['ingest', 'run', '--help'], publicHelpIo.io, { publicIngest, ingest: lowLevelIngest }),
runKtxCli(['ingest', 'run', '--help'], publicHelpIo.io, { publicIngest, ingest: lowLevelIngest }),
).resolves.toBe(0);
expect(publicHelpIo.stdout()).toContain('Usage: klo ingest [options] [connectionId]');
expect(publicHelpIo.stdout()).not.toContain('Usage: klo ingest ' + 'run');
expect(publicHelpIo.stdout()).toContain('Usage: ktx ingest [options] [connectionId]');
expect(publicHelpIo.stdout()).not.toContain('Usage: ktx ingest ' + 'run');
await expect(
runKloCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
runKtxCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
publicIngest,
ingest: lowLevelIngest,
}),
@ -720,11 +720,11 @@ describe('runKloCli', () => {
const ingestRunIo = makeIo();
const ingestReplayHelpIo = makeIo();
await expect(runKloCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(
await expect(runKtxCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(
0,
);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'ingest',
@ -746,7 +746,7 @@ describe('runKloCli', () => {
{ ingest },
),
).resolves.toBe(0);
await expect(runKloCli(['dev', 'ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0);
expect(doctor).toHaveBeenCalledWith({ command: 'setup', outputMode: 'json', inputMode: 'disabled' }, doctorIo.io);
expect(ingest).toHaveBeenCalledWith(
@ -763,7 +763,7 @@ describe('runKloCli', () => {
},
ingestRunIo.io,
);
expect(ingestReplayHelpIo.stdout()).toContain('Usage: klo dev ingest replay [options] <runId>');
expect(ingestReplayHelpIo.stdout()).toContain('Usage: ktx dev ingest replay [options] <runId>');
expect(ingestReplayHelpIo.stdout()).toContain('<runId>');
expect(doctorIo.stderr()).toBe('');
expect(ingestRunIo.stderr()).toBe('');
@ -774,7 +774,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
const connection = vi.fn(async () => 0);
await expect(runKloCli(['--project-dir', tempDir, 'connection', 'list'], testIo.io, { connection })).resolves.toBe(
await expect(runKtxCli(['--project-dir', tempDir, 'connection', 'list'], testIo.io, { connection })).resolves.toBe(
0,
);
@ -788,9 +788,9 @@ describe('runKloCli', () => {
const statusIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'status', '--json'], setupIo.io, { setup }),
runKtxCli(['--project-dir', tempDir, 'setup', 'status', '--json'], setupIo.io, { setup }),
).resolves.toBe(0);
await expect(runKloCli(['--project-dir', tempDir, 'status', '--json'], statusIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli(['--project-dir', tempDir, 'status', '--json'], statusIo.io, { setup })).resolves.toBe(0);
expect(setup).toHaveBeenNthCalledWith(1, { command: 'status', projectDir: tempDir, json: true }, setupIo.io);
expect(setup).toHaveBeenNthCalledWith(2, { command: 'status', projectDir: tempDir, json: true }, statusIo.io);
@ -803,21 +803,21 @@ describe('runKloCli', () => {
const statusIo = makeIo();
const stopIo = makeIo();
await expect(runKloCli(['--project-dir', tempDir, 'setup', 'context', 'build'], buildIo.io, { setup })).resolves.toBe(
await expect(runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'build'], buildIo.io, { setup })).resolves.toBe(
0,
);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'watch', 'setup-context-local-1'], watchIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'watch', 'setup-context-local-1'], watchIo.io, {
setup,
}),
).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'status', 'setup-context-local-1', '--json'], statusIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'status', 'setup-context-local-1', '--json'], statusIo.io, {
setup,
}),
).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'stop', 'setup-context-local-1'], stopIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'stop', 'setup-context-local-1'], stopIo.io, {
setup,
}),
).resolves.toBe(0);
@ -849,7 +849,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -883,7 +883,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -907,7 +907,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -943,7 +943,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'setup',
'--project-dir',
@ -997,7 +997,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1045,7 +1045,7 @@ describe('runKloCli', () => {
const removeIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1064,7 +1064,7 @@ describe('runKloCli', () => {
),
).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'remove', '--agents'], removeIo.io, { setup }),
runKtxCli(['--project-dir', tempDir, 'setup', 'remove', '--agents'], removeIo.io, { setup }),
).resolves.toBe(0);
expect(setup).toHaveBeenNthCalledWith(
@ -1088,7 +1088,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1115,7 +1115,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'deterministic'], setupIo.io, { setup }),
runKtxCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'deterministic'], setupIo.io, { setup }),
).resolves.toBe(1);
expect(setup).not.toHaveBeenCalled();
@ -1127,7 +1127,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'gateway'], setupIo.io, { setup }),
runKtxCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'gateway'], setupIo.io, { setup }),
).resolves.toBe(1);
expect(setup).not.toHaveBeenCalled();
@ -1139,7 +1139,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1165,7 +1165,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--enable-historic-sql', '--disable-historic-sql'], setupIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', '--enable-historic-sql', '--disable-historic-sql'], setupIo.io, {
setup,
}),
).resolves.toBe(1);
@ -1179,12 +1179,12 @@ describe('runKloCli', () => {
const toolsIo = makeIo();
const agent = vi.fn(async () => 0);
await expect(runKloCli(['agent', '--help'], helpIo.io, { agent })).resolves.toBe(0);
await expect(runKtxCli(['agent', '--help'], helpIo.io, { agent })).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'agent', 'tools', '--json'], toolsIo.io, { agent }),
runKtxCli(['--project-dir', tempDir, 'agent', 'tools', '--json'], toolsIo.io, { agent }),
).resolves.toBe(0);
expect(helpIo.stdout()).toContain('Usage: klo agent');
expect(helpIo.stdout()).toContain('Usage: ktx agent');
expect(toolsIo.stderr()).toBe('');
expect(agent).toHaveBeenCalledWith({ command: 'tools', projectDir: tempDir, json: true }, toolsIo.io);
});
@ -1277,13 +1277,13 @@ describe('runKloCli', () => {
for (const entry of cases) {
const io = makeIo();
await expect(runKloCli(entry.argv, io.io, { agent })).resolves.toBe(0);
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(runKloCli(['--help'], helpIo.io, { agent })).resolves.toBe(0);
await expect(runKtxCli(['--help'], helpIo.io, { agent })).resolves.toBe(0);
expect(helpIo.stdout()).not.toContain('agent ');
});
@ -1323,7 +1323,7 @@ describe('runKloCli', () => {
const io = makeIo();
await expect(
runKloCli(
runKtxCli(
['--project-dir', tempDir, 'agent', 'sl', 'list', '--json', '--connection-id', 'warehouse', '--query', 'paid'],
io.io,
{ agent },
@ -1376,7 +1376,7 @@ describe('runKloCli', () => {
const io = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'agent', 'wiki', 'search', 'paid order', '--json', '--limit', '5'], io.io, {
runKtxCli(['--project-dir', tempDir, 'agent', 'wiki', 'search', 'paid order', '--json', '--limit', '5'], io.io, {
agent,
}),
).resolves.toBe(0);
@ -1394,23 +1394,23 @@ describe('runKloCli', () => {
});
it('dispatches public connection subcommands through the existing connection implementation', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'klo-connection-dispatch-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-connection-dispatch-'));
const connection = vi.fn(async () => 0);
await expect(
runKloCli(['--project-dir', tempDir, 'connection', 'list'], makeIo().io, { connection }),
runKtxCli(['--project-dir', tempDir, 'connection', 'list'], makeIo().io, { connection }),
).resolves.toBe(0);
const removeIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'connection', 'remove', 'warehouse', '--force', '--no-input'], removeIo.io, {
runKtxCli(['--project-dir', tempDir, 'connection', 'remove', 'warehouse', '--force', '--no-input'], removeIo.io, {
connection,
}),
).resolves.toBe(0);
const mapIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'connection', 'map', 'prod-metabase', '--json'], mapIo.io, {
runKtxCli(['--project-dir', tempDir, 'connection', 'map', 'prod-metabase', '--json'], mapIo.io, {
connection,
}),
).resolves.toBe(0);
@ -1444,9 +1444,9 @@ describe('runKloCli', () => {
it('prints help for connection metabase setup', async () => {
const helpIo = makeIo();
await expect(runKloCli(['connection', 'metabase', 'setup', '--help'], helpIo.io)).resolves.toBe(0);
await expect(runKtxCli(['connection', 'metabase', 'setup', '--help'], helpIo.io)).resolves.toBe(0);
expect(helpIo.stdout()).toContain('Usage: klo connection metabase setup');
expect(helpIo.stdout()).toContain('Usage: ktx connection metabase setup');
for (const option of [
'--id <connectionId>',
'--url <url>',
@ -1465,10 +1465,10 @@ describe('runKloCli', () => {
}
expect(helpIo.stdout()).toContain('Guided equivalent of:');
for (const line of [
'klo connection mapping refresh <connectionId> --auto-accept',
'klo connection mapping set <connectionId> databaseMappings <id>=<target>',
'klo connection mapping set-sync-enabled <connectionId> <id> --enabled true',
'klo ingest <connectionId>',
'ktx connection mapping refresh <connectionId> --auto-accept',
'ktx connection mapping set <connectionId> databaseMappings <id>=<target>',
'ktx connection mapping set-sync-enabled <connectionId> <id> --enabled true',
'ktx ingest <connectionId>',
]) {
expect(helpIo.stdout()).toContain(line);
}
@ -1481,7 +1481,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'connection',
'metabase',
@ -1598,7 +1598,7 @@ describe('runKloCli', () => {
],
]) {
const testIo = makeIo();
await expect(runKloCli(argv, testIo.io, { connectionMetabaseSetup })).resolves.toBe(1);
await expect(runKtxCli(argv, testIo.io, { connectionMetabaseSetup })).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/map|sync|sync-mode|conflict|cannot be used|invalid|integer|choices/i);
}
@ -1615,7 +1615,7 @@ describe('runKloCli', () => {
]) {
const testIo = makeIo();
await expect(runKloCli(argv, testIo.io)).resolves.toBe(1);
await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown command|error:/);
}
@ -1626,7 +1626,7 @@ describe('runKloCli', () => {
const connection = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'connection',
'add',
@ -1688,9 +1688,9 @@ describe('runKloCli', () => {
const testIo = makeIo();
const connectionNotion = vi.fn(async () => 0);
await expect(runKloCli(argv, testIo.io, { connectionNotion })).resolves.toBe(0);
await expect(runKtxCli(argv, testIo.io, { connectionNotion })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo connection notion');
expect(testIo.stdout()).toContain('Usage: ktx connection notion');
expect(testIo.stdout()).toContain('pick');
expect(testIo.stderr()).toBe('');
expect(connectionNotion).not.toHaveBeenCalled();
@ -1702,7 +1702,7 @@ describe('runKloCli', () => {
const connectionNotion = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1739,7 +1739,7 @@ describe('runKloCli', () => {
const connectionNotion = vi.fn(async () => 0);
await expect(
runKloCli(['connection', 'notion', 'pick', 'notion-main', '--root-page-id', 'not-a-uuid'], testIo.io, {
runKtxCli(['connection', 'notion', 'pick', 'notion-main', '--root-page-id', 'not-a-uuid'], testIo.io, {
connectionNotion,
}),
).resolves.toBe(0);
@ -1761,7 +1761,7 @@ describe('runKloCli', () => {
const connectionNotion = vi.fn(async () => 0);
await expect(
runKloCli(['connection', 'notion', 'pick', 'notion-main', '--no-input'], testIo.io, { connectionNotion }),
runKtxCli(['connection', 'notion', 'pick', 'notion-main', '--no-input'], testIo.io, { connectionNotion }),
).resolves.toBe(1);
expect(connectionNotion).not.toHaveBeenCalled();
@ -1773,18 +1773,18 @@ describe('runKloCli', () => {
const connection = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', tempDir, '--debug', 'connection', 'list'], testIo.io, { connection }),
runKtxCli(['--project-dir', tempDir, '--debug', 'connection', 'list'], testIo.io, { connection }),
).resolves.toBe(0);
expect(testIo.stderr()).toContain(`[debug] projectDir=${tempDir}`);
expect(testIo.stderr()).toContain('[debug] dispatch=connection');
});
it('routes low-level scan through klo dev with top-level project-dir', async () => {
it('routes low-level scan through ktx dev with top-level project-dir', async () => {
const testIo = makeIo();
const scan = vi.fn().mockResolvedValue(0);
await expect(runKloCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse'], testIo.io, { scan })).resolves.toBe(
await expect(runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse'], testIo.io, { scan })).resolves.toBe(
0,
);
@ -1807,7 +1807,7 @@ describe('runKloCli', () => {
const serveStdio = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'serve',
'--mcp',
@ -1843,9 +1843,9 @@ describe('runKloCli', () => {
it('prints dev help for bare dev commands', async () => {
const testIo = makeIo();
await expect(runKloCli(['dev'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo dev [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
expect(testIo.stdout()).toContain('Low-level diagnostics');
expect(testIo.stdout()).toContain('scan');
expect(testIo.stdout()).toContain('ingest');
@ -1857,15 +1857,15 @@ describe('runKloCli', () => {
it('prints dev command help without invoking low-level execution', async () => {
for (const [command, expected] of [
['scan', ['Usage: klo dev scan', '--dry-run', 'status', 'report']],
['ingest', ['Usage: klo dev ingest', 'run', 'replay']],
['mapping', ['Usage: klo dev mapping', 'sync-state', 'validate']],
['scan', ['Usage: ktx dev scan', '--dry-run', 'status', 'report']],
['ingest', ['Usage: ktx dev ingest', 'run', 'replay']],
['mapping', ['Usage: ktx dev mapping', 'sync-state', 'validate']],
] as const) {
const testIo = makeIo();
const scan = vi.fn().mockResolvedValue(0);
const sl = vi.fn().mockResolvedValue(0);
await expect(runKloCli(['dev', command, '--help'], testIo.io, { scan, sl })).resolves.toBe(0);
await expect(runKtxCli(['dev', command, '--help'], testIo.io, { scan, sl })).resolves.toBe(0);
for (const text of expected) {
expect(testIo.stdout()).toContain(text);
@ -1880,9 +1880,9 @@ describe('runKloCli', () => {
const testIo = makeIo();
const scan = vi.fn().mockResolvedValue(0);
await expect(runKloCli(['dev', 'scan', 'report', '--help'], testIo.io, { scan })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'scan', 'report', '--help'], testIo.io, { scan })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo dev scan report [options] <runId>');
expect(testIo.stdout()).toContain('Usage: ktx dev scan report [options] <runId>');
expect(testIo.stderr()).toBe('');
expect(scan).not.toHaveBeenCalled();
});
@ -1890,7 +1890,7 @@ describe('runKloCli', () => {
it('rejects removed reserved dev subcommands', async () => {
const testIo = makeIo();
await expect(runKloCli(['dev', 'artifacts'], testIo.io)).resolves.toBe(1);
await expect(runKtxCli(['dev', 'artifacts'], testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown command|error:/);
});
@ -1906,7 +1906,7 @@ describe('runKloCli', () => {
['setup', 'demo', 'replay', '--json', '--plain'],
]) {
const testIo = makeIo();
await expect(runKloCli(argv, testIo.io, { ingest, demo })).resolves.toBe(1);
await expect(runKtxCli(argv, testIo.io, { ingest, demo })).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/conflict|cannot be used/i);
}
@ -1920,7 +1920,7 @@ describe('runKloCli', () => {
const tokenIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'connection',
'add',
@ -1946,14 +1946,14 @@ describe('runKloCli', () => {
it('validates connection mapping set syntax before runner domain validation', async () => {
const badFieldIo = makeIo();
await expect(
runKloCli(['connection', 'mapping', 'set', 'prod-metabase', 'invalidMappings', '1=warehouse'], badFieldIo.io),
runKtxCli(['connection', 'mapping', 'set', 'prod-metabase', 'invalidMappings', '1=warehouse'], badFieldIo.io),
).resolves.toBe(1);
expect(badFieldIo.stderr()).toContain('databaseMappings or connectionMappings');
for (const assignment of ['missing-equals', '=warehouse', '1=']) {
const testIo = makeIo();
await expect(
runKloCli(['connection', 'mapping', 'set', 'prod-metabase', 'databaseMappings', assignment], testIo.io),
runKtxCli(['connection', 'mapping', 'set', 'prod-metabase', 'databaseMappings', assignment], testIo.io),
).resolves.toBe(1);
expect(testIo.stderr()).toContain('non-empty <key>=<value>');
}
@ -1962,7 +1962,7 @@ describe('runKloCli', () => {
it('does not expose root init after setup owns project creation', async () => {
const testIo = makeIo();
await expect(runKloCli(['init'], testIo.io)).resolves.toBe(1);
await expect(runKtxCli(['init'], testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toContain("error: unknown command 'init'");
});
@ -1970,7 +1970,7 @@ describe('runKloCli', () => {
it('returns an error code for unknown commands', async () => {
const testIo = makeIo();
await expect(runKloCli(['unknown'], testIo.io)).resolves.toBe(1);
await expect(runKtxCli(['unknown'], testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toContain("error: unknown command 'unknown'");
});

View file

@ -1,48 +1,48 @@
import { profileMark } from './startup-profile.js';
export {
getKloCliPackageInfo,
getKtxCliPackageInfo,
runInitForCommander,
runKloCli,
type KloCliDeps,
type KloCliIo,
type KloCliPackageInfo,
runKtxCli,
type KtxCliDeps,
type KtxCliIo,
type KtxCliPackageInfo,
} from './cli-runtime.js';
export { runKloAgent, type KloAgentArgs } from './agent.js';
export { runKtxAgent, type KtxAgentArgs } from './agent.js';
export {
KLO_AGENT_MAX_ROWS_CAP,
createKloAgentRuntime,
KTX_AGENT_MAX_ROWS_CAP,
createKtxAgentRuntime,
parseAgentMaxRows,
readAgentJsonFile,
writeAgentJson,
writeAgentJsonError,
type KloAgentRuntime,
type KloAgentRuntimeDeps,
type KtxAgentRuntime,
type KtxAgentRuntimeDeps,
} from './agent-runtime.js';
export { runKloSetup, type KloSetupArgs, type KloSetupStatus } from './setup.js';
export { runKtxSetup, type KtxSetupArgs, type KtxSetupStatus } from './setup.js';
export type {
KloSetupDatabaseDriver,
KloSetupDatabasesArgs,
KloSetupDatabasesDeps,
KloSetupDatabasesResult,
KtxSetupDatabaseDriver,
KtxSetupDatabasesArgs,
KtxSetupDatabasesDeps,
KtxSetupDatabasesResult,
} from './setup-databases.js';
export { runKloSetupDatabasesStep } from './setup-databases.js';
export { runKtxSetupDatabasesStep } from './setup-databases.js';
export type {
KloSetupEmbeddingBackend,
KloSetupEmbeddingsArgs,
KloSetupEmbeddingsDeps,
KloSetupEmbeddingsResult,
KtxSetupEmbeddingBackend,
KtxSetupEmbeddingsArgs,
KtxSetupEmbeddingsDeps,
KtxSetupEmbeddingsResult,
} from './setup-embeddings.js';
export { runKloSetupEmbeddingsStep } from './setup-embeddings.js';
export { runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
export type {
KloSetupSourcesArgs,
KloSetupSourcesDeps,
KloSetupSourcesPromptAdapter,
KloSetupSourcesResult,
KloSetupSourceType,
KtxSetupSourcesArgs,
KtxSetupSourcesDeps,
KtxSetupSourcesPromptAdapter,
KtxSetupSourcesResult,
KtxSetupSourceType,
} from './setup-sources.js';
export { runKloSetupSourcesStep } from './setup-sources.js';
export type { KloMemoryFlowTuiIo, MemoryFlowTuiLiveSession } from './memory-flow-tui.js';
export { runKtxSetupSourcesStep } from './setup-sources.js';
export type { KtxMemoryFlowTuiIo, MemoryFlowTuiLiveSession } from './memory-flow-tui.js';
export {
renderMemoryFlowTui,
sanitizeMemoryFlowTuiError,

View file

@ -36,7 +36,7 @@ describe('readIngestReportSnapshotFile', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-report-file-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-report-file-'));
});
afterEach(async () => {

View file

@ -1,5 +1,5 @@
import { readFile } from 'node:fs/promises';
import { parseIngestReportSnapshot, type IngestReportSnapshot } from '@klo/context/ingest';
import { parseIngestReportSnapshot, type IngestReportSnapshot } from '@ktx/context/ingest';
export async function readIngestReportSnapshotFile(reportFile: string): Promise<IngestReportSnapshot> {
const raw = await readFile(reportFile, 'utf-8');

View file

@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events';
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { AgentRunnerService, type RunLoopParams } from '@klo/context/agent';
import { AgentRunnerService, type RunLoopParams } from '@ktx/context/agent';
import {
LocalLookerRuntimeStore,
LocalMetabaseSourceStateReader,
@ -22,10 +22,10 @@ import {
type RunLocalIngestOptions,
type SourceAdapter,
type SqliteBundleIngestStore,
} from '@klo/context/ingest';
import { initKloProject, kloLocalStateDbPath, loadKloProject } from '@klo/context/project';
} from '@ktx/context/ingest';
import { initKtxProject, ktxLocalStateDbPath, loadKtxProject } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { type KloIngestArgs, runKloIngest } from './ingest.js';
import { type KtxIngestArgs, runKtxIngest } from './ingest.js';
import { resetVizFallbackWarningsForTest } from './viz-fallback.js';
function makeIo(
@ -104,7 +104,7 @@ function makeIo(
async function writeWarehouseConfig(projectDir: string): Promise<void> {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -123,7 +123,7 @@ async function writeWarehouseConfig(projectDir: string): Promise<void> {
async function writeMetabaseConfig(projectDir: string): Promise<void> {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -438,9 +438,9 @@ type SyncModeCase = {
async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCase): Promise<void> {
const projectDir = join(tempDir, `metabase-sync-mode-${input.name}`);
await initKloProject({ projectDir, projectName: `metabase-sync-mode-${input.name}` });
await initKtxProject({ projectDir, projectName: `metabase-sync-mode-${input.name}` });
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
`project: metabase-sync-mode-${input.name}`,
'connections:',
@ -461,8 +461,8 @@ async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCas
'utf-8',
);
const project = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
const project = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
await store.replaceSourceState({
connectionId: 'prod-metabase',
syncMode: input.syncMode,
@ -490,7 +490,7 @@ async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCas
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -640,10 +640,10 @@ function localFakeBundleReport(jobId: string, overrides: Partial<IngestReportSna
}
async function localBundleStore(projectDir: string, ids: [string, string]): Promise<SqliteBundleIngestStore> {
const { SqliteBundleIngestStore } = await import('@klo/context/ingest');
const project = await loadKloProject({ projectDir });
const { SqliteBundleIngestStore } = await import('@ktx/context/ingest');
const project = await loadKtxProject({ projectDir });
return new SqliteBundleIngestStore({
dbPath: kloLocalStateDbPath(project),
dbPath: ktxLocalStateDbPath(project),
idFactory: (() => {
let index = 0;
return () => ids[index++] ?? `generated-${index}`;
@ -696,7 +696,7 @@ function emitLiveLocalMemoryFlow(memoryFlow: MemoryFlowEventSink | undefined): v
memoryFlow?.finish('done');
}
describe('runKloIngest', () => {
describe('runKtxIngest', () => {
let tempDir: string;
let originalTerm: string | undefined;
@ -704,7 +704,7 @@ describe('runKloIngest', () => {
resetVizFallbackWarningsForTest();
originalTerm = process.env.TERM;
process.env.TERM = 'xterm-256color';
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-ingest-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-ingest-'));
});
afterEach(async () => {
@ -718,7 +718,7 @@ describe('runKloIngest', () => {
it('runs local ingest and reads status', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -731,7 +731,7 @@ describe('runKloIngest', () => {
const runIo = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -757,7 +757,7 @@ describe('runKloIngest', () => {
const statusIo = makeIo();
await expect(
runKloIngest({ command: 'status', projectDir, runId: 'cli-local-run-1', outputMode: 'plain' }, statusIo.io),
runKtxIngest({ command: 'status', projectDir, runId: 'cli-local-run-1', outputMode: 'plain' }, statusIo.io),
).resolves.toBe(0);
expect(statusIo.stdout()).toContain('Report: report-live-1');
@ -770,7 +770,7 @@ describe('runKloIngest', () => {
it('routes metabase scheduled pulls to the fan-out runner and prints child summaries', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeMetabaseConfig(projectDir);
const io = makeIo();
const report = localFakeBundleReport('metabase-child-1', {
@ -782,7 +782,7 @@ describe('runKloIngest', () => {
});
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -828,7 +828,7 @@ describe('runKloIngest', () => {
it('prints Metabase fan-out progress before the final summary', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeMetabaseConfig(projectDir);
const io = makeIo();
const report = localFakeBundleReport('metabase-child-1', {
@ -840,7 +840,7 @@ describe('runKloIngest', () => {
});
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -908,9 +908,9 @@ describe('runKloIngest', () => {
it('runs Metabase scheduled ingest through the public CLI command path with real fan-out', async () => {
const projectDir = join(tempDir, 'metabase-cli-project');
await initKloProject({ projectDir, projectName: 'metabase-cli' });
await initKtxProject({ projectDir, projectName: 'metabase-cli' });
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: metabase-cli',
'connections:',
@ -933,12 +933,12 @@ describe('runKloIngest', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
const project = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
await store.replaceSourceState({
connectionId: 'prod-metabase',
syncMode: 'ALL',
defaultTagNames: ['klo'],
defaultTagNames: ['ktx'],
selections: [],
mappings: [
{
@ -969,7 +969,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1001,7 +1001,7 @@ describe('runKloIngest', () => {
const statusIo = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{ command: 'status', projectDir, runId: 'metabase-child-1', outputMode: 'plain' },
statusIo.io,
),
@ -1046,12 +1046,12 @@ describe('runKloIngest', () => {
it('prints metabase fan-out JSON results', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeMetabaseConfig(projectDir);
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1081,12 +1081,12 @@ describe('runKloIngest', () => {
it('rejects source-dir uploads through the metabase fan-out route', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeMetabaseConfig(projectDir);
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1105,13 +1105,13 @@ describe('runKloIngest', () => {
).resolves.toBe(1);
expect(io.stderr()).toContain('source-dir uploads are not supported for the Metabase fan-out adapter');
expect(io.stderr()).not.toContain('klo dev ingest run requires llm.provider.backend');
expect(io.stderr()).not.toContain('ktx dev ingest run requires llm.provider.backend');
expect(io.stdout()).toBe('');
});
it('prints previous run and diff summary for local ingest results', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1120,7 +1120,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1145,15 +1145,15 @@ describe('runKloIngest', () => {
it('passes the debug LLM request file to local ingest runs', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const runLocalIngest = vi.fn(async (input: RunLocalIngestOptions) =>
completedLocalBundleRun(input, 'job-debug'),
);
const io = makeIo();
const debugFile = join(projectDir, '.klo', 'llm-debug.jsonl');
const debugFile = join(projectDir, '.ktx', 'llm-debug.jsonl');
const exitCode = await runKloIngest(
const exitCode = await runKtxIngest(
{
command: 'run',
projectDir,
@ -1172,7 +1172,7 @@ describe('runKloIngest', () => {
it('passes daemon database introspection URL to default local ingest adapters', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1187,7 +1187,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1196,7 +1196,7 @@ describe('runKloIngest', () => {
sourceDir,
databaseIntrospectionUrl: 'http://127.0.0.1:8765',
outputMode: 'plain',
} satisfies KloIngestArgs,
} satisfies KtxIngestArgs,
io.io,
{
createAdapters,
@ -1220,9 +1220,9 @@ describe('runKloIngest', () => {
it('passes the target connection id when constructing local historic-sql adapters', async () => {
const projectDir = join(tempDir, 'historic-sql-project');
await initKloProject({ projectDir, projectName: 'historic-sql-project' });
await initKtxProject({ projectDir, projectName: 'historic-sql-project' });
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: historic-sql-project',
'connections:',
@ -1250,7 +1250,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1281,7 +1281,7 @@ describe('runKloIngest', () => {
it('passes local Looker pull-config options and agent runner into scheduled ingest for Looker scheduled ingest', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const pullConfigOptions = {
looker: {
@ -1299,14 +1299,14 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
connectionId: 'warehouse',
adapter: 'fake',
outputMode: 'plain',
} satisfies KloIngestArgs,
} satisfies KtxIngestArgs,
io.io,
{
createAdapters,
@ -1335,9 +1335,9 @@ describe('runKloIngest', () => {
it('runs Looker scheduled ingest through the public CLI command path', async () => {
const projectDir = join(tempDir, 'looker-project');
await initKloProject({ projectDir, projectName: 'looker-cli' });
await initKtxProject({ projectDir, projectName: 'looker-cli' });
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: looker-cli',
'connections:',
@ -1357,8 +1357,8 @@ describe('runKloIngest', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir });
const store = new LocalLookerRuntimeStore({ dbPath: kloLocalStateDbPath(project) });
const project = await loadKtxProject({ projectDir });
const store = new LocalLookerRuntimeStore({ dbPath: ktxLocalStateDbPath(project) });
await store.setCursors('prod-looker', {
dashboardsLastSyncedAt: null,
looksLastSyncedAt: null,
@ -1366,7 +1366,7 @@ describe('runKloIngest', () => {
await store.upsertConnectionMapping({
lookerConnectionId: 'prod-looker',
lookerConnectionName: 'analytics',
kloConnectionId: 'prod-warehouse',
ktxConnectionId: 'prod-warehouse',
source: 'cli',
});
const runtimeClient = makeCliLookerRuntimeClient();
@ -1375,7 +1375,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1419,7 +1419,7 @@ describe('runKloIngest', () => {
const statusIo = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{ command: 'status', projectDir, runId: 'cli-looker-job', outputMode: 'plain' },
statusIo.io,
),
@ -1431,7 +1431,7 @@ describe('runKloIngest', () => {
it('renders live memory-flow frames for run --viz when stdout is interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1462,7 +1462,7 @@ describe('runKloIngest', () => {
const startLiveMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => null);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1483,15 +1483,15 @@ describe('runKloIngest', () => {
expect(runLocal).toHaveBeenCalledWith(expect.objectContaining({ memoryFlow: expect.any(Object) }));
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
expect((io.stdout().match(/KLO memory flow/g) ?? []).length).toBeGreaterThan(1);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect((io.stdout().match(/KTX memory flow/g) ?? []).length).toBeGreaterThan(1);
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).toContain('fake-orders');
expect(io.stderr()).toBe('');
});
it('uses the TUI live session for run --viz when stdin and stdout are interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1510,7 +1510,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1539,13 +1539,13 @@ describe('runKloIngest', () => {
expect(liveSession.update).toHaveBeenCalled();
expect(liveSession.close).toHaveBeenCalledTimes(1);
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toBe('');
});
it('prints a final plain summary after live viz completes', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
const liveSession = {
@ -1560,7 +1560,7 @@ describe('runKloIngest', () => {
});
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1580,7 +1580,7 @@ describe('runKloIngest', () => {
it('falls back to text live rendering when the TUI live session is unavailable', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1594,7 +1594,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1614,12 +1614,12 @@ describe('runKloIngest', () => {
expect(startLiveMemoryFlow).toHaveBeenCalledTimes(1);
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
});
it('falls back to text live rendering when TUI startup fails with a redacted warning', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1638,7 +1638,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1657,13 +1657,13 @@ describe('runKloIngest', () => {
).resolves.toBe(0);
expect(io.stderr()).toContain('TUI visualization unavailable: Failed [redacted-url] [redacted]');
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
});
it('does not start live TUI when run --viz disables input', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1680,7 +1680,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1697,12 +1697,12 @@ describe('runKloIngest', () => {
expect(startLiveMemoryFlow).not.toHaveBeenCalled();
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
});
it('does not attach a live memory-flow sink for plain run output', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1712,7 +1712,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1728,12 +1728,12 @@ describe('runKloIngest', () => {
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
expect(io.stdout()).toContain('Job: plain-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
});
it('falls back to plain run output for run --viz when stdout is not interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1742,7 +1742,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: false });
const runLocal = vi.fn(async (input: RunLocalIngestOptions) => completedLocalBundleRun(input, 'non-tty-viz-run'));
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1760,7 +1760,7 @@ describe('runKloIngest', () => {
).resolves.toBe(0);
expect(io.stdout()).toContain('Job: non-tty-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
);
@ -1768,7 +1768,7 @@ describe('runKloIngest', () => {
it('falls back to plain run output for run --viz when stdin raw mode is unavailable', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1783,7 +1783,7 @@ describe('runKloIngest', () => {
}));
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1804,7 +1804,7 @@ describe('runKloIngest', () => {
expect(startLiveMemoryFlow).not.toHaveBeenCalled();
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
expect(io.stdout()).toContain('Job: raw-missing-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
);
@ -1812,11 +1812,11 @@ describe('runKloIngest', () => {
it('returns an error code for missing status', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloIngest({ command: 'status', projectDir, runId: 'missing-run', outputMode: 'plain' }, io.io),
runKtxIngest({ command: 'status', projectDir, runId: 'missing-run', outputMode: 'plain' }, io.io),
).resolves.toBe(1);
expect(io.stderr()).toContain('Local ingest run or report "missing-run" was not found');
@ -1824,13 +1824,13 @@ describe('runKloIngest', () => {
it('uses the latest local ingest report when status has no run id', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
await persistLocalBundleReport(projectDir, localFakeBundleReport('older-run'));
await persistLocalBundleReport(projectDir, localFakeBundleReport('newer-run'));
const io = makeIo();
await expect(runKloIngest({ command: 'status', projectDir, outputMode: 'plain' }, io.io)).resolves.toBe(0);
await expect(runKtxIngest({ command: 'status', projectDir, outputMode: 'plain' }, io.io)).resolves.toBe(0);
expect(io.stdout()).toContain('Run: run-newer-run');
expect(io.stdout()).toContain('Job: newer-run');
@ -1839,28 +1839,28 @@ describe('runKloIngest', () => {
it('renders the latest local ingest report through watch when run id is omitted', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
await persistLocalBundleReport(projectDir, localFakeBundleReport('watch-latest'));
const io = makeIo({ isTTY: true });
await expect(
runKloIngest({ command: 'watch', projectDir, outputMode: 'viz', inputMode: 'disabled' }, io.io),
runKtxIngest({ command: 'watch', projectDir, outputMode: 'viz', inputMode: 'disabled' }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).toContain('Run: run-watch-latest');
expect(io.stderr()).toBe('');
});
it('renders report-file replay through the memory-flow TUI', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const reportFile = await writeBundleReportFile(tempDir);
const io = makeIo({ isTTY: true });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -1873,7 +1873,7 @@ describe('runKloIngest', () => {
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/metabase done');
expect(io.stdout()).toContain('KTX memory flow warehouse/metabase done');
expect(io.stdout()).toContain('Saved 2 memories from 2 raw files');
expect(io.stdout()).toContain('Commit: abc12345 Run: run-1 Report: report-1');
expect(io.stdout()).toContain('SOURCE');
@ -1884,12 +1884,12 @@ describe('runKloIngest', () => {
it('prints report-file JSON without looking up local ingest status', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const reportFile = await writeBundleReportFile(tempDir);
const io = makeIo();
await expect(
runKloIngest({ command: 'status', projectDir, runId: 'report-1', reportFile, outputMode: 'json' }, io.io),
runKtxIngest({ command: 'status', projectDir, runId: 'report-1', reportFile, outputMode: 'json' }, io.io),
).resolves.toBe(0);
const parsed = JSON.parse(io.stdout());
@ -1905,13 +1905,13 @@ describe('runKloIngest', () => {
it('routes interactive report-file replay through the stored TUI renderer', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const reportFile = await writeBundleReportFile(tempDir);
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -1937,12 +1937,12 @@ describe('runKloIngest', () => {
it('rejects report-file replay when the requested id does not match the report', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const reportFile = await writeBundleReportFile(tempDir);
const io = makeIo();
await expect(
runKloIngest({ command: 'replay', projectDir, runId: 'unrelated-id', reportFile, outputMode: 'plain' }, io.io),
runKtxIngest({ command: 'replay', projectDir, runId: 'unrelated-id', reportFile, outputMode: 'plain' }, io.io),
).resolves.toBe(1);
expect(io.stderr()).toContain(
@ -1953,7 +1953,7 @@ describe('runKloIngest', () => {
it('renders memory-flow snapshot for status --viz when stdout is interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1963,13 +1963,13 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true });
await expect(
runKloIngest(
runKtxIngest(
{ command: 'status', projectDir, runId: 'viz-run-1', outputMode: 'viz', inputMode: 'disabled' },
io.io,
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).toContain('SOURCE');
expect(io.stdout()).toContain('CHUNKS');
expect(io.stdout()).toContain('WORKUNITS');
@ -1979,7 +1979,7 @@ describe('runKloIngest', () => {
it('uses the TUI renderer for stored status --viz when stdin and stdout are interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1991,7 +1991,7 @@ describe('runKloIngest', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'status',
projectDir,
@ -2015,7 +2015,7 @@ describe('runKloIngest', () => {
it('falls back to the text renderer when TUI declines stored status --viz', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2027,7 +2027,7 @@ describe('runKloIngest', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => false);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'status',
projectDir,
@ -2040,12 +2040,12 @@ describe('runKloIngest', () => {
).resolves.toBe(0);
expect(renderStoredMemoryFlow).toHaveBeenCalledTimes(1);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
});
it('does not use TUI for stored --viz when input is disabled', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2057,7 +2057,7 @@ describe('runKloIngest', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2071,12 +2071,12 @@ describe('runKloIngest', () => {
).resolves.toBe(0);
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
});
it('falls back to plain status for stored --viz when stdin raw mode is unavailable', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2088,7 +2088,7 @@ describe('runKloIngest', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2103,7 +2103,7 @@ describe('runKloIngest', () => {
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
expect(io.stdout()).toContain('Run: run-raw-missing-stored-viz-run');
expect(io.stdout()).toContain('Job: raw-missing-stored-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
);
@ -2111,7 +2111,7 @@ describe('runKloIngest', () => {
it('keeps stored --viz snapshot-only when input is disabled', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2121,7 +2121,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2133,14 +2133,14 @@ describe('runKloIngest', () => {
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
expect(io.stderr()).toBe('');
});
it('keeps disabled-input stored --viz snapshot output even when stdin raw mode is unavailable', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2150,7 +2150,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, rawMode: false, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2162,14 +2162,14 @@ describe('runKloIngest', () => {
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
expect(io.stderr()).toBe('');
});
it('degrades stored --viz snapshots to plain status when stdout is redirected even when input is disabled', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2179,7 +2179,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: false });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2193,7 +2193,7 @@ describe('runKloIngest', () => {
expect(io.stdout()).toContain('Run: run-redirected-no-input-viz-run');
expect(io.stdout()).toContain('Job: redirected-no-input-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
);
@ -2201,7 +2201,7 @@ describe('runKloIngest', () => {
it('degrades ingest replay --viz to plain status when TERM is dumb', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2211,7 +2211,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true });
await expect(
runKloIngest(
runKtxIngest(
{ command: 'replay', projectDir, runId: 'dumb-terminal-viz-run', outputMode: 'viz' },
io.io,
{ env: { ...process.env, TERM: 'dumb' } },
@ -2220,7 +2220,7 @@ describe('runKloIngest', () => {
expect(io.stdout()).toContain('Run: run-dumb-terminal-viz-run');
expect(io.stdout()).toContain('Job: dumb-terminal-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but TERM=dumb does not support the visual renderer; printing plain output.',
);
@ -2228,7 +2228,7 @@ describe('runKloIngest', () => {
it('falls back to plain status for --viz when stdout is not interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2238,12 +2238,12 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: false });
await expect(
runKloIngest({ command: 'replay', projectDir, runId: 'viz-run-2', outputMode: 'viz' }, io.io),
runKtxIngest({ command: 'replay', projectDir, runId: 'viz-run-2', outputMode: 'viz' }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain('Run: run-viz-run-2');
expect(io.stdout()).toContain('Job: viz-run-2');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
);
@ -2251,7 +2251,7 @@ describe('runKloIngest', () => {
it('prints JSON for status --json', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2261,7 +2261,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest({ command: 'status', projectDir, runId: 'json-run-1', outputMode: 'json' }, io.io),
runKtxIngest({ command: 'status', projectDir, runId: 'json-run-1', outputMode: 'json' }, io.io),
).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toMatchObject({

View file

@ -13,13 +13,13 @@ import {
renderMemoryFlowReplay,
runLocalIngest,
runLocalMetabaseIngest,
} from '@klo/context/ingest';
import { loadKloProject } from '@klo/context/project';
} from '@ktx/context/ingest';
import { loadKtxProject } from '@ktx/context/project';
import { readIngestReportSnapshotFile } from './ingest-report-file.js';
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
import { type KloMemoryFlowStdin, renderMemoryFlowInteractively } from './memory-flow-interactive.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
import { type KtxMemoryFlowStdin, renderMemoryFlowInteractively } from './memory-flow-interactive.js';
import {
type KloMemoryFlowTuiIo,
type KtxMemoryFlowTuiIo,
type MemoryFlowTuiLiveSession,
renderMemoryFlowTui,
startLiveMemoryFlowTui,
@ -29,10 +29,10 @@ import { profileMark } from './startup-profile.js';
profileMark('module:ingest');
export type KloIngestOutputMode = 'plain' | 'json' | 'viz';
type KloIngestInputMode = 'auto' | 'disabled';
export type KtxIngestOutputMode = 'plain' | 'json' | 'viz';
type KtxIngestInputMode = 'auto' | 'disabled';
export type KloIngestArgs =
export type KtxIngestArgs =
| {
command: 'run';
projectDir: string;
@ -41,28 +41,28 @@ export type KloIngestArgs =
sourceDir?: string;
databaseIntrospectionUrl?: string;
debugLlmRequestFile?: string;
outputMode: KloIngestOutputMode;
inputMode?: KloIngestInputMode;
outputMode: KtxIngestOutputMode;
inputMode?: KtxIngestInputMode;
}
| {
command: 'status' | 'replay' | 'watch';
projectDir: string;
runId?: string;
reportFile?: string;
outputMode: KloIngestOutputMode;
inputMode?: KloIngestInputMode;
outputMode: KtxIngestOutputMode;
inputMode?: KtxIngestInputMode;
};
interface KloIngestIo {
stdin?: KloMemoryFlowStdin;
interface KtxIngestIo {
stdin?: KtxMemoryFlowStdin;
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
stderr: { write(chunk: string): void };
}
interface KloIngestDeps {
interface KtxIngestDeps {
jobIdFactory?: () => string;
now?: () => Date;
createAdapters?: typeof createKloCliLocalIngestAdapters;
createAdapters?: typeof createKtxCliLocalIngestAdapters;
runLocalIngest?: typeof runLocalIngest;
runLocalMetabaseIngest?: typeof runLocalMetabaseIngest;
readReportFile?: typeof readIngestReportSnapshotFile;
@ -93,7 +93,7 @@ function reportActionCounts(report: IngestReportSnapshot): { wikiCount: number;
};
}
function writeReportStatus(report: IngestReportSnapshot, io: KloIngestIo): void {
function writeReportStatus(report: IngestReportSnapshot, io: KtxIngestIo): void {
const counts = reportActionCounts(report);
io.stdout.write(`Report: ${report.id}\n`);
io.stdout.write(`Run: ${report.runId}\n`);
@ -110,7 +110,7 @@ function writeReportStatus(report: IngestReportSnapshot, io: KloIngestIo): void
io.stdout.write(`Provenance rows: ${report.body.provenanceRows.length}\n`);
}
function writeMetabaseFanoutStatus(result: LocalMetabaseFanoutResult, io: KloIngestIo): void {
function writeMetabaseFanoutStatus(result: LocalMetabaseFanoutResult, io: KtxIngestIo): void {
io.stdout.write(`Metabase fan-out: ${result.status}\n`);
io.stdout.write(`Source: ${result.metabaseConnectionId}\n`);
io.stdout.write(`Children: ${result.children.length}\n`);
@ -132,7 +132,7 @@ function pluralize(count: number, singular: string, plural = `${singular}s`): st
function createMetabaseFanoutProgress(
connectionId: string,
io: KloIngestIo,
io: KtxIngestIo,
): LocalMetabaseFanoutProgress {
io.stdout.write(`Metabase ingest: ${connectionId}\n`);
io.stdout.write('Checking mappings and scheduled-pull targets...\n');
@ -156,7 +156,7 @@ function createMetabaseFanoutProgress(
};
}
function writeReportJson(report: IngestReportSnapshot, io: KloIngestIo): void {
function writeReportJson(report: IngestReportSnapshot, io: KtxIngestIo): void {
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
}
@ -172,21 +172,21 @@ function assertReportMatchesReplayId(report: IngestReportSnapshot, requestedId:
}
async function readStoredIngestReport(
project: Awaited<ReturnType<typeof loadKloProject>>,
project: Awaited<ReturnType<typeof loadKtxProject>>,
runId: string | undefined,
): Promise<IngestReportSnapshot | null> {
return runId ? await getLocalIngestStatus(project, runId) : await getLatestLocalIngestStatus(project);
}
function isInteractiveTerminal(io: KloIngestIo): boolean {
function isInteractiveTerminal(io: KtxIngestIo): boolean {
return io.stdout.isTTY === true;
}
function terminalWidth(io: KloIngestIo): number | undefined {
function terminalWidth(io: KtxIngestIo): number | undefined {
return io.stdout.columns ?? process.stdout.columns;
}
function isTuiCapableIo(io: KloIngestIo): io is KloIngestIo & KloMemoryFlowTuiIo {
function isTuiCapableIo(io: KtxIngestIo): io is KtxIngestIo & KtxMemoryFlowTuiIo {
return (
io.stdin?.isTTY === true &&
io.stdout.isTTY === true &&
@ -201,11 +201,11 @@ interface EffectiveIngestOutputModeOptions {
}
function effectiveIngestOutputMode(
outputMode: KloIngestOutputMode,
io: KloIngestIo,
outputMode: KtxIngestOutputMode,
io: KtxIngestIo,
env: NodeJS.ProcessEnv,
options: EffectiveIngestOutputModeOptions = {},
): KloIngestOutputMode {
): KtxIngestOutputMode {
if (outputMode !== 'viz') {
return outputMode;
}
@ -219,7 +219,7 @@ function effectiveIngestOutputMode(
return 'plain';
}
function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KloIngestIo, options: { clear?: boolean } = {}): void {
function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KtxIngestIo, options: { clear?: boolean } = {}): void {
if (options.clear) {
io.stdout.write('\u001b[2J\u001b[H');
}
@ -228,7 +228,7 @@ function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KloIngestIo, opt
}
function initialRunMemoryFlowInput(
args: Extract<KloIngestArgs, { command: 'run' }>,
args: Extract<KtxIngestArgs, { command: 'run' }>,
runId: string,
): MemoryFlowReplayInput {
return {
@ -247,8 +247,8 @@ function initialRunMemoryFlowInput(
async function writeReportRecord(
report: IngestReportSnapshot,
outputMode: KloIngestOutputMode,
io: KloIngestIo,
outputMode: KtxIngestOutputMode,
io: KtxIngestIo,
options: {
interactive?: boolean;
renderStoredMemoryFlow?: typeof renderMemoryFlowTui;
@ -288,16 +288,16 @@ async function writeReportRecord(
writeReportStatus(report, io);
}
export async function runKloIngest(
args: KloIngestArgs,
io: KloIngestIo = process,
deps: KloIngestDeps = {},
export async function runKtxIngest(
args: KtxIngestArgs,
io: KtxIngestIo = process,
deps: KtxIngestDeps = {},
): Promise<number> {
try {
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
const env = deps.env ?? process.env;
if (args.command === 'run') {
const createAdapters = deps.createAdapters ?? createKloCliLocalIngestAdapters;
const createAdapters = deps.createAdapters ?? createKtxCliLocalIngestAdapters;
const executeLocalIngest = deps.runLocalIngest ?? runLocalIngest;
const localIngestOptions = deps.localIngestOptions ?? {};
const adapterOptions = {
@ -409,7 +409,7 @@ export async function runKloIngest(
throw new Error(
args.runId
? `Local ingest run or report "${args.runId}" was not found`
: 'No local ingest reports were found. Run `klo ingest --all` first.',
: 'No local ingest reports were found. Run `ktx ingest --all` first.',
);
}
await writeReportRecord(report, args.outputMode, io, {

View file

@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import type { KloCliIo } from '../cli-runtime.js';
import type { KtxCliIo } from '../cli-runtime.js';
import { resolveOutputMode } from './mode.js';
function ioWith(isTTY: boolean | undefined): KloCliIo {
function ioWith(isTTY: boolean | undefined): KtxCliIo {
return {
stdout: { isTTY, write: () => {} },
stderr: { write: () => {} },
@ -24,14 +24,14 @@ describe('resolveOutputMode', () => {
expect(() => resolveOutputMode({ explicit: 'fancy', io: ioWith(true), env: {} })).toThrow(/Invalid --output/);
});
it('honors KLO_OUTPUT env var when no explicit value', () => {
expect(resolveOutputMode({ io: ioWith(true), env: { KLO_OUTPUT: 'plain' } })).toBe('plain');
expect(resolveOutputMode({ io: ioWith(false), env: { KLO_OUTPUT: 'pretty' } })).toBe('pretty');
expect(resolveOutputMode({ io: ioWith(false), env: { KLO_OUTPUT: 'json' } })).toBe('json');
it('honors KTX_OUTPUT env var when no explicit value', () => {
expect(resolveOutputMode({ io: ioWith(true), env: { KTX_OUTPUT: 'plain' } })).toBe('plain');
expect(resolveOutputMode({ io: ioWith(false), env: { KTX_OUTPUT: 'pretty' } })).toBe('pretty');
expect(resolveOutputMode({ io: ioWith(false), env: { KTX_OUTPUT: 'json' } })).toBe('json');
});
it('throws on unknown KLO_OUTPUT', () => {
expect(() => resolveOutputMode({ io: ioWith(true), env: { KLO_OUTPUT: 'fancy' } })).toThrow(/Invalid KLO_OUTPUT/);
it('throws on unknown KTX_OUTPUT', () => {
expect(() => resolveOutputMode({ io: ioWith(true), env: { KTX_OUTPUT: 'fancy' } })).toThrow(/Invalid KTX_OUTPUT/);
});
it('returns plain when CI is set to a truthy value', () => {
@ -54,7 +54,7 @@ describe('resolveOutputMode', () => {
expect(resolveOutputMode({ io: ioWith(undefined), env: {} })).toBe('plain');
});
it('explicit value beats KLO_OUTPUT env var', () => {
expect(resolveOutputMode({ explicit: 'json', io: ioWith(true), env: { KLO_OUTPUT: 'plain' } })).toBe('json');
it('explicit value beats KTX_OUTPUT env var', () => {
expect(resolveOutputMode({ explicit: 'json', io: ioWith(true), env: { KTX_OUTPUT: 'plain' } })).toBe('json');
});
});

View file

@ -1,17 +1,17 @@
import type { KloCliIo } from '../cli-runtime.js';
import type { KtxCliIo } from '../cli-runtime.js';
export type KloOutputMode = 'pretty' | 'plain' | 'json';
export type KtxOutputMode = 'pretty' | 'plain' | 'json';
const MODES: ReadonlySet<string> = new Set(['pretty', 'plain', 'json']);
export interface ResolveOutputModeArgs {
explicit?: string;
json?: boolean;
io: KloCliIo;
io: KtxCliIo;
env?: NodeJS.ProcessEnv;
}
export function resolveOutputMode(args: ResolveOutputModeArgs): KloOutputMode {
export function resolveOutputMode(args: ResolveOutputModeArgs): KtxOutputMode {
if (args.json === true) {
return 'json';
}
@ -19,15 +19,15 @@ export function resolveOutputMode(args: ResolveOutputModeArgs): KloOutputMode {
if (!MODES.has(args.explicit)) {
throw new Error(`Invalid --output value: ${args.explicit}. Expected one of pretty, plain, json.`);
}
return args.explicit as KloOutputMode;
return args.explicit as KtxOutputMode;
}
const env = args.env ?? process.env;
const envMode = env.KLO_OUTPUT;
const envMode = env.KTX_OUTPUT;
if (envMode !== undefined && envMode !== '') {
if (!MODES.has(envMode)) {
throw new Error(`Invalid KLO_OUTPUT value: ${envMode}. Expected one of pretty, plain, json.`);
throw new Error(`Invalid KTX_OUTPUT value: ${envMode}. Expected one of pretty, plain, json.`);
}
return envMode as KloOutputMode;
return envMode as KtxOutputMode;
}
const ci = env.CI;
if (ci !== undefined && ci !== '' && ci !== '0' && ci !== 'false') {

View file

@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest';
import type { KloCliIo } from '../cli-runtime.js';
import type { KtxCliIo } from '../cli-runtime.js';
import { printList, type PrintListColumn } from './print-list.js';
import { SYMBOLS } from './symbols.js';
function recorder(): { io: KloCliIo; out: () => string; err: () => string } {
function recorder(): { io: KtxCliIo; out: () => string; err: () => string } {
let stdout = '';
let stderr = '';
return {

View file

@ -1,5 +1,5 @@
import type { KloCliIo } from '../cli-runtime.js';
import type { KloOutputMode } from './mode.js';
import type { KtxCliIo } from '../cli-runtime.js';
import type { KtxOutputMode } from './mode.js';
import { bold, dim, SYMBOLS } from './symbols.js';
export interface PrintListColumn<Row> {
@ -24,8 +24,8 @@ export interface PrintListArgs<Row> {
groupBy?: keyof Row & string;
emptyMessage: string;
command: string;
mode: KloOutputMode;
io: KloCliIo;
mode: KtxOutputMode;
io: KtxCliIo;
}
export function printList<Row extends object>(args: PrintListArgs<Row>): void {

View file

@ -1,9 +1,9 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initKloProject } from '@klo/context/project';
import { initKtxProject } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { runKloKnowledge } from './knowledge.js';
import { runKtxKnowledge } from './knowledge.js';
function makeIo() {
let stdout = '';
@ -26,11 +26,11 @@ function makeIo() {
};
}
describe('runKloKnowledge', () => {
describe('runKtxKnowledge', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-knowledge-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-knowledge-'));
});
afterEach(async () => {
@ -39,11 +39,11 @@ describe('runKloKnowledge', () => {
it('writes, reads, lists, and searches knowledge pages', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const writeIo = makeIo();
await expect(
runKloKnowledge(
runKtxKnowledge(
{
command: 'write',
projectDir,
@ -63,33 +63,33 @@ describe('runKloKnowledge', () => {
const readIo = makeIo();
await expect(
runKloKnowledge({ command: 'read', projectDir, key: 'metrics/revenue', userId: 'local' }, readIo.io),
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.');
const listIo = makeIo();
await expect(runKloKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
expect(listIo.stdout()).toContain('GLOBAL\tmetrics/revenue\tRevenue');
const searchIo = makeIo();
await expect(
runKloKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io),
runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io),
).resolves.toBe(0);
expect(searchIo.stdout()).toContain('metrics/revenue');
});
it('explains empty search results for a project without wiki pages', async () => {
const projectDir = join(tempDir, 'empty-project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const searchIo = makeIo();
await expect(
runKloKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io),
runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io),
).resolves.toBe(0);
expect(searchIo.stdout()).toBe('');
expect(searchIo.stderr()).toContain('No local wiki pages found');
expect(searchIo.stderr()).toContain('klo wiki write');
expect(searchIo.stderr()).toContain('ktx wiki write');
});
});

View file

@ -1,13 +1,13 @@
import { loadKloProject } from '@klo/context/project';
import { loadKtxProject } from '@ktx/context/project';
import {
type LocalKnowledgeScope,
listLocalKnowledgePages,
readLocalKnowledgePage,
searchLocalKnowledgePages,
writeLocalKnowledgePage,
} from '@klo/context/wiki';
} from '@ktx/context/wiki';
export type KloKnowledgeArgs =
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 }
@ -24,14 +24,14 @@ export type KloKnowledgeArgs =
slRefs: string[];
};
interface KloKnowledgeIo {
interface KtxKnowledgeIo {
stdout: { write(chunk: string): void };
stderr: { write(chunk: string): void };
}
export async function runKloKnowledge(args: KloKnowledgeArgs, io: KloKnowledgeIo = process): Promise<number> {
export async function runKtxKnowledge(args: KtxKnowledgeArgs, io: KtxKnowledgeIo = process): Promise<number> {
try {
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
if (args.command === 'list') {
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
for (const page of pages) {
@ -56,11 +56,11 @@ export async function runKloKnowledge(args: KloKnowledgeArgs, io: KloKnowledgeIo
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 \`klo wiki write <key> --summary <summary> --content <content>\` or run ingest.\n`,
`No local wiki pages found in ${project.projectDir}. Create one with \`ktx wiki write <key> --summary <summary> --content <content>\` or run ingest.\n`,
);
} else {
io.stderr.write(
`No local wiki pages matched "${args.query}". Run \`klo wiki list\` to inspect available pages.\n`,
`No local wiki pages matched "${args.query}". Run \`ktx wiki list\` to inspect available pages.\n`,
);
}
return 0;

View file

@ -1,15 +1,15 @@
import { join } from 'node:path';
import { createBigQueryLiveDatabaseIntrospection, isKloBigQueryConnectionConfig } from '@klo/connector-bigquery';
import { createClickHouseLiveDatabaseIntrospection, isKloClickHouseConnectionConfig } from '@klo/connector-clickhouse';
import { createMysqlLiveDatabaseIntrospection, isKloMysqlConnectionConfig } from '@klo/connector-mysql';
import { createBigQueryLiveDatabaseIntrospection, isKtxBigQueryConnectionConfig } from '@ktx/connector-bigquery';
import { createClickHouseLiveDatabaseIntrospection, isKtxClickHouseConnectionConfig } from '@ktx/connector-clickhouse';
import { createMysqlLiveDatabaseIntrospection, isKtxMysqlConnectionConfig } from '@ktx/connector-mysql';
import {
createPostgresLiveDatabaseIntrospection,
isKloPostgresConnectionConfig,
type KloPostgresConnectionConfig,
KloPostgresHistoricSqlQueryClient,
} from '@klo/connector-postgres';
import { createSqliteLiveDatabaseIntrospection, isKloSqliteConnectionConfig } from '@klo/connector-sqlite';
import { createSqlServerLiveDatabaseIntrospection, isKloSqlServerConnectionConfig } from '@klo/connector-sqlserver';
isKtxPostgresConnectionConfig,
type KtxPostgresConnectionConfig,
KtxPostgresHistoricSqlQueryClient,
} from '@ktx/connector-postgres';
import { createSqliteLiveDatabaseIntrospection, isKtxSqliteConnectionConfig } from '@ktx/connector-sqlite';
import { createSqlServerLiveDatabaseIntrospection, isKtxSqlServerConnectionConfig } from '@ktx/connector-sqlserver';
import {
createDaemonLiveDatabaseIntrospection,
createDefaultLocalIngestAdapters,
@ -17,9 +17,9 @@ import {
type LiveDatabaseIntrospectionPort,
LiveDatabaseSourceAdapter,
type SourceAdapter,
} from '@klo/context/ingest';
import type { KloLocalProject } from '@klo/context/project';
import { createHttpSqlAnalysisPort } from '@klo/context/sql-analysis';
} from '@ktx/context/ingest';
import type { KtxLocalProject } from '@ktx/context/project';
import { createHttpSqlAnalysisPort } from '@ktx/context/sql-analysis';
function hasSnowflakeDriver(connection: unknown): boolean {
return (
@ -29,8 +29,8 @@ function hasSnowflakeDriver(connection: unknown): boolean {
);
}
function createKloCliLiveDatabaseIntrospection(
project: KloLocalProject,
function createKtxCliLiveDatabaseIntrospection(
project: KtxLocalProject,
options: DefaultLocalIngestAdaptersOptions = {},
): LiveDatabaseIntrospectionPort {
const daemon = createDaemonLiveDatabaseIntrospection({
@ -60,29 +60,29 @@ function createKloCliLiveDatabaseIntrospection(
return {
async extractSchema(connectionId: string) {
const connection = project.config.connections[connectionId];
if (isKloPostgresConnectionConfig(connection)) {
if (isKtxPostgresConnectionConfig(connection)) {
return postgres.extractSchema(connectionId);
}
if (isKloSqliteConnectionConfig(connection)) {
if (isKtxSqliteConnectionConfig(connection)) {
return sqlite.extractSchema(connectionId);
}
if (isKloMysqlConnectionConfig(connection)) {
if (isKtxMysqlConnectionConfig(connection)) {
return mysql.extractSchema(connectionId);
}
if (isKloClickHouseConnectionConfig(connection)) {
if (isKtxClickHouseConnectionConfig(connection)) {
return clickhouse.extractSchema(connectionId);
}
if (isKloSqlServerConnectionConfig(connection)) {
if (isKtxSqlServerConnectionConfig(connection)) {
return sqlserver.extractSchema(connectionId);
}
if (isKloBigQueryConnectionConfig(connection)) {
if (isKtxBigQueryConnectionConfig(connection)) {
return bigquery.extractSchema(connectionId);
}
if (hasSnowflakeDriver(connection)) {
const { createSnowflakeLiveDatabaseIntrospection, isKloSnowflakeConnectionConfig } = await import(
'@klo/connector-snowflake'
const { createSnowflakeLiveDatabaseIntrospection, isKtxSnowflakeConnectionConfig } = await import(
'@ktx/connector-snowflake'
);
if (!isKloSnowflakeConnectionConfig(connection)) {
if (!isKtxSnowflakeConnectionConfig(connection)) {
return daemon.extractSchema(connectionId);
}
const snowflake = createSnowflakeLiveDatabaseIntrospection({
@ -95,13 +95,13 @@ function createKloCliLiveDatabaseIntrospection(
};
}
interface KloCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
interface KtxCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
historicSqlConnectionId?: string;
sqlAnalysisUrl?: string;
}
function isEnabledPostgresHistoricSqlConnection(connection: KloPostgresConnectionConfig | undefined): boolean {
if (!connection || !isKloPostgresConnectionConfig(connection)) {
function isEnabledPostgresHistoricSqlConnection(connection: KtxPostgresConnectionConfig | undefined): boolean {
if (!connection || !isKtxPostgresConnectionConfig(connection)) {
return false;
}
const historicSql =
@ -113,16 +113,16 @@ function isEnabledPostgresHistoricSqlConnection(connection: KloPostgresConnectio
return historicSql?.enabled === true && historicSql.dialect === 'postgres';
}
function createEphemeralPostgresHistoricSqlClient(project: KloLocalProject, connectionId: string) {
const connection = project.config.connections[connectionId] as KloPostgresConnectionConfig | undefined;
if (!isKloPostgresConnectionConfig(connection)) {
function createEphemeralPostgresHistoricSqlClient(project: KtxLocalProject, connectionId: string) {
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
if (!isKtxPostgresConnectionConfig(connection)) {
throw new Error(
`Historic SQL local ingest requires a Postgres connection, got ${String(connection?.driver ?? 'unknown')}`,
);
}
return {
async executeQuery(sql: string, params?: unknown[]) {
const client = new KloPostgresHistoricSqlQueryClient({
const client = new KtxPostgresHistoricSqlQueryClient({
connectionId,
connection,
});
@ -135,12 +135,12 @@ function createEphemeralPostgresHistoricSqlClient(project: KloLocalProject, conn
};
}
function historicSqlOptionsForLocalRun(project: KloLocalProject, options: KloCliLocalIngestAdaptersOptions) {
function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCliLocalIngestAdaptersOptions) {
const connectionId = options.historicSqlConnectionId;
if (!connectionId) {
return undefined;
}
const connection = project.config.connections[connectionId] as KloPostgresConnectionConfig | undefined;
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
if (!isEnabledPostgresHistoricSqlConnection(connection)) {
return undefined;
}
@ -148,18 +148,18 @@ function historicSqlOptionsForLocalRun(project: KloLocalProject, options: KloCli
sqlAnalysis: createHttpSqlAnalysisPort({
baseUrl:
options.sqlAnalysisUrl ??
process.env.KLO_SQL_ANALYSIS_URL ??
process.env.KLO_DAEMON_URL ??
process.env.KTX_SQL_ANALYSIS_URL ??
process.env.KTX_DAEMON_URL ??
'http://127.0.0.1:8765',
}),
postgresQueryClient: createEphemeralPostgresHistoricSqlClient(project, connectionId),
postgresBaselineRootDir: join(project.projectDir, '.klo/cache/historic-sql'),
postgresBaselineRootDir: join(project.projectDir, '.ktx/cache/historic-sql'),
};
}
export function createKloCliLocalIngestAdapters(
project: KloLocalProject,
options: KloCliLocalIngestAdaptersOptions = {},
export function createKtxCliLocalIngestAdapters(
project: KtxLocalProject,
options: KtxCliLocalIngestAdaptersOptions = {},
): SourceAdapter[] {
const historicSql = historicSqlOptionsForLocalRun(project, options);
const base = createDefaultLocalIngestAdapters(project, {
@ -167,7 +167,7 @@ export function createKloCliLocalIngestAdapters(
...(historicSql ? { historicSql } : {}),
});
const liveDatabase = new LiveDatabaseSourceAdapter({
introspection: createKloCliLiveDatabaseIntrospection(project, options),
introspection: createKtxCliLiveDatabaseIntrospection(project, options),
});
return base.map((adapter) => (adapter.source === 'live-database' ? liveDatabase : adapter));
}

View file

@ -1,9 +1,9 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initKloProject, loadKloProject } from '@klo/context/project';
import { initKtxProject, loadKtxProject } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createKloCliScanConnector } from './local-scan-connectors.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js';
const bigQueryMock = vi.hoisted(() => ({
constructorInputs: [] as Array<{
@ -13,10 +13,10 @@ const bigQueryMock = vi.hoisted(() => ({
}>,
}));
vi.mock('@klo/connector-bigquery', () => ({
isKloBigQueryConnectionConfig: (connection: { driver?: unknown } | undefined) =>
vi.mock('@ktx/connector-bigquery', () => ({
isKtxBigQueryConnectionConfig: (connection: { driver?: unknown } | undefined) =>
String(connection?.driver ?? '').toLowerCase() === 'bigquery',
KloBigQueryScanConnector: class {
KtxBigQueryScanConnector: class {
readonly id: string;
readonly driver = 'bigquery';
@ -27,12 +27,12 @@ vi.mock('@klo/connector-bigquery', () => ({
},
}));
describe('createKloCliScanConnector', () => {
describe('createKtxCliScanConnector', () => {
let tempDir: string;
beforeEach(async () => {
bigQueryMock.constructorInputs.length = 0;
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-scan-connector-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-scan-connector-'));
});
afterEach(async () => {
@ -40,9 +40,9 @@ describe('createKloCliScanConnector', () => {
});
it('creates a native sqlite connector from standalone config', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -54,9 +54,9 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
const connector = await createKloCliScanConnector(project, 'warehouse');
const connector = await createKtxCliScanConnector(project, 'warehouse');
expect(connector.id).toBe('sqlite:warehouse');
expect(connector.driver).toBe('sqlite');
@ -66,9 +66,9 @@ describe('createKloCliScanConnector', () => {
['maxBytesBilled', ' maxBytesBilled: 123456789', 123456789],
['max_bytes_billed', ' max_bytes_billed: "987654321"', '987654321'],
])('passes BigQuery %s from standalone config', async (_label, byteCapLine, expectedMaxBytesBilled) => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -81,9 +81,9 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
const connector = await createKloCliScanConnector(project, 'warehouse');
const connector = await createKtxCliScanConnector(project, 'warehouse');
expect(connector.id).toBe('bigquery:warehouse');
expect(connector.driver).toBe('bigquery');
@ -96,9 +96,9 @@ describe('createKloCliScanConnector', () => {
});
it('does not create a standalone PostHog scan connector', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -111,17 +111,17 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
await expect(createKloCliScanConnector(project, 'product')).rejects.toThrow(
'Connection "product" uses driver "posthog", which has no native standalone KLO scan connector',
await expect(createKtxCliScanConnector(project, 'product')).rejects.toThrow(
'Connection "product" uses driver "posthog", which has no native standalone KTX scan connector',
);
});
it('throws for structural daemon-only fallback configs', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -132,17 +132,17 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
await expect(createKloCliScanConnector(project, 'warehouse')).rejects.toThrow(
'Connection "warehouse" uses driver "duckdb", which has no native standalone KLO scan connector',
await expect(createKtxCliScanConnector(project, 'warehouse')).rejects.toThrow(
'Connection "warehouse" uses driver "duckdb", which has no native standalone KTX scan connector',
);
});
it('throws a clear error when the connection block has no driver field', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -154,10 +154,10 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
await expect(createKloCliScanConnector(project, 'warehouse')).rejects.toThrow(
'Connection "warehouse" has no `driver` field in klo.yaml',
await expect(createKtxCliScanConnector(project, 'warehouse')).rejects.toThrow(
'Connection "warehouse" has no `driver` field in ktx.yaml',
);
});
});

View file

@ -1,10 +1,10 @@
import type { KloLocalProject } from '@klo/context/project';
import type { KloScanConnector } from '@klo/context/scan';
import type { KtxLocalProject } from '@ktx/context/project';
import type { KtxScanConnector } from '@ktx/context/scan';
const SUPPORTED_DRIVERS = 'sqlite, postgres, mysql, clickhouse, sqlserver, bigquery, snowflake';
function bigQueryMaxBytesBilled(
connection: KloLocalProject['config']['connections'][string],
connection: KtxLocalProject['config']['connections'][string],
): number | string | undefined {
const raw = connection.maxBytesBilled ?? connection.max_bytes_billed;
if (typeof raw === 'number') {
@ -17,55 +17,55 @@ function bigQueryMaxBytesBilled(
return undefined;
}
export async function createKloCliScanConnector(
project: KloLocalProject,
export async function createKtxCliScanConnector(
project: KtxLocalProject,
connectionId: string,
): Promise<KloScanConnector> {
): Promise<KtxScanConnector> {
const connection = project.config.connections[connectionId];
if (!connection) {
throw new Error(`Connection "${connectionId}" is not configured in klo.yaml`);
throw new Error(`Connection "${connectionId}" is not configured in ktx.yaml`);
}
const driver = String(connection.driver ?? '').toLowerCase();
if (!driver) {
throw new Error(
`Connection "${connectionId}" has no \`driver\` field in klo.yaml. Supported drivers: ${SUPPORTED_DRIVERS}.`,
`Connection "${connectionId}" has no \`driver\` field in ktx.yaml. Supported drivers: ${SUPPORTED_DRIVERS}.`,
);
}
if (driver === 'sqlite' || driver === 'sqlite3') {
const { KloSqliteScanConnector, isKloSqliteConnectionConfig } = await import('@klo/connector-sqlite');
if (isKloSqliteConnectionConfig(connection)) {
return new KloSqliteScanConnector({ connectionId, connection, projectDir: project.projectDir });
const { KtxSqliteScanConnector, isKtxSqliteConnectionConfig } = await import('@ktx/connector-sqlite');
if (isKtxSqliteConnectionConfig(connection)) {
return new KtxSqliteScanConnector({ connectionId, connection, projectDir: project.projectDir });
}
}
if (driver === 'postgres' || driver === 'postgresql') {
const { KloPostgresScanConnector, isKloPostgresConnectionConfig } = await import('@klo/connector-postgres');
if (isKloPostgresConnectionConfig(connection)) {
return new KloPostgresScanConnector({ connectionId, connection });
const { KtxPostgresScanConnector, isKtxPostgresConnectionConfig } = await import('@ktx/connector-postgres');
if (isKtxPostgresConnectionConfig(connection)) {
return new KtxPostgresScanConnector({ connectionId, connection });
}
}
if (driver === 'mysql') {
const { KloMysqlScanConnector, isKloMysqlConnectionConfig } = await import('@klo/connector-mysql');
if (isKloMysqlConnectionConfig(connection)) {
return new KloMysqlScanConnector({ connectionId, connection });
const { KtxMysqlScanConnector, isKtxMysqlConnectionConfig } = await import('@ktx/connector-mysql');
if (isKtxMysqlConnectionConfig(connection)) {
return new KtxMysqlScanConnector({ connectionId, connection });
}
}
if (driver === 'clickhouse') {
const { KloClickHouseScanConnector, isKloClickHouseConnectionConfig } = await import('@klo/connector-clickhouse');
if (isKloClickHouseConnectionConfig(connection)) {
return new KloClickHouseScanConnector({ connectionId, connection });
const { KtxClickHouseScanConnector, isKtxClickHouseConnectionConfig } = await import('@ktx/connector-clickhouse');
if (isKtxClickHouseConnectionConfig(connection)) {
return new KtxClickHouseScanConnector({ connectionId, connection });
}
}
if (driver === 'sqlserver') {
const { KloSqlServerScanConnector, isKloSqlServerConnectionConfig } = await import('@klo/connector-sqlserver');
if (isKloSqlServerConnectionConfig(connection)) {
return new KloSqlServerScanConnector({ connectionId, connection });
const { KtxSqlServerScanConnector, isKtxSqlServerConnectionConfig } = await import('@ktx/connector-sqlserver');
if (isKtxSqlServerConnectionConfig(connection)) {
return new KtxSqlServerScanConnector({ connectionId, connection });
}
}
if (driver === 'bigquery') {
const { KloBigQueryScanConnector, isKloBigQueryConnectionConfig } = await import('@klo/connector-bigquery');
if (isKloBigQueryConnectionConfig(connection)) {
const { KtxBigQueryScanConnector, isKtxBigQueryConnectionConfig } = await import('@ktx/connector-bigquery');
if (isKtxBigQueryConnectionConfig(connection)) {
const maxBytesBilled = bigQueryMaxBytesBilled(connection);
return new KloBigQueryScanConnector({
return new KtxBigQueryScanConnector({
connectionId,
connection,
...(maxBytesBilled !== undefined ? { maxBytesBilled } : {}),
@ -73,12 +73,12 @@ export async function createKloCliScanConnector(
}
}
if (driver === 'snowflake') {
const { KloSnowflakeScanConnector, isKloSnowflakeConnectionConfig } = await import('@klo/connector-snowflake');
if (isKloSnowflakeConnectionConfig(connection)) {
return new KloSnowflakeScanConnector({ connectionId, connection });
const { KtxSnowflakeScanConnector, isKtxSnowflakeConnectionConfig } = await import('@ktx/connector-snowflake');
if (isKtxSnowflakeConnectionConfig(connection)) {
return new KtxSnowflakeScanConnector({ connectionId, connection });
}
}
throw new Error(
`Connection "${connectionId}" uses driver "${driver}", which has no native standalone KLO scan connector. Supported drivers: ${SUPPORTED_DRIVERS}.`,
`Connection "${connectionId}" uses driver "${driver}", which has no native standalone KTX scan connector. Supported drivers: ${SUPPORTED_DRIVERS}.`,
);
}

View file

@ -1,5 +1,5 @@
/* @jsxImportSource react */
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { Box, Text } from 'ink';
import React, { type ReactNode } from 'react';
import { buildDemoMetrics, formatCost, formatDuration } from './demo-metrics.js';
@ -315,7 +315,7 @@ function pad(str: string, width: number): string {
return str.length >= width ? str : str + ' '.repeat(width - str.length);
}
const KLO_LOGO_SMALL = [
const KTX_LOGO_SMALL = [
'██╗ ██╗██╗ ██████╗ ',
'██║ ██╔╝██║ ██╔═══██╗',
'█████╔╝ ██║ ██║ ██║',
@ -328,7 +328,7 @@ export function Logo(props: { theme: HudTheme; done: boolean }): ReactNode {
const color = props.done ? props.theme.complete : props.theme.active;
return (
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
{KLO_LOGO_SMALL.map((line, idx) => (
{KTX_LOGO_SMALL.map((line, idx) => (
<Text key={idx} color={color}>
{line}
</Text>
@ -492,7 +492,7 @@ export function ActivityFeed(props: {
</Box>
)}
{/* Results — what KLO has created */}
{/* Results — what KTX has created */}
{insights.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text color={props.theme.text}> Created so far:</Text>
@ -524,7 +524,7 @@ export function ActivityFeed(props: {
<Text color={props.theme.active}>{spinner(props.frame)} Saving to context layer...</Text>
)}
{savedEvent && (
<Text color={props.theme.complete}> Saved your agents can now use the KLO context layer</Text>
<Text color={props.theme.complete}> Saved your agents can now use the KTX context layer</Text>
)}
{/* Phase 7: Completion */}
@ -559,12 +559,12 @@ function CompletionSummary(props: {
<>
<Text color={props.theme.border}>{'─'.repeat(60)}</Text>
<Text bold color={props.theme.complete}>
KLO finished ingesting your data
KTX finished ingesting your data
</Text>
{(sl > 0 || wiki > 0) && (
<>
<Text />
<Text color={props.theme.text}>KLO created:</Text>
<Text color={props.theme.text}>KTX created:</Text>
{sl > 0 && (
<Text color={props.theme.active}>
{' '}📊 {sl} query definition{sl === 1 ? '' : 's'} so agents can write accurate SQL for your data

View file

@ -1,5 +1,5 @@
import { EventEmitter } from 'node:events';
import type { MemoryFlowReplayInput } from '@klo/context/ingest';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest';
import { describe, expect, it, vi } from 'vitest';
import { memoryFlowCommandForKey, renderMemoryFlowInteractively } from './memory-flow-interactive.js';

View file

@ -7,26 +7,26 @@ import {
type MemoryFlowInteractionCommand,
type MemoryFlowInteractionState,
type MemoryFlowReplayInput,
} from '@klo/context/ingest';
} from '@ktx/context/ingest';
interface KloMemoryFlowKey {
interface KtxMemoryFlowKey {
name?: string;
ctrl?: boolean;
}
export interface KloMemoryFlowStdin {
export interface KtxMemoryFlowStdin {
isTTY?: boolean;
isRaw?: boolean;
setRawMode?(value: boolean): void;
resume?(): void;
pause?(): void;
on(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
off?(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
removeListener?(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
on(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
off?(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
removeListener?(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
}
interface KloMemoryFlowInteractiveIo {
stdin?: KloMemoryFlowStdin;
interface KtxMemoryFlowInteractiveIo {
stdin?: KtxMemoryFlowStdin;
stdout: {
isTTY?: boolean;
columns?: number;
@ -35,17 +35,17 @@ interface KloMemoryFlowInteractiveIo {
}
interface RenderMemoryFlowInteractiveOptions {
prepareKeypressEvents?(stdin: KloMemoryFlowStdin): void;
prepareKeypressEvents?(stdin: KtxMemoryFlowStdin): void;
}
function defaultPrepareKeypressEvents(stdin: KloMemoryFlowStdin): void {
function defaultPrepareKeypressEvents(stdin: KtxMemoryFlowStdin): void {
emitKeypressEvents(stdin as Parameters<typeof emitKeypressEvents>[0]);
}
export function memoryFlowCommandForKey(
chunk: string,
search: MemoryFlowInteractionState['search'],
key: KloMemoryFlowKey,
key: KtxMemoryFlowKey,
): MemoryFlowInteractionCommand | null {
if (search.editing) {
if (key.name === 'escape') return 'search-clear';
@ -76,8 +76,8 @@ export function memoryFlowCommandForKey(
}
function removeKeypressListener(
stdin: KloMemoryFlowStdin,
handler: (chunk: string, key: KloMemoryFlowKey) => void,
stdin: KtxMemoryFlowStdin,
handler: (chunk: string, key: KtxMemoryFlowKey) => void,
): void {
if (stdin.off) {
stdin.off('keypress', handler);
@ -86,7 +86,7 @@ function removeKeypressListener(
stdin.removeListener?.('keypress', handler);
}
function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState, io: KloMemoryFlowInteractiveIo): void {
function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState, io: KtxMemoryFlowInteractiveIo): void {
const view = buildMemoryFlowViewModel(input);
io.stdout.write('\u001b[2J\u001b[H');
io.stdout.write(renderMemoryFlowInteractive(view, state, { terminalWidth: io.stdout.columns }));
@ -94,7 +94,7 @@ function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState
export async function renderMemoryFlowInteractively(
input: MemoryFlowReplayInput,
io: KloMemoryFlowInteractiveIo,
io: KtxMemoryFlowInteractiveIo,
options: RenderMemoryFlowInteractiveOptions = {},
): Promise<void> {
const stdin = io.stdin;
@ -119,7 +119,7 @@ export async function renderMemoryFlowInteractively(
stdin.pause?.();
};
const handleKeypress = (_chunk: string, key: KloMemoryFlowKey): void => {
const handleKeypress = (_chunk: string, key: KtxMemoryFlowKey): void => {
const command = memoryFlowCommandForKey(_chunk, state.search, key);
if (!command) {
return;

View file

@ -1,5 +1,5 @@
/* @jsxImportSource react */
import type { MemoryFlowReplayInput } from '@klo/context/ingest';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest';
import { render as renderInkTest } from 'ink-testing-library';
import React, { type ReactNode } from 'react';
import { describe, expect, it, vi } from 'vitest';
@ -9,7 +9,7 @@ import {
renderMemoryFlowTui,
sanitizeMemoryFlowTuiError,
startLiveMemoryFlowTui,
type KloMemoryFlowTuiIo,
type KtxMemoryFlowTuiIo,
type MemoryFlowInkInstance,
type MemoryFlowInkRenderOptions,
} from './memory-flow-tui.js';
@ -72,7 +72,7 @@ function packagedReplayInput(overrides: Partial<MemoryFlowReplayInput> = {}): Me
};
}
function makeIo(): { io: KloMemoryFlowTuiIo; stderr: () => string } {
function makeIo(): { io: KtxMemoryFlowTuiIo; stderr: () => string } {
let stderr = '';
return { io: { stdin: { isTTY: true, setRawMode: vi.fn() }, stdout: { isTTY: true, columns: 120, write: vi.fn() }, stderr: { write(chunk: string) { stderr += chunk; } } }, stderr: () => stderr };
}
@ -103,7 +103,7 @@ describe('sanitizeMemoryFlowTuiError', () => {
});
describe('MemoryFlowTuiApp', () => {
it('always shows the KLO logo', () => {
it('always shows the KTX logo', () => {
const { lastFrame } = renderInkTest(<MemoryFlowTuiApp input={replayInput()} terminalWidth={120} onExit={vi.fn()} showBoot={false} />);
expect(lastFrame()).toContain('█████╔╝');
});
@ -198,12 +198,12 @@ describe('MemoryFlowTuiApp', () => {
expect(frame).toContain('Created so far:');
expect(frame).toContain('order lifecycle');
expect(frame).toContain('customer metrics');
expect(frame).toContain('KLO finished ingesting your data');
expect(frame).toContain('klo sl list');
expect(frame).toContain('klo wiki list');
expect(frame).toContain('klo serve --mcp stdio --user-id local');
expect(frame).not.toContain(['klo', 'ask'].join(' '));
expect(frame).not.toContain(['klo', 'mcp'].join(' '));
expect(frame).toContain('KTX finished ingesting your data');
expect(frame).toContain('ktx sl list');
expect(frame).toContain('ktx wiki list');
expect(frame).toContain('ktx serve --mcp stdio --user-id local');
expect(frame).not.toContain(['ktx', 'ask'].join(' '));
expect(frame).not.toContain(['ktx', 'mcp'].join(' '));
});
it('handles quit while running', async () => {
@ -254,7 +254,7 @@ describe('MemoryFlowTuiApp', () => {
it('hides completion while running', () => {
const { lastFrame } = renderInkTest(<MemoryFlowTuiApp input={runningReplayInput()} terminalWidth={120} onExit={vi.fn()} showBoot={false} />);
expect(lastFrame()).not.toContain('KLO finished ingesting');
expect(lastFrame()).not.toContain('KTX finished ingesting');
});
});

View file

@ -12,7 +12,7 @@ import {
reduceMemoryFlowInteractionState,
selectedMemoryFlowColumn,
selectedMemoryFlowDetails,
} from '@klo/context/ingest';
} from '@ktx/context/ingest';
import { Box, Text, render as renderInkRuntime, useApp, useInput } from 'ink';
import React, { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { buildDemoMetrics } from './demo-metrics.js';
@ -56,7 +56,7 @@ const STAGE_LABELS = {
saved: 'MEMORY',
} satisfies Record<MemoryFlowColumnId, string>;
export interface KloMemoryFlowTuiIo {
export interface KtxMemoryFlowTuiIo {
stdin?: { isTTY?: boolean; setRawMode?(value: boolean): void };
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
stderr: { write(chunk: string): void };
@ -76,9 +76,9 @@ export interface MemoryFlowInkInstance {
}
export interface MemoryFlowInkRenderOptions {
stdin?: KloMemoryFlowTuiIo['stdin'];
stdout: KloMemoryFlowTuiIo['stdout'];
stderr: KloMemoryFlowTuiIo['stderr'];
stdin?: KtxMemoryFlowTuiIo['stdin'];
stdout: KtxMemoryFlowTuiIo['stdout'];
stderr: KtxMemoryFlowTuiIo['stderr'];
exitOnCtrlC: boolean;
patchConsole: boolean;
maxFps: number;
@ -429,7 +429,7 @@ export function MemoryFlowTuiApp(props: MemoryFlowTuiAppProps): ReactNode {
function renderTree(
input: MemoryFlowReplayInput,
io: KloMemoryFlowTuiIo,
io: KtxMemoryFlowTuiIo,
onExit: () => void,
options: RenderTreeOptions = {},
): ReactNode {
@ -459,7 +459,7 @@ function renderInk(tree: ReactNode, options: MemoryFlowInkRenderOptions): Memory
}) as MemoryFlowInkInstance;
}
function renderOptions(io: KloMemoryFlowTuiIo): MemoryFlowInkRenderOptions {
function renderOptions(io: KtxMemoryFlowTuiIo): MemoryFlowInkRenderOptions {
return {
stdin: io.stdin,
stdout: io.stdout,
@ -491,7 +491,7 @@ function resolveTiming(options: RenderMemoryFlowTuiOptions): MemoryFlowTuiTiming
export async function renderMemoryFlowTui(
input: MemoryFlowReplayInput,
io: KloMemoryFlowTuiIo,
io: KtxMemoryFlowTuiIo,
options: RenderMemoryFlowTuiOptions = {},
): Promise<boolean> {
let instance: MemoryFlowInkInstance | null = null;
@ -516,7 +516,7 @@ export async function renderMemoryFlowTui(
export async function startLiveMemoryFlowTui(
input: MemoryFlowReplayInput,
io: KloMemoryFlowTuiIo,
io: KtxMemoryFlowTuiIo,
options: StartLiveMemoryFlowTuiOptions = {},
): Promise<MemoryFlowTuiLiveSession | null> {
let instance: MemoryFlowInkInstance | null = null;

Some files were not shown because too many files have changed in this diff Show more