2026-05-19 18:18:56 +02:00
|
|
|
import { readFile, writeFile } from 'node:fs/promises';
|
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
|
|
|
import { resolveKtxConfigReference } from './context/core/config-reference.js';
|
|
|
|
|
import { type KtxProjectConfig, type KtxProjectEmbeddingConfig, serializeKtxProjectConfig } from './context/project/config.js';
|
|
|
|
|
import { loadKtxProject } from './context/project/project.js';
|
|
|
|
|
import { markKtxSetupStateStepComplete, readKtxSetupState } from './context/project/setup-config.js';
|
|
|
|
|
import type { KtxEmbeddingConfig } from './llm/types.js';
|
|
|
|
|
import { type KtxEmbeddingHealthCheckResult, runKtxEmbeddingHealthCheck } from './llm/embedding-health.js';
|
2026-05-10 23:51:24 +02:00
|
|
|
import type { KtxCliIo } from './cli-runtime.js';
|
feat(cli): setup progress spinners, Tab-to-select, and banner polish (#296)
* fix(cli): double the height of the setup banner t crossbar
* fix(cli): unify setup multi-select hints and make Tab the select key
The six interactive multi-select surfaces in `ktx setup` documented three
different hint voices, one had no hint at all, and they named two different
select keys (Space vs Tab). Tab is the only key that can toggle selection
without colliding with type-to-search input, so make it the single documented
select key everywhere and compose every hint from one shared fragment
vocabulary in prompt-navigation.ts.
- Register `updateSettings({ aliases: { tab: 'space' } })` so Tab toggles flat
multiselects; the alias applies only to non-text prompts, leaving typed
search input (schema/Notion) untouched.
- Add the missing hint to the agent-targets prompt and drop the stray
"Space to select … Esc …" info line plus the now-dead writeSetupInfo helper.
- Replace the schema-scope ad-hoc hint with the searchable-multiselect voice
and standardize "filter" -> "search" vocabulary.
- Delete DEFAULT_TREE_PICKER_HELP_TEXT and the unused TreePickerChrome.helpText
seam; render the shared tree hint instead.
* refactor(cli): show LLM check progress for every setup backend
Rename runLlmHealthCheckWithProgress to validateModelWithProgress and
wrap the Claude subscription and Codex auth probes in the same spinner
progress as the Anthropic API and Vertex backends, so each backend shows
consistent "Checking <provider> LLM" output during setup.
* feat(cli): add ktx-orange progress spinners to setup steps
Add a shared runWithCliSpinner helper and a TTY-aware createCliSpinner:
an animated clack spinner in a terminal, and a static stderr-only spinner
before raw-mode pickers (the table tree picker and demo tour), where the
animated spinner's stdin grab would otherwise corrupt the next prompt.
Wrap the slow setup waits in progress spinners: managed runtime install,
embedding daemon start + first-run model download, embeddings health
check, the connection-test gate, and source validation / dbt clone /
Metabase discovery. Recolor every spinner frame from clack's magenta to
the ktx mascot orange (#FF8A4C) via the static helper and clack's
styleFrame option.
2026-06-12 16:43:10 +02:00
|
|
|
import { createCliSpinner, errorMessage, writePrefixedLines, type KtxCliSpinner } from './clack.js';
|
2026-05-11 15:50:34 +02:00
|
|
|
import {
|
|
|
|
|
ensureManagedLocalEmbeddingsDaemon,
|
|
|
|
|
managedLocalEmbeddingHealthConfig,
|
|
|
|
|
type ManagedLocalEmbeddingsDaemon,
|
|
|
|
|
} from './managed-local-embeddings.js';
|
test: split cli tests from source tree (#216)
* feat(cli): define full warehouse dialect contract
* test(cli): keep dialect edge tests focused
* fix(cli): stabilize dialect contract foundation
* refactor(connectors): own read-only query preparation
* refactor(connectors): resolve dialects through registry
* refactor(connectors): keep concrete dialect classes internal
* chore(workspace): enforce dialect import boundary
* refactor(cli): resolve relationship dialect at scan boundary
* refactor(cli): use dialect display parsing for entity details
* refactor(cli): use dialect display parsing for warehouse catalog
* refactor(cli): use dialect SQL in relationship workflows
* test(cli): verify solid dialect scan workflow closure
* test: split cli tests from source tree
* refactor(cli): standardize BigQuery scope listing
* feat(sqlite): implement connector scope listing
* test(connectors): cover required table listing
* feat(cli): add warehouse driver registry
* refactor(setup): route scope discovery through driver registry
* refactor(cli): route local query execution through driver registry
* refactor(historic-sql): route dialect support through driver registry
* refactor(cli): test warehouse connections through driver registry
* fix(cli): close driver registry type export gaps
* Improve setup daemon diagnostics
* refactor(setup): centralize rail-prefixed diagnostics + query-history fallback
Extract errorMessage, writePrefixedLines, and flushPrefixedBufferedCommandOutput
into clack.ts so the setup wizard, managed daemons, and embedding/agent steps
share one rail-formatted writer. setup-databases.ts also adds a
"disable query history and retry" option when the schema-context build fails
and query history is the likely culprit, surfaced via a new
failed-query-history-unavailable status.
* fix(cli): carry catalog through the picker so BigQuery/Snowflake/SQL Server scope filters match
The setup picker's KtxTableListEntry was a 2-level { schema, name }, so
qualifiedTableId always wrote db.name into enabled_tables. When BigQuery,
Snowflake, or SQL Server later ran fast ingest, their introspect step filtered
the scope set with scopedTableNames(scope, { catalog: projectId|database, db })
— catalog was non-null on the introspect side but null in the scope refs, so
every entry was rejected, the live-database adapter staged zero table files,
and detect() failed with 'Adapter "live-database" did not recognize fetched
source output'.
Align the picker boundary with the canonical 3-level KtxTableRef:
- Add catalog: string | null to KtxTableListEntry.
- BigQuery/Snowflake/SQL Server listTables populate catalog from the
resolved projectId / database; Postgres/MySQL/ClickHouse/SQLite set null.
- qualifiedTableId emits catalog.schema.name when catalog is non-null
(resolveEnabledTables already accepts the 3-part shape) and
schemasFromEnabledTables now goes through parseDottedTableEntry so it
recovers the schema correctly from both 2-part and 3-part entries.
- Export parseDottedTableEntry from enabled-tables.ts (@internal) for picker
reuse.
Update listTables expectations in all seven connector tests and the setup /
picker test fixtures. Add a picker regression test that covers the
catalog-bearing round-trip (save + refine).
* fix(cli): allow debug telemetry under opt-out env
2026-05-26 08:49:05 +02:00
|
|
|
import { ManagedPythonDaemonStartError } from './managed-python-daemon.js';
|
2026-05-11 15:50:34 +02:00
|
|
|
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
|
2026-05-13 17:01:48 +02:00
|
|
|
import { withTextInputNavigation } from './prompt-navigation.js';
|
2026-05-10 23:12:26 +02:00
|
|
|
import { envCredentialReference, writeProjectLocalSecretReference } from './setup-secrets.js';
|
2026-05-13 17:01:48 +02:00
|
|
|
import {
|
|
|
|
|
createKtxSetupPromptAdapter,
|
|
|
|
|
type KtxSetupPromptOption,
|
|
|
|
|
} from './setup-prompts.js';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export type KtxSetupEmbeddingBackend = 'openai' | 'sentence-transformers';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupEmbeddingsArgs {
|
2026-05-10 23:12:26 +02:00
|
|
|
projectDir: string;
|
|
|
|
|
inputMode: 'auto' | 'disabled';
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: string;
|
|
|
|
|
runtimeInstallPolicy: KtxManagedPythonInstallPolicy;
|
2026-05-10 23:51:24 +02:00
|
|
|
embeddingBackend?: KtxSetupEmbeddingBackend;
|
2026-05-10 23:12:26 +02:00
|
|
|
embeddingApiKeyEnv?: string;
|
|
|
|
|
embeddingApiKeyFile?: string;
|
|
|
|
|
forcePrompt?: boolean;
|
|
|
|
|
showPromptInstructions?: boolean;
|
|
|
|
|
skipEmbeddings: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export type KtxSetupEmbeddingsResult =
|
2026-05-10 23:12:26 +02:00
|
|
|
| { status: 'ready'; projectDir: string }
|
|
|
|
|
| { status: 'skipped'; projectDir: string }
|
|
|
|
|
| { status: 'back'; projectDir: string }
|
|
|
|
|
| { status: 'missing-input'; projectDir: string }
|
|
|
|
|
| { status: 'failed'; projectDir: string };
|
|
|
|
|
|
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
|
|
|
/** @internal */
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupEmbeddingsPromptAdapter {
|
2026-05-13 17:01:48 +02:00
|
|
|
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
2026-05-10 23:12:26 +02:00
|
|
|
password(options: { message: string }): Promise<string | undefined>;
|
|
|
|
|
cancel(message: string): void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupEmbeddingsDeps {
|
2026-05-10 23:12:26 +02:00
|
|
|
env?: NodeJS.ProcessEnv;
|
2026-05-10 23:51:24 +02:00
|
|
|
prompts?: KtxSetupEmbeddingsPromptAdapter;
|
|
|
|
|
healthCheck?: (config: KtxEmbeddingConfig) => Promise<KtxEmbeddingHealthCheckResult>;
|
2026-05-11 15:50:34 +02:00
|
|
|
ensureLocalEmbeddings?: (options: {
|
|
|
|
|
cliVersion: string;
|
2026-05-19 18:18:56 +02:00
|
|
|
projectDir: string;
|
2026-05-11 15:50:34 +02:00
|
|
|
installPolicy: KtxManagedPythonInstallPolicy;
|
|
|
|
|
io: KtxCliIo;
|
|
|
|
|
}) => Promise<ManagedLocalEmbeddingsDaemon>;
|
2026-05-12 21:50:41 -07:00
|
|
|
spinner?: () => KtxCliSpinner;
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
type BackendChoice = KtxSetupEmbeddingBackend | 'back';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
const DEFAULTS: Record<
|
2026-05-10 23:51:24 +02:00
|
|
|
KtxSetupEmbeddingBackend,
|
2026-05-10 23:12:26 +02:00
|
|
|
{ model: string; dimensions: number; envName?: string; baseUrl?: string; pathPrefix?: string }
|
|
|
|
|
> = {
|
|
|
|
|
openai: { model: 'text-embedding-3-small', dimensions: 1536, envName: 'OPENAI_API_KEY' },
|
|
|
|
|
'sentence-transformers': {
|
|
|
|
|
model: 'all-MiniLM-L6-v2',
|
|
|
|
|
dimensions: 384,
|
|
|
|
|
baseUrl: 'http://127.0.0.1:8765',
|
|
|
|
|
pathPrefix: '',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
const LOCAL_EMBEDDING_BACKEND: KtxSetupEmbeddingBackend = 'sentence-transformers';
|
2026-05-10 23:12:26 +02:00
|
|
|
const EMBEDDING_OPTION_PROMPT_CONTEXT =
|
2026-06-11 13:49:45 +02:00
|
|
|
'ktx uses embeddings for semantic search over semantic-layer sources, wiki context, schema metadata, ' +
|
2026-05-10 23:12:26 +02:00
|
|
|
'and relationship evidence.';
|
|
|
|
|
const LOCAL_EMBEDDING_HEALTH_TIMEOUT_MS = 120_000;
|
2026-05-19 18:18:56 +02:00
|
|
|
const LOCAL_EMBEDDING_STDERR_TAIL_LINES = 40;
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function createPromptAdapter(): KtxSetupEmbeddingsPromptAdapter {
|
2026-05-13 17:01:48 +02:00
|
|
|
return createKtxSetupPromptAdapter({ selectCancelValue: 'back' });
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 16:26:23 -07:00
|
|
|
async function hasCompletedEmbeddings(projectDir: string, config: KtxProjectConfig): Promise<boolean> {
|
2026-05-10 23:12:26 +02:00
|
|
|
return (
|
2026-05-13 13:55:21 +02:00
|
|
|
(await readKtxSetupState(projectDir)).completed_steps.includes('embeddings') &&
|
2026-05-10 23:12:26 +02:00
|
|
|
config.ingest.embeddings.backend !== 'none' &&
|
|
|
|
|
typeof config.ingest.embeddings.model === 'string' &&
|
|
|
|
|
config.ingest.embeddings.model.length > 0 &&
|
|
|
|
|
config.ingest.embeddings.dimensions > 0
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildProjectEmbeddingConfig(input: {
|
2026-05-10 23:51:24 +02:00
|
|
|
backend: KtxSetupEmbeddingBackend;
|
2026-05-10 23:12:26 +02:00
|
|
|
model: string;
|
|
|
|
|
dimensions: number;
|
|
|
|
|
credentialRef?: string;
|
2026-05-10 23:51:24 +02:00
|
|
|
}): KtxProjectEmbeddingConfig {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (input.backend === 'openai') {
|
|
|
|
|
return {
|
|
|
|
|
backend: 'openai',
|
|
|
|
|
model: input.model,
|
|
|
|
|
dimensions: input.dimensions,
|
|
|
|
|
openai: {
|
|
|
|
|
...(input.credentialRef ? { api_key: input.credentialRef } : {}),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const defaults = DEFAULTS[input.backend];
|
|
|
|
|
return {
|
|
|
|
|
backend: input.backend,
|
|
|
|
|
model: input.model,
|
|
|
|
|
dimensions: input.dimensions,
|
|
|
|
|
sentenceTransformers: {
|
|
|
|
|
base_url: defaults.baseUrl ?? '',
|
|
|
|
|
pathPrefix: defaults.pathPrefix ?? '',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildHealthConfig(input: {
|
2026-05-10 23:51:24 +02:00
|
|
|
backend: KtxSetupEmbeddingBackend;
|
2026-05-10 23:12:26 +02:00
|
|
|
model: string;
|
|
|
|
|
dimensions: number;
|
|
|
|
|
credentialValue?: string;
|
2026-05-10 23:51:24 +02:00
|
|
|
}): KtxEmbeddingConfig {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (input.backend === 'openai') {
|
|
|
|
|
return {
|
|
|
|
|
backend: 'openai',
|
|
|
|
|
model: input.model,
|
|
|
|
|
dimensions: input.dimensions,
|
|
|
|
|
openai: {
|
|
|
|
|
...(input.credentialValue ? { apiKey: input.credentialValue } : {}),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const defaults = DEFAULTS[input.backend];
|
|
|
|
|
return {
|
|
|
|
|
backend: input.backend,
|
|
|
|
|
model: input.model,
|
|
|
|
|
dimensions: input.dimensions,
|
|
|
|
|
sentenceTransformers: {
|
|
|
|
|
baseURL: defaults.baseUrl ?? '',
|
|
|
|
|
pathPrefix: defaults.pathPrefix ?? '',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function embeddingBackendDisplayName(backend: KtxSetupEmbeddingBackend): string {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (backend === 'openai') {
|
|
|
|
|
return 'OpenAI';
|
|
|
|
|
}
|
|
|
|
|
return 'sentence-transformers';
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
async function persistEmbeddingConfig(projectDir: string, embeddings: KtxProjectEmbeddingConfig): Promise<void> {
|
|
|
|
|
const project = await loadKtxProject({ projectDir });
|
2026-05-13 13:55:21 +02:00
|
|
|
const config = {
|
|
|
|
|
...project.config,
|
|
|
|
|
ingest: {
|
|
|
|
|
...project.config.ingest,
|
|
|
|
|
embeddings,
|
|
|
|
|
},
|
|
|
|
|
scan: {
|
|
|
|
|
...project.config.scan,
|
|
|
|
|
enrichment: {
|
|
|
|
|
...project.config.scan.enrichment,
|
2026-05-10 23:12:26 +02:00
|
|
|
embeddings,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-05-13 13:55:21 +02:00
|
|
|
};
|
2026-05-10 23:51:24 +02:00
|
|
|
await writeFile(project.configPath, serializeKtxProjectConfig(config), 'utf-8');
|
2026-05-12 16:26:23 -07:00
|
|
|
await markKtxSetupStateStepComplete(projectDir, 'embeddings');
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function chooseCredentialRef(
|
2026-05-10 23:51:24 +02:00
|
|
|
backend: Extract<KtxSetupEmbeddingBackend, 'openai'>,
|
|
|
|
|
args: KtxSetupEmbeddingsArgs,
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
deps: KtxSetupEmbeddingsDeps,
|
2026-05-10 23:12:26 +02:00
|
|
|
): Promise<{ status: 'ready'; ref: string; value: string } | { status: 'back' | 'missing-input' }> {
|
|
|
|
|
const env = deps.env ?? process.env;
|
|
|
|
|
if (args.embeddingApiKeyEnv) {
|
|
|
|
|
const ref = envCredentialReference(args.embeddingApiKeyEnv);
|
2026-05-10 23:51:24 +02:00
|
|
|
const value = resolveKtxConfigReference(ref, env);
|
2026-05-10 23:12:26 +02:00
|
|
|
if (!value) {
|
|
|
|
|
io.stderr.write(`Missing embedding API key: ${args.embeddingApiKeyEnv} is not set.\n`);
|
|
|
|
|
return { status: 'missing-input' };
|
|
|
|
|
}
|
|
|
|
|
return { status: 'ready', ref, value };
|
|
|
|
|
}
|
|
|
|
|
if (args.embeddingApiKeyFile) {
|
|
|
|
|
const ref = `file:${args.embeddingApiKeyFile}`;
|
|
|
|
|
let value: string | undefined;
|
|
|
|
|
try {
|
2026-05-10 23:51:24 +02:00
|
|
|
value = resolveKtxConfigReference(ref, env);
|
2026-05-10 23:12:26 +02:00
|
|
|
} catch {
|
|
|
|
|
value = undefined;
|
|
|
|
|
}
|
|
|
|
|
if (!value) {
|
|
|
|
|
io.stderr.write(`Missing embedding API key file: ${args.embeddingApiKeyFile}\n`);
|
|
|
|
|
return { status: 'missing-input' };
|
|
|
|
|
}
|
|
|
|
|
return { status: 'ready', ref, value };
|
|
|
|
|
}
|
|
|
|
|
if (args.inputMode === 'disabled') {
|
|
|
|
|
io.stderr.write('Missing embedding API key: pass --embedding-api-key-env or --embedding-api-key-file.\n');
|
|
|
|
|
return { status: 'missing-input' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultEnv = DEFAULTS[backend].envName ?? 'EMBEDDING_API_KEY';
|
|
|
|
|
const prompts = deps.prompts ?? createPromptAdapter();
|
|
|
|
|
const choice = await prompts.select({
|
2026-06-11 13:49:45 +02:00
|
|
|
message: `How should ktx find your ${embeddingBackendDisplayName(backend)} embedding API key?`,
|
2026-05-10 23:12:26 +02:00
|
|
|
options: [
|
|
|
|
|
{ value: 'paste', label: 'Paste a key and save it as a local secret file' },
|
2026-06-04 14:11:08 +02:00
|
|
|
{ value: 'env', label: `Use ${defaultEnv} from the environment` },
|
2026-05-10 23:12:26 +02:00
|
|
|
{ value: 'back', label: 'Back' },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
if (choice === 'back') {
|
|
|
|
|
return { status: 'back' };
|
|
|
|
|
}
|
|
|
|
|
if (choice === 'paste') {
|
|
|
|
|
io.stdout.write(
|
2026-05-12 15:46:56 -07:00
|
|
|
`│ ${[
|
2026-06-11 13:49:45 +02:00
|
|
|
`ktx will save the key in .ktx/secrets/${backend}-api-key with local file permissions,`,
|
2026-05-10 23:51:24 +02:00
|
|
|
'then write a file: reference in ktx.yaml.',
|
2026-05-10 23:12:26 +02:00
|
|
|
].join(' ')}\n`,
|
|
|
|
|
);
|
|
|
|
|
const value = await prompts.password({ message: withTextInputNavigation(`${backend} embedding API key`) });
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
return { status: 'back' };
|
|
|
|
|
}
|
|
|
|
|
if (!value.trim()) {
|
|
|
|
|
return { status: 'missing-input' };
|
|
|
|
|
}
|
|
|
|
|
const ref = await writeProjectLocalSecretReference({
|
|
|
|
|
projectDir: args.projectDir,
|
|
|
|
|
fileName: `${backend}-api-key`,
|
|
|
|
|
value,
|
|
|
|
|
});
|
|
|
|
|
return { status: 'ready', ref, value: value.trim() };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ref = envCredentialReference(defaultEnv);
|
2026-05-10 23:51:24 +02:00
|
|
|
const value = resolveKtxConfigReference(ref, env);
|
2026-05-10 23:12:26 +02:00
|
|
|
if (!value) {
|
|
|
|
|
io.stderr.write(`Missing embedding API key: ${defaultEnv} is not set.\n`);
|
|
|
|
|
return { status: 'missing-input' };
|
|
|
|
|
}
|
|
|
|
|
return { status: 'ready', ref, value };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function chooseEmbeddingBackend(
|
2026-05-10 23:51:24 +02:00
|
|
|
args: KtxSetupEmbeddingsArgs,
|
|
|
|
|
deps: KtxSetupEmbeddingsDeps,
|
2026-05-10 23:12:26 +02:00
|
|
|
): Promise<BackendChoice> {
|
|
|
|
|
if (args.embeddingBackend) {
|
|
|
|
|
return args.embeddingBackend;
|
|
|
|
|
}
|
|
|
|
|
if (args.inputMode === 'disabled') {
|
|
|
|
|
return LOCAL_EMBEDDING_BACKEND;
|
|
|
|
|
}
|
|
|
|
|
const choice = await (deps.prompts ?? createPromptAdapter()).select({
|
2026-06-11 13:49:45 +02:00
|
|
|
message: `Which embedding option should ktx use?\n\n${EMBEDDING_OPTION_PROMPT_CONTEXT}`,
|
2026-05-10 23:12:26 +02:00
|
|
|
options: [
|
|
|
|
|
{ value: 'sentence-transformers', label: 'Local sentence-transformers embeddings' },
|
2026-05-13 17:01:48 +02:00
|
|
|
{ value: 'openai', label: 'OpenAI embeddings', hint: 'recommended' },
|
2026-05-10 23:12:26 +02:00
|
|
|
{ value: 'back', label: 'Back' },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
if (choice === 'openai' || choice === 'sentence-transformers' || choice === 'back') {
|
|
|
|
|
return choice;
|
|
|
|
|
}
|
|
|
|
|
return 'back';
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 18:18:56 +02:00
|
|
|
async function readLocalEmbeddingDaemonStderrTail(stderrLog: string | undefined): Promise<string[]> {
|
|
|
|
|
if (!stderrLog) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const lines = (await readFile(stderrLog, 'utf8'))
|
|
|
|
|
.split(/\r?\n/)
|
|
|
|
|
.map((line) => line.trimEnd())
|
|
|
|
|
.filter((line) => line.trim().length > 0);
|
|
|
|
|
return lines.slice(-LOCAL_EMBEDDING_STDERR_TAIL_LINES);
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function localEmbeddingSetupMessage(message: string, stderrTail: string[] = []): string {
|
|
|
|
|
const lines = [
|
2026-05-10 23:12:26 +02:00
|
|
|
`Local embedding health check failed: ${message}`,
|
2026-06-11 13:49:45 +02:00
|
|
|
'Local embeddings use the ktx-managed Python runtime.',
|
2026-05-20 01:36:54 +02:00
|
|
|
'Prepare the runtime with: ktx admin runtime start --feature local-embeddings',
|
2026-05-11 15:50:34 +02:00
|
|
|
'Use --yes with setup to install and start the runtime without prompting.',
|
|
|
|
|
'The first run may download Python packages and the all-MiniLM-L6-v2 model.',
|
2026-05-19 18:18:56 +02:00
|
|
|
];
|
|
|
|
|
if (stderrTail.length > 0) {
|
2026-06-11 13:49:45 +02:00
|
|
|
lines.push('Recent ktx daemon stderr:', ...stderrTail);
|
2026-05-19 18:18:56 +02:00
|
|
|
}
|
|
|
|
|
return lines.join('\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function promptAfterLocalEmbeddingFailure(
|
2026-05-10 23:51:24 +02:00
|
|
|
deps: KtxSetupEmbeddingsDeps,
|
|
|
|
|
): Promise<'retry' | Extract<KtxSetupEmbeddingBackend, 'openai'> | 'back'> {
|
2026-05-10 23:12:26 +02:00
|
|
|
const choice = await (deps.prompts ?? createPromptAdapter()).select({
|
2026-06-11 13:49:45 +02:00
|
|
|
message: 'Local embeddings are not reachable. Start the local ktx daemon, then retry.',
|
2026-05-10 23:12:26 +02:00
|
|
|
options: [
|
|
|
|
|
{ value: 'retry', label: 'Retry' },
|
|
|
|
|
{ value: 'openai', label: 'Use OpenAI embeddings' },
|
|
|
|
|
{ value: 'back', label: 'Back' },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
if (choice === 'openai' || choice === 'back') {
|
|
|
|
|
return choice;
|
|
|
|
|
}
|
|
|
|
|
return 'retry';
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function healthCheckStartText(backend: KtxSetupEmbeddingBackend, model: string, dimensions: number): string {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (backend === LOCAL_EMBEDDING_BACKEND) {
|
2026-05-17 19:15:09 +02:00
|
|
|
return `Testing local embeddings (${model})`;
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
return `Checking ${backend} embeddings (${model}, ${dimensions} dimensions).`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 21:53:23 -07:00
|
|
|
function startHealthCheckProgress(
|
|
|
|
|
spinner: KtxCliSpinner,
|
|
|
|
|
message: string,
|
|
|
|
|
): { succeed(msg: string): void; fail(msg: string): void } {
|
2026-05-12 21:50:41 -07:00
|
|
|
spinner.start(message);
|
2026-05-10 23:12:26 +02:00
|
|
|
return {
|
2026-05-12 21:50:41 -07:00
|
|
|
succeed(msg: string) {
|
|
|
|
|
spinner.stop(msg);
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
2026-05-12 21:50:41 -07:00
|
|
|
fail(msg: string) {
|
|
|
|
|
spinner.error(msg);
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export async function runKtxSetupEmbeddingsStep(
|
|
|
|
|
args: KtxSetupEmbeddingsArgs,
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
deps: KtxSetupEmbeddingsDeps = {},
|
|
|
|
|
): Promise<KtxSetupEmbeddingsResult> {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (args.skipEmbeddings) {
|
2026-05-12 16:58:09 -07:00
|
|
|
io.stdout.write('│ Embeddings setup skipped.\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
return { status: 'skipped', projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
const project = await loadKtxProject({ projectDir: args.projectDir });
|
2026-05-10 23:12:26 +02:00
|
|
|
if (
|
|
|
|
|
args.forcePrompt !== true &&
|
2026-05-12 16:26:23 -07:00
|
|
|
(await hasCompletedEmbeddings(args.projectDir, project.config)) &&
|
2026-05-10 23:12:26 +02:00
|
|
|
!args.embeddingBackend &&
|
|
|
|
|
!args.embeddingApiKeyEnv &&
|
|
|
|
|
!args.embeddingApiKeyFile
|
|
|
|
|
) {
|
2026-05-12 16:58:09 -07:00
|
|
|
io.stdout.write(`│ Embeddings ready: yes (${project.config.ingest.embeddings.model})\n`);
|
2026-05-10 23:12:26 +02:00
|
|
|
return { status: 'ready', projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const healthCheck =
|
|
|
|
|
deps.healthCheck ??
|
2026-05-10 23:51:24 +02:00
|
|
|
((config: KtxEmbeddingConfig) =>
|
|
|
|
|
runKtxEmbeddingHealthCheck(config, { timeoutMs: LOCAL_EMBEDDING_HEALTH_TIMEOUT_MS }));
|
|
|
|
|
let selectedBackend: KtxSetupEmbeddingBackend | undefined;
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
if (!selectedBackend) {
|
|
|
|
|
const backend = await chooseEmbeddingBackend(args, deps);
|
|
|
|
|
if (backend === 'back') {
|
|
|
|
|
return { status: 'back', projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
selectedBackend = backend;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaults = DEFAULTS[selectedBackend];
|
|
|
|
|
const model = defaults.model;
|
|
|
|
|
const dimensions = defaults.dimensions;
|
|
|
|
|
let credentialRef: string | undefined;
|
|
|
|
|
let credentialValue: string | undefined;
|
|
|
|
|
|
|
|
|
|
if (selectedBackend === 'openai') {
|
|
|
|
|
const credential = await chooseCredentialRef(selectedBackend, args, io, deps);
|
|
|
|
|
if (credential.status === 'back' && !args.embeddingBackend && args.inputMode !== 'disabled') {
|
|
|
|
|
selectedBackend = undefined;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (credential.status !== 'ready') {
|
|
|
|
|
return { status: credential.status, projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
credentialRef = credential.ref;
|
|
|
|
|
credentialValue = credential.value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
let managedLocalEmbeddings: ManagedLocalEmbeddingsDaemon | undefined;
|
|
|
|
|
if (selectedBackend === LOCAL_EMBEDDING_BACKEND) {
|
|
|
|
|
const ensureLocalEmbeddings = deps.ensureLocalEmbeddings ?? ensureManagedLocalEmbeddingsDaemon;
|
|
|
|
|
try {
|
|
|
|
|
managedLocalEmbeddings = await ensureLocalEmbeddings({
|
|
|
|
|
cliVersion: args.cliVersion,
|
2026-05-14 14:35:55 +02:00
|
|
|
projectDir: args.projectDir,
|
2026-05-11 15:50:34 +02:00
|
|
|
installPolicy: args.runtimeInstallPolicy,
|
|
|
|
|
io,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
test: split cli tests from source tree (#216)
* feat(cli): define full warehouse dialect contract
* test(cli): keep dialect edge tests focused
* fix(cli): stabilize dialect contract foundation
* refactor(connectors): own read-only query preparation
* refactor(connectors): resolve dialects through registry
* refactor(connectors): keep concrete dialect classes internal
* chore(workspace): enforce dialect import boundary
* refactor(cli): resolve relationship dialect at scan boundary
* refactor(cli): use dialect display parsing for entity details
* refactor(cli): use dialect display parsing for warehouse catalog
* refactor(cli): use dialect SQL in relationship workflows
* test(cli): verify solid dialect scan workflow closure
* test: split cli tests from source tree
* refactor(cli): standardize BigQuery scope listing
* feat(sqlite): implement connector scope listing
* test(connectors): cover required table listing
* feat(cli): add warehouse driver registry
* refactor(setup): route scope discovery through driver registry
* refactor(cli): route local query execution through driver registry
* refactor(historic-sql): route dialect support through driver registry
* refactor(cli): test warehouse connections through driver registry
* fix(cli): close driver registry type export gaps
* Improve setup daemon diagnostics
* refactor(setup): centralize rail-prefixed diagnostics + query-history fallback
Extract errorMessage, writePrefixedLines, and flushPrefixedBufferedCommandOutput
into clack.ts so the setup wizard, managed daemons, and embedding/agent steps
share one rail-formatted writer. setup-databases.ts also adds a
"disable query history and retry" option when the schema-context build fails
and query history is the likely culprit, surfaced via a new
failed-query-history-unavailable status.
* fix(cli): carry catalog through the picker so BigQuery/Snowflake/SQL Server scope filters match
The setup picker's KtxTableListEntry was a 2-level { schema, name }, so
qualifiedTableId always wrote db.name into enabled_tables. When BigQuery,
Snowflake, or SQL Server later ran fast ingest, their introspect step filtered
the scope set with scopedTableNames(scope, { catalog: projectId|database, db })
— catalog was non-null on the introspect side but null in the scope refs, so
every entry was rejected, the live-database adapter staged zero table files,
and detect() failed with 'Adapter "live-database" did not recognize fetched
source output'.
Align the picker boundary with the canonical 3-level KtxTableRef:
- Add catalog: string | null to KtxTableListEntry.
- BigQuery/Snowflake/SQL Server listTables populate catalog from the
resolved projectId / database; Postgres/MySQL/ClickHouse/SQLite set null.
- qualifiedTableId emits catalog.schema.name when catalog is non-null
(resolveEnabledTables already accepts the 3-part shape) and
schemasFromEnabledTables now goes through parseDottedTableEntry so it
recovers the schema correctly from both 2-part and 3-part entries.
- Export parseDottedTableEntry from enabled-tables.ts (@internal) for picker
reuse.
Update listTables expectations in all seven connector tests and the setup /
picker test fixtures. Add a picker regression test that covers the
catalog-bearing round-trip (save + refine).
* fix(cli): allow debug telemetry under opt-out env
2026-05-26 08:49:05 +02:00
|
|
|
const write = (chunk: string) => io.stderr.write(chunk);
|
|
|
|
|
if (error instanceof ManagedPythonDaemonStartError) {
|
|
|
|
|
const tail = await readLocalEmbeddingDaemonStderrTail(error.stderrLog);
|
|
|
|
|
writePrefixedLines(write, localEmbeddingSetupMessage(error.detail, tail));
|
|
|
|
|
} else {
|
|
|
|
|
writePrefixedLines(write, errorMessage(error));
|
|
|
|
|
}
|
2026-05-11 15:50:34 +02:00
|
|
|
return { status: 'failed', projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const healthConfig =
|
|
|
|
|
selectedBackend === LOCAL_EMBEDDING_BACKEND && managedLocalEmbeddings
|
|
|
|
|
? managedLocalEmbeddingHealthConfig({
|
|
|
|
|
baseUrl: managedLocalEmbeddings.baseUrl,
|
|
|
|
|
model,
|
|
|
|
|
dimensions,
|
|
|
|
|
})
|
|
|
|
|
: buildHealthConfig({
|
|
|
|
|
backend: selectedBackend,
|
|
|
|
|
model,
|
|
|
|
|
dimensions,
|
|
|
|
|
credentialValue,
|
|
|
|
|
});
|
feat(cli): setup progress spinners, Tab-to-select, and banner polish (#296)
* fix(cli): double the height of the setup banner t crossbar
* fix(cli): unify setup multi-select hints and make Tab the select key
The six interactive multi-select surfaces in `ktx setup` documented three
different hint voices, one had no hint at all, and they named two different
select keys (Space vs Tab). Tab is the only key that can toggle selection
without colliding with type-to-search input, so make it the single documented
select key everywhere and compose every hint from one shared fragment
vocabulary in prompt-navigation.ts.
- Register `updateSettings({ aliases: { tab: 'space' } })` so Tab toggles flat
multiselects; the alias applies only to non-text prompts, leaving typed
search input (schema/Notion) untouched.
- Add the missing hint to the agent-targets prompt and drop the stray
"Space to select … Esc …" info line plus the now-dead writeSetupInfo helper.
- Replace the schema-scope ad-hoc hint with the searchable-multiselect voice
and standardize "filter" -> "search" vocabulary.
- Delete DEFAULT_TREE_PICKER_HELP_TEXT and the unused TreePickerChrome.helpText
seam; render the shared tree hint instead.
* refactor(cli): show LLM check progress for every setup backend
Rename runLlmHealthCheckWithProgress to validateModelWithProgress and
wrap the Claude subscription and Codex auth probes in the same spinner
progress as the Anthropic API and Vertex backends, so each backend shows
consistent "Checking <provider> LLM" output during setup.
* feat(cli): add ktx-orange progress spinners to setup steps
Add a shared runWithCliSpinner helper and a TTY-aware createCliSpinner:
an animated clack spinner in a terminal, and a static stderr-only spinner
before raw-mode pickers (the table tree picker and demo tour), where the
animated spinner's stdin grab would otherwise corrupt the next prompt.
Wrap the slow setup waits in progress spinners: managed runtime install,
embedding daemon start + first-run model download, embeddings health
check, the connection-test gate, and source validation / dbt clone /
Metabase discovery. Recolor every spinner frame from clack's magenta to
the ktx mascot orange (#FF8A4C) via the static helper and clack's
styleFrame option.
2026-06-12 16:43:10 +02:00
|
|
|
const healthSpinner = (deps.spinner ?? (() => createCliSpinner(io)))();
|
2026-05-12 21:50:41 -07:00
|
|
|
const progress = startHealthCheckProgress(healthSpinner, healthCheckStartText(selectedBackend, model, dimensions));
|
2026-05-10 23:51:24 +02:00
|
|
|
let health: KtxEmbeddingHealthCheckResult;
|
2026-05-10 23:12:26 +02:00
|
|
|
try {
|
|
|
|
|
health = await healthCheck(healthConfig);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
progress.fail('Embedding test failed');
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
if (health.ok) {
|
|
|
|
|
progress.succeed(`Embedding test passed (${model}, ${dimensions} dimensions)`);
|
|
|
|
|
await persistEmbeddingConfig(
|
|
|
|
|
args.projectDir,
|
2026-05-11 15:50:34 +02:00
|
|
|
selectedBackend === LOCAL_EMBEDDING_BACKEND
|
2026-05-21 02:21:22 +02:00
|
|
|
? {
|
|
|
|
|
backend: 'sentence-transformers' as const,
|
|
|
|
|
model,
|
|
|
|
|
dimensions,
|
|
|
|
|
}
|
2026-05-11 15:50:34 +02:00
|
|
|
: buildProjectEmbeddingConfig({
|
|
|
|
|
backend: selectedBackend,
|
|
|
|
|
model,
|
|
|
|
|
dimensions,
|
|
|
|
|
credentialRef,
|
|
|
|
|
}),
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
2026-05-12 16:58:09 -07:00
|
|
|
io.stdout.write(`│ Embeddings ready: yes (${model}, ${dimensions} dimensions)\n`);
|
2026-05-10 23:12:26 +02:00
|
|
|
return { status: 'ready', projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
progress.fail('Embedding test failed');
|
2026-05-19 18:18:56 +02:00
|
|
|
const stderrTail =
|
|
|
|
|
selectedBackend === 'sentence-transformers'
|
|
|
|
|
? await readLocalEmbeddingDaemonStderrTail(managedLocalEmbeddings?.stderrLog)
|
|
|
|
|
: [];
|
2026-05-10 23:12:26 +02:00
|
|
|
io.stderr.write(
|
|
|
|
|
selectedBackend === 'sentence-transformers'
|
2026-05-19 18:18:56 +02:00
|
|
|
? `${localEmbeddingSetupMessage(health.message, stderrTail)}\n`
|
2026-05-10 23:12:26 +02:00
|
|
|
: `Embedding health check failed: ${health.message}\n`,
|
|
|
|
|
);
|
|
|
|
|
if (args.inputMode === 'disabled') {
|
|
|
|
|
return { status: 'failed', projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
if (selectedBackend !== 'sentence-transformers' && (args.embeddingApiKeyEnv || args.embeddingApiKeyFile)) {
|
|
|
|
|
return { status: 'failed', projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
const nextAction =
|
|
|
|
|
selectedBackend === 'sentence-transformers' ? await promptAfterLocalEmbeddingFailure(deps) : 'retry';
|
|
|
|
|
if (nextAction === 'back') {
|
|
|
|
|
return { status: 'back', projectDir: args.projectDir };
|
|
|
|
|
}
|
|
|
|
|
if (nextAction === 'openai') {
|
|
|
|
|
selectedBackend = nextAction;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|