2026-05-10 23:12:26 +02:00
|
|
|
import { existsSync } from 'node:fs';
|
2026-05-14 17:39:31 +02:00
|
|
|
import { basename, join, resolve } from 'node:path';
|
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 { getLatestLocalIngestStatus } from './context/ingest/local-ingest.js';
|
2026-05-30 00:42:59 +02:00
|
|
|
import { ingestReportOutcome, savedMemoryCountsForReport } from './context/ingest/reports.js';
|
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 { ktxLocalStateDbPath } from './context/project/local-state-db.js';
|
|
|
|
|
import { loadKtxProject, type KtxLocalProject } from './context/project/project.js';
|
|
|
|
|
import { readKtxSetupState } from './context/project/setup-config.js';
|
2026-05-22 18:18:47 +02:00
|
|
|
import { getKtxCliPackageInfo, type KtxCliIo } from './cli-runtime.js';
|
2026-06-03 01:00:21 +02:00
|
|
|
import { formatNextStepLines, formatSetupNextStepLines } from './next-steps.js';
|
2026-05-17 10:27:29 +02:00
|
|
|
import { runtimeInstallPolicyFromFlags } from './managed-python-command.js';
|
|
|
|
|
import { readManagedPythonRuntimeStatus } from './managed-python-runtime.js';
|
|
|
|
|
import { resolveProjectRuntimeRequirements } from './runtime-requirements.js';
|
2026-05-13 17:01:48 +02:00
|
|
|
import { isKtxSetupExitError } from './setup-interrupt.js';
|
2026-05-10 23:12:26 +02:00
|
|
|
import {
|
2026-05-10 23:51:24 +02:00
|
|
|
type KtxAgentScope,
|
|
|
|
|
type KtxAgentTarget,
|
|
|
|
|
type KtxSetupAgentsDeps,
|
|
|
|
|
readKtxAgentInstallManifest,
|
|
|
|
|
runKtxSetupAgentsStep,
|
2026-05-18 18:54:20 -04:00
|
|
|
targetDisplayName,
|
2026-05-10 23:12:26 +02:00
|
|
|
} from './setup-agents.js';
|
|
|
|
|
import {
|
2026-05-10 23:51:24 +02:00
|
|
|
type KtxSetupDatabaseDriver,
|
|
|
|
|
type KtxSetupDatabasesDeps,
|
|
|
|
|
runKtxSetupDatabasesStep,
|
2026-05-10 23:12:26 +02:00
|
|
|
} from './setup-databases.js';
|
2026-05-10 23:51:24 +02:00
|
|
|
import { type KtxSetupEmbeddingsDeps, runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
|
2026-05-13 08:42:38 -04:00
|
|
|
import {
|
|
|
|
|
type KtxSetupLlmBackend,
|
|
|
|
|
type KtxSetupModelDeps,
|
|
|
|
|
isKtxSetupLlmConfigReady,
|
|
|
|
|
runKtxSetupAnthropicModelStep,
|
|
|
|
|
} from './setup-models.js';
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
import { type KtxSetupProjectDeps, runKtxSetupProjectStep } from './setup-project.js';
|
2026-05-10 23:13:17 -07:00
|
|
|
import {
|
2026-06-03 01:00:21 +02:00
|
|
|
classifyKtxSetupCompletion,
|
2026-05-10 23:13:17 -07:00
|
|
|
type KtxSetupReadyMenuDeps,
|
2026-06-03 01:00:21 +02:00
|
|
|
runKtxSetupReadyMenu,
|
|
|
|
|
setupHasContextTargets,
|
2026-05-10 23:13:17 -07:00
|
|
|
} from './setup-ready-menu.js';
|
2026-05-10 23:51:24 +02:00
|
|
|
import { type KtxSetupSourcesDeps, type KtxSetupSourceType, runKtxSetupSourcesStep } from './setup-sources.js';
|
2026-05-17 10:27:29 +02:00
|
|
|
import {
|
|
|
|
|
type KtxSetupRuntimeDeps,
|
|
|
|
|
type KtxSetupRuntimeResult,
|
|
|
|
|
runKtxSetupRuntimeStep,
|
|
|
|
|
} from './setup-runtime.js';
|
2026-05-13 17:01:48 +02:00
|
|
|
import {
|
|
|
|
|
createKtxSetupPromptAdapter,
|
|
|
|
|
createKtxSetupUiAdapter,
|
|
|
|
|
type KtxSetupPromptOption,
|
|
|
|
|
type KtxSetupUiAdapter,
|
|
|
|
|
} from './setup-prompts.js';
|
2026-05-10 23:12:26 +02:00
|
|
|
import {
|
2026-05-10 23:51:24 +02:00
|
|
|
readKtxSetupContextState,
|
|
|
|
|
type KtxSetupContextDeps,
|
|
|
|
|
type KtxSetupContextResult,
|
|
|
|
|
runKtxSetupContextStep,
|
2026-05-10 23:12:26 +02:00
|
|
|
setupContextStatusFromState,
|
2026-05-10 23:51:24 +02:00
|
|
|
type KtxSetupContextStatusSummary,
|
2026-05-10 23:12:26 +02:00
|
|
|
} from './setup-context.js';
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupStatus {
|
2026-05-10 23:12:26 +02:00
|
|
|
project: { path: string; ready: boolean; name?: string };
|
|
|
|
|
llm: { backend?: string; ready: boolean; model?: string };
|
|
|
|
|
embeddings: { backend?: string; ready: boolean; model?: string; dimensions?: number };
|
|
|
|
|
databases: Array<{ connectionId: string; ready: boolean }>;
|
|
|
|
|
sources: Array<{ connectionId: string; type: string; ready: boolean }>;
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime: { required: boolean; ready: boolean; features: string[]; detail?: string };
|
2026-05-10 23:51:24 +02:00
|
|
|
context: KtxSetupContextStatusSummary;
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: Array<{ target: string; scope: string; ready: boolean }>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export type KtxSetupArgs =
|
2026-05-10 23:12:26 +02:00
|
|
|
| {
|
|
|
|
|
command: 'run';
|
|
|
|
|
projectDir: string;
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto';
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: boolean;
|
2026-05-10 23:51:24 +02:00
|
|
|
target?: KtxAgentTarget;
|
|
|
|
|
agentScope?: KtxAgentScope;
|
2026-05-10 23:12:26 +02:00
|
|
|
skipAgents?: boolean;
|
|
|
|
|
inputMode: 'auto' | 'disabled';
|
|
|
|
|
yes: boolean;
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: string;
|
2026-05-13 08:42:38 -04:00
|
|
|
llmBackend?: KtxSetupLlmBackend;
|
2026-05-10 23:12:26 +02:00
|
|
|
anthropicApiKeyEnv?: string;
|
|
|
|
|
anthropicApiKeyFile?: string;
|
2026-05-16 12:06:34 +02:00
|
|
|
llmModel?: string;
|
2026-05-13 08:42:38 -04:00
|
|
|
vertexProject?: string;
|
|
|
|
|
vertexLocation?: string;
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: boolean;
|
|
|
|
|
embeddingBackend?: 'openai' | 'sentence-transformers';
|
|
|
|
|
embeddingApiKeyEnv?: string;
|
|
|
|
|
embeddingApiKeyFile?: string;
|
|
|
|
|
skipEmbeddings: boolean;
|
2026-05-10 23:51:24 +02:00
|
|
|
databaseDrivers?: KtxSetupDatabaseDriver[];
|
2026-05-10 23:12:26 +02:00
|
|
|
databaseConnectionIds?: string[];
|
|
|
|
|
databaseConnectionId?: string;
|
|
|
|
|
databaseUrl?: string;
|
|
|
|
|
databaseSchemas: string[];
|
2026-05-14 01:43:06 +02:00
|
|
|
enableQueryHistory?: boolean;
|
|
|
|
|
disableQueryHistory?: boolean;
|
|
|
|
|
queryHistoryWindowDays?: number;
|
|
|
|
|
queryHistoryMinExecutions?: number;
|
|
|
|
|
queryHistoryServiceAccountPatterns?: string[];
|
|
|
|
|
queryHistoryRedactionPatterns?: string[];
|
2026-05-10 23:12:26 +02:00
|
|
|
skipDatabases: boolean;
|
2026-05-10 23:51:24 +02:00
|
|
|
source?: KtxSetupSourceType;
|
2026-05-10 23:12:26 +02:00
|
|
|
sourceConnectionId?: string;
|
|
|
|
|
sourcePath?: string;
|
|
|
|
|
sourceGitUrl?: string;
|
|
|
|
|
sourceBranch?: string;
|
|
|
|
|
sourceSubpath?: string;
|
|
|
|
|
sourceAuthTokenRef?: string;
|
|
|
|
|
sourceUrl?: string;
|
|
|
|
|
sourceApiKeyRef?: string;
|
|
|
|
|
sourceClientId?: string;
|
|
|
|
|
sourceClientSecretRef?: string;
|
|
|
|
|
sourceWarehouseConnectionId?: string;
|
|
|
|
|
sourceProjectName?: string;
|
|
|
|
|
sourceProfilesPath?: string;
|
|
|
|
|
sourceTarget?: string;
|
|
|
|
|
metabaseDatabaseId?: number;
|
|
|
|
|
notionCrawlMode?: 'all_accessible' | 'selected_roots';
|
|
|
|
|
notionRootPageIds?: string[];
|
|
|
|
|
runInitialSourceIngest?: boolean;
|
|
|
|
|
skipSources?: boolean;
|
|
|
|
|
showEntryMenu?: boolean;
|
2026-05-13 00:38:26 +02:00
|
|
|
};
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupDeps {
|
|
|
|
|
project?: KtxSetupProjectDeps;
|
2026-05-10 23:12:26 +02:00
|
|
|
model?: (
|
2026-05-10 23:51:24 +02:00
|
|
|
args: Parameters<typeof runKtxSetupAnthropicModelStep>[0],
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
) => Promise<Awaited<ReturnType<typeof runKtxSetupAnthropicModelStep>>>;
|
|
|
|
|
modelDeps?: KtxSetupModelDeps;
|
2026-05-10 23:12:26 +02:00
|
|
|
embeddings?: (
|
2026-05-10 23:51:24 +02:00
|
|
|
args: Parameters<typeof runKtxSetupEmbeddingsStep>[0],
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
) => Promise<Awaited<ReturnType<typeof runKtxSetupEmbeddingsStep>>>;
|
|
|
|
|
embeddingsDeps?: KtxSetupEmbeddingsDeps;
|
2026-05-10 23:12:26 +02:00
|
|
|
databases?: (
|
2026-05-10 23:51:24 +02:00
|
|
|
args: Parameters<typeof runKtxSetupDatabasesStep>[0],
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
) => Promise<Awaited<ReturnType<typeof runKtxSetupDatabasesStep>>>;
|
|
|
|
|
databasesDeps?: KtxSetupDatabasesDeps;
|
2026-05-10 23:12:26 +02:00
|
|
|
sources?: (
|
2026-05-10 23:51:24 +02:00
|
|
|
args: Parameters<typeof runKtxSetupSourcesStep>[0],
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
) => Promise<Awaited<ReturnType<typeof runKtxSetupSourcesStep>>>;
|
|
|
|
|
sourcesDeps?: KtxSetupSourcesDeps;
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime?: (args: Parameters<typeof runKtxSetupRuntimeStep>[0], io: KtxCliIo) => Promise<KtxSetupRuntimeResult>;
|
|
|
|
|
runtimeDeps?: KtxSetupRuntimeDeps;
|
2026-05-10 23:12:26 +02:00
|
|
|
agents?: (
|
2026-05-10 23:51:24 +02:00
|
|
|
args: Parameters<typeof runKtxSetupAgentsStep>[0],
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
) => Promise<Awaited<ReturnType<typeof runKtxSetupAgentsStep>>>;
|
|
|
|
|
agentsDeps?: KtxSetupAgentsDeps;
|
|
|
|
|
context?: (args: Parameters<typeof runKtxSetupContextStep>[0], io: KtxCliIo) => Promise<KtxSetupContextResult>;
|
|
|
|
|
contextDeps?: KtxSetupContextDeps;
|
|
|
|
|
readyMenuDeps?: KtxSetupReadyMenuDeps;
|
|
|
|
|
entryMenuDeps?: KtxSetupEntryMenuDeps;
|
2026-05-13 17:01:48 +02:00
|
|
|
setupUi?: KtxSetupUiAdapter;
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SOURCE_DRIVERS = new Set(['dbt', 'metricflow', 'metabase', 'looker', 'lookml', 'notion']);
|
2026-05-26 23:03:47 +02:00
|
|
|
const KTX_DOCS_URL = 'https://docs.kaelio.com/ktx';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
type KtxSetupEntryAction = 'setup' | 'new-project' | 'agents' | 'status' | 'demo' | 'exit';
|
2026-05-17 10:27:29 +02:00
|
|
|
type KtxSetupFlowStep = 'models' | 'embeddings' | 'databases' | 'sources' | 'runtime' | 'context' | 'agents';
|
2026-05-10 23:51:24 +02:00
|
|
|
type KtxSetupFlowStatus =
|
2026-05-10 23:12:26 +02:00
|
|
|
| 'ready'
|
|
|
|
|
| 'skipped'
|
|
|
|
|
| 'back'
|
|
|
|
|
| 'missing-input'
|
2026-05-15 07:09:58 -04:00
|
|
|
| 'failed';
|
2026-05-22 18:18:47 +02:00
|
|
|
type TelemetrySetupStep =
|
|
|
|
|
| 'project'
|
|
|
|
|
| 'runtime'
|
|
|
|
|
| 'models'
|
|
|
|
|
| 'embeddings'
|
|
|
|
|
| 'databases'
|
|
|
|
|
| 'sources'
|
|
|
|
|
| 'context'
|
|
|
|
|
| 'agents'
|
|
|
|
|
| 'demo-tour';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupEntryMenuPromptAdapter {
|
2026-05-13 17:01:48 +02:00
|
|
|
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
2026-05-10 23:12:26 +02:00
|
|
|
cancel(message: string): void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupEntryMenuDeps {
|
|
|
|
|
prompts?: KtxSetupEntryMenuPromptAdapter;
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function createEntryMenuPromptAdapter(): KtxSetupEntryMenuPromptAdapter {
|
2026-05-13 17:01:48 +02:00
|
|
|
return createKtxSetupPromptAdapter({
|
|
|
|
|
selectCancelValue: 'exit',
|
|
|
|
|
cancelOnSelectCancel: false,
|
|
|
|
|
});
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 18:18:47 +02:00
|
|
|
function setupTelemetryOutcome(
|
|
|
|
|
status: KtxSetupFlowStatus | Extract<Awaited<ReturnType<typeof runKtxSetupProjectStep>>, { status: string }>['status'],
|
|
|
|
|
): 'completed' | 'skipped' | 'abandoned' {
|
|
|
|
|
if (status === 'ready') return 'completed';
|
|
|
|
|
if (status === 'skipped') return 'skipped';
|
|
|
|
|
return 'abandoned';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function recordSetupStep(input: {
|
|
|
|
|
projectDir: string;
|
|
|
|
|
step: TelemetrySetupStep;
|
|
|
|
|
status: KtxSetupFlowStatus | Extract<Awaited<ReturnType<typeof runKtxSetupProjectStep>>, { status: string }>['status'];
|
|
|
|
|
startedAt: number;
|
|
|
|
|
io: KtxCliIo;
|
|
|
|
|
cliVersion?: string;
|
2026-06-02 17:23:51 +02:00
|
|
|
errorDetail?: string;
|
2026-05-22 18:18:47 +02:00
|
|
|
}): Promise<void> {
|
|
|
|
|
const { emitTelemetryEvent } = await import('./telemetry/index.js');
|
|
|
|
|
await emitTelemetryEvent({
|
|
|
|
|
name: 'setup_step',
|
|
|
|
|
projectDir: input.projectDir,
|
|
|
|
|
io: input.io,
|
|
|
|
|
packageInfo: { ...getKtxCliPackageInfo(), version: input.cliVersion ?? getKtxCliPackageInfo().version },
|
|
|
|
|
fields: {
|
|
|
|
|
step: input.step,
|
|
|
|
|
outcome: setupTelemetryOutcome(input.status),
|
|
|
|
|
durationMs: Math.max(0, performance.now() - input.startedAt),
|
2026-06-02 17:23:51 +02:00
|
|
|
...(input.errorDetail ? { errorDetail: input.errorDetail } : {}),
|
2026-05-22 18:18:47 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
async function runKtxSetupEntryMenu(
|
|
|
|
|
status: KtxSetupStatus,
|
|
|
|
|
deps: KtxSetupEntryMenuDeps = {},
|
|
|
|
|
): Promise<{ action: KtxSetupEntryAction }> {
|
2026-05-10 23:12:26 +02:00
|
|
|
const prompts = deps.prompts ?? createEntryMenuPromptAdapter();
|
|
|
|
|
const options = status.project.ready
|
|
|
|
|
? [
|
|
|
|
|
{ value: 'setup', label: 'Resume or change an existing setup' },
|
2026-05-10 23:51:24 +02:00
|
|
|
{ value: 'new-project', label: 'Create a new KTX project' },
|
|
|
|
|
{ value: 'agents', label: 'Connect a coding agent to KTX' },
|
2026-05-10 23:12:26 +02:00
|
|
|
{ value: 'status', label: 'Check setup status' },
|
2026-05-11 16:13:30 -07:00
|
|
|
{ value: 'demo', label: 'Explore a pre-built KTX project' },
|
2026-05-10 23:12:26 +02:00
|
|
|
{ value: 'exit', label: 'Exit' },
|
|
|
|
|
]
|
|
|
|
|
: [
|
2026-05-10 23:51:24 +02:00
|
|
|
{ value: 'setup', label: 'Set up KTX for my data' },
|
2026-05-10 23:12:26 +02:00
|
|
|
{ value: 'status', label: 'Check setup status' },
|
2026-05-11 16:13:30 -07:00
|
|
|
{ value: 'demo', label: 'Explore a pre-built KTX project' },
|
2026-05-10 23:12:26 +02:00
|
|
|
{ value: 'exit', label: 'Exit' },
|
|
|
|
|
];
|
|
|
|
|
const action = (await prompts.select({
|
|
|
|
|
message: 'What do you want to do?',
|
|
|
|
|
options,
|
2026-05-10 23:51:24 +02:00
|
|
|
})) as KtxSetupEntryAction;
|
2026-05-10 23:12:26 +02:00
|
|
|
return { action };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
async function runKtxSetupDemoFromEntryMenu(
|
|
|
|
|
args: Extract<KtxSetupArgs, { command: 'run' }>,
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
deps: KtxSetupDeps,
|
2026-05-10 23:12:26 +02:00
|
|
|
): Promise<number> {
|
2026-05-11 15:56:44 -07:00
|
|
|
const { runDemoTour } = await import('./setup-demo-tour.js');
|
2026-05-22 18:18:47 +02:00
|
|
|
const startedAt = performance.now();
|
|
|
|
|
const result = await runDemoTour(
|
|
|
|
|
{ inputMode: args.inputMode, cliVersion: args.cliVersion },
|
2026-05-10 23:12:26 +02:00
|
|
|
io,
|
2026-05-11 15:56:44 -07:00
|
|
|
{ agents: deps.agents },
|
2026-05-12 01:07:47 +02:00
|
|
|
);
|
2026-05-22 18:18:47 +02:00
|
|
|
await recordSetupStep({
|
|
|
|
|
projectDir: args.projectDir,
|
|
|
|
|
step: 'demo-tour',
|
|
|
|
|
status: result === 0 ? 'ready' : 'failed',
|
|
|
|
|
startedAt,
|
|
|
|
|
io,
|
|
|
|
|
cliVersion: args.cliVersion,
|
|
|
|
|
});
|
|
|
|
|
return result;
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function embeddingsReady(status: KtxSetupStatus['embeddings']): boolean {
|
2026-05-10 23:12:26 +02:00
|
|
|
return (
|
|
|
|
|
status.backend !== undefined &&
|
|
|
|
|
status.backend !== 'none' &&
|
|
|
|
|
typeof status.model === 'string' &&
|
|
|
|
|
status.model.length > 0 &&
|
|
|
|
|
typeof status.dimensions === 'number' &&
|
|
|
|
|
status.dimensions > 0
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function sourceConnections(config: Awaited<ReturnType<typeof loadKtxProject>>['config']) {
|
2026-05-10 23:12:26 +02:00
|
|
|
return Object.entries(config.connections)
|
|
|
|
|
.filter(([, connection]) => SOURCE_DRIVERS.has(String(connection.driver ?? '').toLowerCase()))
|
|
|
|
|
.map(([connectionId, connection]) => ({
|
|
|
|
|
connectionId,
|
|
|
|
|
type: String(connection.driver).toLowerCase(),
|
|
|
|
|
}))
|
|
|
|
|
.sort((left, right) => left.connectionId.localeCompare(right.connectionId));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 01:07:47 +02:00
|
|
|
type LocalIngestStatusReport = NonNullable<Awaited<ReturnType<typeof getLatestLocalIngestStatus>>>;
|
|
|
|
|
|
|
|
|
|
function reportHasSavedContext(report: LocalIngestStatusReport): boolean {
|
2026-05-30 00:42:59 +02:00
|
|
|
if (ingestReportOutcome(report) === 'error') {
|
2026-05-12 01:07:47 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const counts = savedMemoryCountsForReport(report);
|
|
|
|
|
return counts.wikiCount > 0 || counts.slCount > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readIngestContextStatus(project: KtxLocalProject): Promise<KtxSetupContextStatusSummary | null> {
|
|
|
|
|
if (!existsSync(ktxLocalStateDbPath(project))) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const report = await getLatestLocalIngestStatus(project);
|
|
|
|
|
if (!report || !reportHasSavedContext(report)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
ready: true,
|
|
|
|
|
status: 'completed',
|
|
|
|
|
runId: report.runId,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 10:27:29 +02:00
|
|
|
export interface ReadKtxSetupStatusOptions {
|
|
|
|
|
cliVersion?: string;
|
|
|
|
|
env?: NodeJS.ProcessEnv;
|
|
|
|
|
readRuntimeStatus?: typeof readManagedPythonRuntimeStatus;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function readKtxSetupStatus(
|
|
|
|
|
projectDir: string,
|
|
|
|
|
options: ReadKtxSetupStatusOptions = {},
|
|
|
|
|
): Promise<KtxSetupStatus> {
|
2026-05-10 23:12:26 +02:00
|
|
|
const resolvedProjectDir = resolve(projectDir);
|
2026-05-10 23:51:24 +02:00
|
|
|
if (!existsSync(join(resolvedProjectDir, 'ktx.yaml'))) {
|
2026-05-10 23:12:26 +02:00
|
|
|
return {
|
|
|
|
|
project: { path: resolvedProjectDir, ready: false },
|
|
|
|
|
llm: { ready: false },
|
|
|
|
|
embeddings: { ready: false },
|
|
|
|
|
databases: [],
|
|
|
|
|
sources: [],
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime: { required: false, ready: true, features: [] },
|
2026-05-10 23:51:24 +02:00
|
|
|
context: setupContextStatusFromState(await readKtxSetupContextState(resolvedProjectDir)),
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
const project = await loadKtxProject({ projectDir: resolvedProjectDir });
|
2026-05-10 23:12:26 +02:00
|
|
|
const llm = {
|
|
|
|
|
backend: project.config.llm.provider.backend,
|
2026-05-12 01:05:28 +02:00
|
|
|
ready: isKtxSetupLlmConfigReady(project.config.llm),
|
2026-05-10 23:12:26 +02:00
|
|
|
model: project.config.llm.models.default,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const embeddings = {
|
|
|
|
|
backend: project.config.ingest.embeddings.backend,
|
|
|
|
|
ready: false,
|
|
|
|
|
model: project.config.ingest.embeddings.model,
|
|
|
|
|
dimensions: project.config.ingest.embeddings.dimensions,
|
|
|
|
|
};
|
|
|
|
|
embeddings.ready = embeddingsReady(embeddings);
|
|
|
|
|
|
2026-05-13 13:55:21 +02:00
|
|
|
const completedSteps = (await readKtxSetupState(resolvedProjectDir)).completed_steps;
|
2026-05-10 23:51:24 +02:00
|
|
|
const contextState = await readKtxSetupContextState(resolvedProjectDir);
|
2026-05-12 01:07:47 +02:00
|
|
|
const setupContextStatus = setupContextStatusFromState(contextState, {
|
|
|
|
|
completedStep: completedSteps.includes('context'),
|
|
|
|
|
});
|
|
|
|
|
const ingestContextStatus = setupContextStatus.ready ? null : await readIngestContextStatus(project);
|
2026-05-10 23:12:26 +02:00
|
|
|
const databaseIds = project.config.setup?.database_connection_ids ?? Object.keys(project.config.connections);
|
|
|
|
|
const databasesComplete = completedSteps.includes('databases');
|
2026-05-10 23:51:24 +02:00
|
|
|
const manifest = await readKtxAgentInstallManifest(resolvedProjectDir);
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
const agentMap = new Map<string, { target: string; scope: string; ready: boolean }>();
|
|
|
|
|
for (const install of manifest?.installs ?? []) {
|
|
|
|
|
agentMap.set(`${install.target}:${install.scope}`, {
|
2026-05-10 23:12:26 +02:00
|
|
|
target: install.target,
|
|
|
|
|
scope: install.scope,
|
|
|
|
|
ready: true,
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const agents = [...agentMap.values()];
|
2026-05-17 10:27:29 +02:00
|
|
|
const runtimeRequirements = resolveProjectRuntimeRequirements(project.config, {
|
|
|
|
|
env: options.env ?? process.env,
|
|
|
|
|
});
|
|
|
|
|
let runtimeReady = runtimeRequirements.features.length === 0 || completedSteps.includes('runtime');
|
|
|
|
|
let runtimeDetail: string | undefined;
|
|
|
|
|
if (runtimeRequirements.features.length > 0 && options.cliVersion) {
|
|
|
|
|
const readRuntimeStatus = options.readRuntimeStatus ?? readManagedPythonRuntimeStatus;
|
|
|
|
|
const runtimeStatus = await readRuntimeStatus({ cliVersion: options.cliVersion, env: options.env ?? process.env });
|
|
|
|
|
runtimeDetail = runtimeStatus.detail;
|
|
|
|
|
runtimeReady =
|
|
|
|
|
runtimeStatus.kind === 'ready' &&
|
|
|
|
|
runtimeStatus.manifest !== undefined &&
|
|
|
|
|
runtimeRequirements.features.every((feature) => runtimeStatus.manifest?.features.includes(feature));
|
|
|
|
|
}
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
return {
|
2026-05-14 17:39:31 +02:00
|
|
|
project: { path: resolvedProjectDir, ready: true, name: basename(project.projectDir) || project.projectDir },
|
2026-05-10 23:12:26 +02:00
|
|
|
llm,
|
|
|
|
|
embeddings,
|
|
|
|
|
databases: databaseIds.map((connectionId) => ({
|
|
|
|
|
connectionId,
|
|
|
|
|
ready: databasesComplete && Object.hasOwn(project.config.connections, connectionId),
|
|
|
|
|
})),
|
|
|
|
|
sources: sourceConnections(project.config).map((source) => ({
|
|
|
|
|
...source,
|
|
|
|
|
ready: completedSteps.includes('sources'),
|
|
|
|
|
})),
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime: {
|
|
|
|
|
required: runtimeRequirements.features.length > 0,
|
|
|
|
|
ready: runtimeReady,
|
|
|
|
|
features: runtimeRequirements.features,
|
|
|
|
|
...(runtimeDetail ? { detail: runtimeDetail } : {}),
|
|
|
|
|
},
|
2026-05-12 01:07:47 +02:00
|
|
|
context: ingestContextStatus ?? setupContextStatus,
|
2026-05-10 23:12:26 +02:00
|
|
|
agents,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatReady(value: boolean): 'yes' | 'no' {
|
|
|
|
|
return value ? 'yes' : 'no';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatConnectionList(ids: string[]): string {
|
|
|
|
|
return ids.length > 0 ? `yes (${ids.join(', ')})` : 'no';
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function formatContextBuilt(status: KtxSetupContextStatusSummary): string {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (status.ready) {
|
|
|
|
|
return 'yes';
|
|
|
|
|
}
|
|
|
|
|
if (status.status === 'not_started') {
|
|
|
|
|
return 'no';
|
|
|
|
|
}
|
|
|
|
|
const runSuffix = status.runId ? ` (${status.runId})` : '';
|
|
|
|
|
return `${status.status.replaceAll('_', ' ')}${runSuffix}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export function formatKtxSetupStatus(status: KtxSetupStatus): string {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (!status.project.ready) {
|
|
|
|
|
return [
|
2026-05-10 23:51:24 +02:00
|
|
|
`No KTX project found at ${status.project.path}.`,
|
2026-05-10 23:12:26 +02:00
|
|
|
'',
|
2026-05-13 00:38:26 +02:00
|
|
|
'Check another project: ktx --project-dir <folder> status',
|
|
|
|
|
'Or from that folder: ktx status',
|
2026-05-10 23:51:24 +02:00
|
|
|
'Create a new KTX project here: ktx setup',
|
2026-05-10 23:12:26 +02:00
|
|
|
'',
|
|
|
|
|
].join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lines = [
|
2026-05-10 23:51:24 +02:00
|
|
|
`KTX project: ${status.project.path}`,
|
2026-05-10 23:12:26 +02:00
|
|
|
`Project ready: ${formatReady(status.project.ready)}`,
|
|
|
|
|
`LLM ready: ${formatReady(status.llm.ready)}${status.llm.model ? ` (${status.llm.model})` : ''}`,
|
|
|
|
|
`Embeddings ready: ${formatReady(status.embeddings.ready)}${
|
|
|
|
|
status.embeddings.model ? ` (${status.embeddings.model})` : ''
|
|
|
|
|
}`,
|
2026-05-14 01:43:06 +02:00
|
|
|
`Databases configured: ${formatConnectionList(status.databases.map((database) => database.connectionId))}`,
|
2026-05-10 23:12:26 +02:00
|
|
|
`Context sources configured: ${formatConnectionList(status.sources.map((source) => source.connectionId))}`,
|
2026-05-17 10:27:29 +02:00
|
|
|
...(status.runtime.required
|
|
|
|
|
? [
|
|
|
|
|
`Runtime ready: ${formatReady(status.runtime.ready)}${
|
|
|
|
|
status.runtime.features.length > 0 ? ` (${status.runtime.features.join(', ')})` : ''
|
|
|
|
|
}`,
|
|
|
|
|
]
|
|
|
|
|
: []),
|
2026-05-10 23:51:24 +02:00
|
|
|
`KTX context built: ${formatContextBuilt(status.context)}`,
|
2026-05-10 23:12:26 +02:00
|
|
|
`Agent integration ready: ${formatReady(status.agents.some((agent) => agent.ready))}${
|
|
|
|
|
status.agents.length > 0 ? ` (${status.agents.map((agent) => `${agent.target}:${agent.scope}`).join(', ')})` : ''
|
|
|
|
|
}`,
|
|
|
|
|
];
|
|
|
|
|
if (!status.context.ready && status.context.status === 'failed' && status.context.detail) {
|
2026-05-13 00:38:26 +02:00
|
|
|
lines.push(`Retry: ${status.context.retryCommand ?? `ktx setup --project-dir ${status.project.path}`}`);
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${lines.join('\n')}\n`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 18:54:20 -04:00
|
|
|
export function formatKtxSetupCompletionSummary(
|
|
|
|
|
status: KtxSetupStatus,
|
|
|
|
|
options: { agentNextActions?: string } = {},
|
|
|
|
|
): string {
|
|
|
|
|
const readyAgents = status.agents.filter((agent) => agent.ready).map((agent) => targetDisplayName(agent.target));
|
|
|
|
|
const lines = [
|
|
|
|
|
'Project',
|
|
|
|
|
` ${status.project.path}`,
|
|
|
|
|
'',
|
|
|
|
|
'Context',
|
|
|
|
|
` ${status.context.ready ? 'built' : formatContextBuilt(status.context)}`,
|
|
|
|
|
'',
|
|
|
|
|
'Agents configured',
|
|
|
|
|
` ${readyAgents.length > 0 ? readyAgents.join(', ') : 'not installed'}`,
|
|
|
|
|
];
|
|
|
|
|
const agentNextActions = options.agentNextActions?.trim();
|
|
|
|
|
if (agentNextActions) {
|
|
|
|
|
lines.push(
|
|
|
|
|
'',
|
|
|
|
|
'REQUIRED BEFORE USING AGENTS',
|
|
|
|
|
'',
|
|
|
|
|
...agentNextActions.split('\n').map((line) => (line ? ` ${line}` : '')),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
lines.push('', agentNextActions ? 'After that, try' : 'Try it');
|
|
|
|
|
lines.push(' Ask your agent: "Use KTX to show me the available tables."');
|
|
|
|
|
return lines.join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function setupStatusReady(status: KtxSetupStatus): boolean {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (!status.project.ready) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!setupHasContextTargets(status)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return (
|
2026-05-12 01:05:28 +02:00
|
|
|
status.llm.ready &&
|
2026-05-10 23:12:26 +02:00
|
|
|
embeddingsReady(status.embeddings) &&
|
|
|
|
|
status.databases.every((database) => database.ready) &&
|
2026-05-17 10:27:29 +02:00
|
|
|
status.sources.every((source) => source.ready) &&
|
|
|
|
|
status.runtime.ready
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function setupContextReady(status: KtxSetupStatus): boolean {
|
2026-05-10 23:12:26 +02:00
|
|
|
return status.context.ready;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 18:54:20 -04:00
|
|
|
function shouldPrintConciseReadySummary(status: KtxSetupStatus): boolean {
|
|
|
|
|
return setupStatusReady(status) && setupContextReady(status) && status.agents.some((agent) => agent.ready);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
function setupRuntimeInstallPolicy(args: Extract<KtxSetupArgs, { command: 'run' }>): 'prompt' | 'auto' | 'never' {
|
2026-05-17 10:27:29 +02:00
|
|
|
if (args.yes) {
|
|
|
|
|
return 'auto';
|
|
|
|
|
}
|
|
|
|
|
return runtimeInstallPolicyFromFlags({ input: args.inputMode === 'disabled' ? false : true });
|
2026-05-11 15:50:34 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 16:56:58 -04:00
|
|
|
async function commitSetupConfigChanges(projectDir: string): Promise<void> {
|
|
|
|
|
const project = await loadKtxProject({ projectDir });
|
|
|
|
|
await project.git.commitFile('ktx.yaml', 'setup: update KTX project config', 'ktx setup', 'setup@ktx.local');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export async function runKtxSetup(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetupDeps = {}): Promise<number> {
|
2026-05-10 23:12:26 +02:00
|
|
|
try {
|
2026-05-10 23:51:24 +02:00
|
|
|
return await runKtxSetupInner(args, io, deps);
|
2026-05-10 23:12:26 +02:00
|
|
|
} catch (error) {
|
2026-05-10 23:51:24 +02:00
|
|
|
if (isKtxSetupExitError(error)) {
|
2026-05-10 23:12:26 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetupDeps = {}): Promise<number> {
|
2026-05-13 17:01:48 +02:00
|
|
|
const setupUi = deps.setupUi ?? createKtxSetupUiAdapter();
|
|
|
|
|
setupUi.intro('KTX setup', io);
|
2026-05-26 23:03:47 +02:00
|
|
|
setupUi.note(KTX_DOCS_URL, '📚 Docs', io);
|
2026-05-10 23:51:24 +02:00
|
|
|
let entryAction: KtxSetupEntryAction | undefined;
|
|
|
|
|
let projectResult: Awaited<ReturnType<typeof runKtxSetupProjectStep>>;
|
2026-05-18 18:54:20 -04:00
|
|
|
let agentNextActions: string | undefined;
|
2026-05-10 23:12:26 +02:00
|
|
|
const canShowEntryMenu =
|
|
|
|
|
args.showEntryMenu === true &&
|
|
|
|
|
args.inputMode !== 'disabled' &&
|
|
|
|
|
!args.agents &&
|
|
|
|
|
(io.stdout.isTTY === true || deps.entryMenuDeps?.prompts !== undefined);
|
|
|
|
|
|
|
|
|
|
setupLoop: while (true) {
|
|
|
|
|
entryAction = undefined;
|
|
|
|
|
if (canShowEntryMenu) {
|
2026-05-17 10:27:29 +02:00
|
|
|
const status = await readKtxSetupStatus(args.projectDir, { cliVersion: args.cliVersion });
|
2026-05-15 07:09:58 -04:00
|
|
|
entryAction = (await runKtxSetupEntryMenu(status, deps.entryMenuDeps)).action;
|
|
|
|
|
if (entryAction === 'exit') {
|
|
|
|
|
(deps.entryMenuDeps?.prompts ?? createEntryMenuPromptAdapter()).cancel('Setup cancelled.');
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
if (entryAction === 'status') {
|
|
|
|
|
io.stdout.write(formatKtxSetupStatus(status));
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
if (entryAction === 'demo') {
|
|
|
|
|
return await runKtxSetupDemoFromEntryMenu(args, io, deps);
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const projectMode = entryAction === 'new-project' ? 'prompt-new' : args.mode;
|
2026-05-22 18:18:47 +02:00
|
|
|
const projectStepStartedAt = performance.now();
|
2026-05-10 23:51:24 +02:00
|
|
|
projectResult = await runKtxSetupProjectStep(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
projectDir: args.projectDir,
|
|
|
|
|
mode: projectMode,
|
|
|
|
|
inputMode: args.inputMode,
|
|
|
|
|
yes: args.yes,
|
|
|
|
|
allowBack: canShowEntryMenu,
|
|
|
|
|
},
|
|
|
|
|
io,
|
|
|
|
|
deps.project,
|
|
|
|
|
);
|
2026-05-22 18:18:47 +02:00
|
|
|
await recordSetupStep({
|
|
|
|
|
projectDir: projectResult.projectDir,
|
|
|
|
|
step: 'project',
|
|
|
|
|
status: projectResult.status,
|
|
|
|
|
startedAt: projectStepStartedAt,
|
|
|
|
|
io,
|
|
|
|
|
cliVersion: args.cliVersion,
|
|
|
|
|
});
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
if (projectResult.status === 'back') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (projectResult.status !== 'ready') {
|
|
|
|
|
return projectResult.status === 'cancelled' ? 0 : 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const agentsRequested = args.agents || entryAction === 'agents';
|
2026-05-17 10:27:29 +02:00
|
|
|
const currentStatus = await readKtxSetupStatus(projectResult.projectDir, { cliVersion: args.cliVersion });
|
2026-05-10 23:12:26 +02:00
|
|
|
let readyAction: string | undefined;
|
2026-05-10 17:08:55 -07:00
|
|
|
|
2026-05-10 23:13:17 -07:00
|
|
|
if (args.inputMode !== 'disabled' && !agentsRequested) {
|
2026-06-03 01:00:21 +02:00
|
|
|
const completion = classifyKtxSetupCompletion(currentStatus);
|
|
|
|
|
if (completion === 'ready') {
|
|
|
|
|
setupUi.note(formatNextStepLines().join('\n'), 'ktx is ready', io);
|
|
|
|
|
const choice = (await runKtxSetupReadyMenu(currentStatus, deps.readyMenuDeps)).action;
|
|
|
|
|
if (choice === 'exit') return 0;
|
|
|
|
|
readyAction = choice;
|
|
|
|
|
} else if (completion === 'needs-context') {
|
|
|
|
|
// Config is done; skip the re-walk and land straight on the build prompt.
|
|
|
|
|
readyAction = 'context';
|
|
|
|
|
} else if (completion === 'needs-agents') {
|
2026-05-10 23:13:17 -07:00
|
|
|
readyAction = 'agents';
|
|
|
|
|
}
|
2026-06-03 01:00:21 +02:00
|
|
|
// 'incomplete' → readyAction stays undefined → run the full setup walk.
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const runOnly = readyAction;
|
2026-05-19 12:18:52 +02:00
|
|
|
const agentOnlySetup = agentsRequested || runOnly === 'agents';
|
2026-05-10 23:12:26 +02:00
|
|
|
const shouldRunModels = !runOnly || runOnly === 'models';
|
|
|
|
|
const shouldRunEmbeddings = !runOnly || runOnly === 'embeddings';
|
|
|
|
|
const shouldRunDatabases = !runOnly || runOnly === 'databases';
|
|
|
|
|
const shouldRunSources = !runOnly || runOnly === 'sources';
|
2026-05-17 10:27:29 +02:00
|
|
|
const shouldRunRuntime =
|
2026-05-19 12:18:52 +02:00
|
|
|
!agentOnlySetup && (!runOnly || runOnly === 'runtime' || runOnly === 'context');
|
|
|
|
|
const shouldRunContext = !agentOnlySetup && (!runOnly || runOnly === 'context');
|
2026-05-10 23:12:26 +02:00
|
|
|
const shouldRunAgents = agentsRequested || !runOnly || runOnly === 'agents';
|
|
|
|
|
const showPromptInstructions = projectResult.confirmedCreation !== true;
|
2026-05-24 19:29:37 +02:00
|
|
|
let skipSourcesFromDatabaseMenu = false;
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-19 12:18:52 +02:00
|
|
|
const setupSteps: KtxSetupFlowStep[] = agentOnlySetup
|
|
|
|
|
? []
|
2026-05-17 10:27:29 +02:00
|
|
|
: ['models', 'embeddings', 'databases', 'sources', 'runtime', 'context'];
|
2026-05-10 23:12:26 +02:00
|
|
|
if (shouldRunAgents && args.skipAgents !== true) {
|
|
|
|
|
setupSteps.push('agents');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
const forcePromptSteps = new Set<KtxSetupFlowStep>();
|
|
|
|
|
const isNavigableSetupStep = (step: KtxSetupFlowStep): boolean => {
|
2026-05-10 23:12:26 +02:00
|
|
|
if (step === 'models') return !args.skipLlm && shouldRunModels;
|
|
|
|
|
if (step === 'embeddings') return !args.skipEmbeddings && shouldRunEmbeddings;
|
|
|
|
|
if (step === 'databases') return !args.skipDatabases && shouldRunDatabases;
|
2026-05-24 19:29:37 +02:00
|
|
|
if (step === 'sources') {
|
|
|
|
|
return args.skipSources !== true && !skipSourcesFromDatabaseMenu && shouldRunSources;
|
|
|
|
|
}
|
2026-05-17 10:27:29 +02:00
|
|
|
if (step === 'runtime') return shouldRunRuntime;
|
2026-05-10 23:12:26 +02:00
|
|
|
if (step === 'context') return shouldRunContext;
|
|
|
|
|
return shouldRunAgents && args.skipAgents !== true;
|
|
|
|
|
};
|
|
|
|
|
const previousNavigableStepIndex = (currentIndex: number): number => {
|
|
|
|
|
for (let index = currentIndex - 1; index >= 0; index -= 1) {
|
|
|
|
|
const previousStep = setupSteps[index];
|
|
|
|
|
if (previousStep && isNavigableSetupStep(previousStep)) {
|
|
|
|
|
return index;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (let stepIndex = 0; stepIndex < setupSteps.length; ) {
|
|
|
|
|
const step = setupSteps[stepIndex];
|
|
|
|
|
if (!step) break;
|
|
|
|
|
|
2026-05-22 18:18:47 +02:00
|
|
|
const stepStartedAt = performance.now();
|
2026-06-02 17:23:51 +02:00
|
|
|
let stepResult: { status: KtxSetupFlowStatus; errorDetail?: string };
|
2026-05-10 23:12:26 +02:00
|
|
|
if (step === 'models') {
|
|
|
|
|
const modelRunner =
|
2026-05-10 23:51:24 +02:00
|
|
|
deps.model ?? ((modelArgs, modelIo) => runKtxSetupAnthropicModelStep(modelArgs, modelIo, deps.modelDeps));
|
2026-05-10 23:12:26 +02:00
|
|
|
stepResult = await modelRunner(
|
|
|
|
|
{
|
|
|
|
|
projectDir: projectResult.projectDir,
|
|
|
|
|
inputMode: args.inputMode,
|
2026-05-13 08:42:38 -04:00
|
|
|
...(args.llmBackend ? { llmBackend: args.llmBackend } : {}),
|
2026-05-10 23:12:26 +02:00
|
|
|
...(args.anthropicApiKeyEnv ? { anthropicApiKeyEnv: args.anthropicApiKeyEnv } : {}),
|
|
|
|
|
...(args.anthropicApiKeyFile ? { anthropicApiKeyFile: args.anthropicApiKeyFile } : {}),
|
2026-05-16 12:06:34 +02:00
|
|
|
...(args.llmModel ? { llmModel: args.llmModel } : {}),
|
2026-05-13 08:42:38 -04:00
|
|
|
...(args.vertexProject ? { vertexProject: args.vertexProject } : {}),
|
|
|
|
|
...(args.vertexLocation ? { vertexLocation: args.vertexLocation } : {}),
|
2026-05-10 23:12:26 +02:00
|
|
|
forcePrompt: forcePromptSteps.has('models') || runOnly === 'models',
|
|
|
|
|
showPromptInstructions,
|
|
|
|
|
skipLlm: args.skipLlm || !shouldRunModels,
|
|
|
|
|
},
|
|
|
|
|
io,
|
|
|
|
|
);
|
|
|
|
|
} else if (step === 'embeddings') {
|
|
|
|
|
const embeddingsRunner =
|
|
|
|
|
deps.embeddings ??
|
2026-05-10 23:51:24 +02:00
|
|
|
((embeddingArgs, embeddingIo) => runKtxSetupEmbeddingsStep(embeddingArgs, embeddingIo, deps.embeddingsDeps));
|
2026-05-10 23:12:26 +02:00
|
|
|
stepResult = await embeddingsRunner(
|
|
|
|
|
{
|
|
|
|
|
projectDir: projectResult.projectDir,
|
|
|
|
|
inputMode: args.inputMode,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: args.cliVersion,
|
|
|
|
|
runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
|
2026-05-10 23:12:26 +02:00
|
|
|
...(args.embeddingBackend ? { embeddingBackend: args.embeddingBackend } : {}),
|
|
|
|
|
...(args.embeddingApiKeyEnv ? { embeddingApiKeyEnv: args.embeddingApiKeyEnv } : {}),
|
|
|
|
|
...(args.embeddingApiKeyFile ? { embeddingApiKeyFile: args.embeddingApiKeyFile } : {}),
|
|
|
|
|
forcePrompt: forcePromptSteps.has('embeddings') || runOnly === 'embeddings',
|
|
|
|
|
showPromptInstructions,
|
|
|
|
|
skipEmbeddings: args.skipEmbeddings || !shouldRunEmbeddings,
|
|
|
|
|
},
|
|
|
|
|
io,
|
|
|
|
|
);
|
|
|
|
|
} else if (step === 'databases') {
|
|
|
|
|
const databasesRunner =
|
|
|
|
|
deps.databases ??
|
2026-05-10 23:51:24 +02:00
|
|
|
((databaseArgs, databaseIo) => runKtxSetupDatabasesStep(databaseArgs, databaseIo, deps.databasesDeps));
|
2026-05-24 19:29:37 +02:00
|
|
|
const databaseResult = await databasesRunner(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
projectDir: projectResult.projectDir,
|
|
|
|
|
inputMode: args.inputMode,
|
2026-06-03 17:19:42 +02:00
|
|
|
yes: args.yes,
|
|
|
|
|
cliVersion: args.cliVersion,
|
|
|
|
|
runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
|
2026-05-10 23:12:26 +02:00
|
|
|
...(args.databaseDrivers ? { databaseDrivers: args.databaseDrivers } : {}),
|
|
|
|
|
...(args.databaseConnectionIds ? { databaseConnectionIds: args.databaseConnectionIds } : {}),
|
|
|
|
|
...(args.databaseConnectionId ? { databaseConnectionId: args.databaseConnectionId } : {}),
|
|
|
|
|
...(args.databaseUrl ? { databaseUrl: args.databaseUrl } : {}),
|
|
|
|
|
databaseSchemas: args.databaseSchemas,
|
2026-05-14 01:43:06 +02:00
|
|
|
...(args.enableQueryHistory !== undefined ? { enableQueryHistory: args.enableQueryHistory } : {}),
|
|
|
|
|
...(args.disableQueryHistory !== undefined ? { disableQueryHistory: args.disableQueryHistory } : {}),
|
|
|
|
|
...(args.queryHistoryWindowDays !== undefined ? { queryHistoryWindowDays: args.queryHistoryWindowDays } : {}),
|
|
|
|
|
...(args.queryHistoryMinExecutions !== undefined
|
|
|
|
|
? { queryHistoryMinExecutions: args.queryHistoryMinExecutions }
|
2026-05-11 19:08:41 +02:00
|
|
|
: {}),
|
2026-05-14 01:43:06 +02:00
|
|
|
...(args.queryHistoryServiceAccountPatterns
|
|
|
|
|
? { queryHistoryServiceAccountPatterns: args.queryHistoryServiceAccountPatterns }
|
2026-05-10 23:12:26 +02:00
|
|
|
: {}),
|
2026-05-14 01:43:06 +02:00
|
|
|
...(args.queryHistoryRedactionPatterns
|
|
|
|
|
? { queryHistoryRedactionPatterns: args.queryHistoryRedactionPatterns }
|
2026-05-10 23:12:26 +02:00
|
|
|
: {}),
|
|
|
|
|
skipDatabases: args.skipDatabases || !shouldRunDatabases,
|
|
|
|
|
},
|
|
|
|
|
io,
|
|
|
|
|
);
|
2026-05-24 19:29:37 +02:00
|
|
|
skipSourcesFromDatabaseMenu = databaseResult.status === 'ready' && databaseResult.skipSources === true;
|
|
|
|
|
stepResult = databaseResult;
|
2026-05-10 23:12:26 +02:00
|
|
|
} else if (step === 'sources') {
|
|
|
|
|
const sourcesRunner =
|
2026-05-10 23:51:24 +02:00
|
|
|
deps.sources ?? ((sourceArgs, sourceIo) => runKtxSetupSourcesStep(sourceArgs, sourceIo, deps.sourcesDeps));
|
2026-05-10 23:12:26 +02:00
|
|
|
stepResult = await sourcesRunner(
|
|
|
|
|
{
|
|
|
|
|
projectDir: projectResult.projectDir,
|
|
|
|
|
inputMode: args.inputMode,
|
|
|
|
|
...(args.source ? { source: args.source } : {}),
|
|
|
|
|
...(args.sourceConnectionId ? { sourceConnectionId: args.sourceConnectionId } : {}),
|
|
|
|
|
...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
|
|
|
|
|
...(args.sourceGitUrl ? { sourceGitUrl: args.sourceGitUrl } : {}),
|
|
|
|
|
...(args.sourceBranch ? { sourceBranch: args.sourceBranch } : {}),
|
|
|
|
|
...(args.sourceSubpath ? { sourceSubpath: args.sourceSubpath } : {}),
|
|
|
|
|
...(args.sourceAuthTokenRef ? { sourceAuthTokenRef: args.sourceAuthTokenRef } : {}),
|
|
|
|
|
...(args.sourceUrl ? { sourceUrl: args.sourceUrl } : {}),
|
|
|
|
|
...(args.sourceApiKeyRef ? { sourceApiKeyRef: args.sourceApiKeyRef } : {}),
|
|
|
|
|
...(args.sourceClientId ? { sourceClientId: args.sourceClientId } : {}),
|
|
|
|
|
...(args.sourceClientSecretRef ? { sourceClientSecretRef: args.sourceClientSecretRef } : {}),
|
|
|
|
|
...(args.sourceWarehouseConnectionId ? { sourceWarehouseConnectionId: args.sourceWarehouseConnectionId } : {}),
|
|
|
|
|
...(args.sourceProjectName ? { sourceProjectName: args.sourceProjectName } : {}),
|
|
|
|
|
...(args.sourceProfilesPath ? { sourceProfilesPath: args.sourceProfilesPath } : {}),
|
|
|
|
|
...(args.sourceTarget ? { sourceTarget: args.sourceTarget } : {}),
|
|
|
|
|
...(args.metabaseDatabaseId !== undefined ? { metabaseDatabaseId: args.metabaseDatabaseId } : {}),
|
|
|
|
|
...(args.notionCrawlMode ? { notionCrawlMode: args.notionCrawlMode } : {}),
|
|
|
|
|
...(args.notionRootPageIds ? { notionRootPageIds: args.notionRootPageIds } : {}),
|
|
|
|
|
runInitialSourceIngest: args.runInitialSourceIngest ?? false,
|
2026-05-24 19:29:37 +02:00
|
|
|
skipSources: args.skipSources === true || !shouldRunSources || skipSourcesFromDatabaseMenu,
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
|
|
|
|
io,
|
|
|
|
|
);
|
2026-05-17 10:27:29 +02:00
|
|
|
} else if (step === 'runtime') {
|
|
|
|
|
const runtimeRunner =
|
|
|
|
|
deps.runtime ??
|
|
|
|
|
((runtimeArgs, runtimeIo) => runKtxSetupRuntimeStep(runtimeArgs, runtimeIo, deps.runtimeDeps));
|
|
|
|
|
stepResult = await runtimeRunner(
|
|
|
|
|
{
|
|
|
|
|
projectDir: projectResult.projectDir,
|
|
|
|
|
inputMode: args.inputMode,
|
|
|
|
|
cliVersion: args.cliVersion,
|
|
|
|
|
runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
|
|
|
|
|
},
|
|
|
|
|
io,
|
|
|
|
|
);
|
2026-05-10 23:12:26 +02:00
|
|
|
} else if (step === 'context') {
|
|
|
|
|
const contextRunner =
|
|
|
|
|
deps.context ??
|
2026-05-10 23:51:24 +02:00
|
|
|
((contextArgs, contextIo) => runKtxSetupContextStep(contextArgs, contextIo, deps.contextDeps));
|
2026-05-10 23:12:26 +02:00
|
|
|
stepResult = await contextRunner(
|
|
|
|
|
{
|
|
|
|
|
projectDir: projectResult.projectDir,
|
|
|
|
|
inputMode: args.inputMode,
|
|
|
|
|
forcePrompt: forcePromptSteps.has('context') || runOnly === 'context',
|
|
|
|
|
allowEmpty: true,
|
2026-05-14 01:43:06 +02:00
|
|
|
cliVersion: args.cliVersion,
|
|
|
|
|
runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
|
|
|
|
io,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
const agentsRunner =
|
2026-05-10 23:51:24 +02:00
|
|
|
deps.agents ?? ((agentArgs, agentIo) => runKtxSetupAgentsStep(agentArgs, agentIo, deps.agentsDeps));
|
2026-05-18 18:54:20 -04:00
|
|
|
const agentResult = await agentsRunner(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
projectDir: projectResult.projectDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
inputMode:
|
|
|
|
|
args.inputMode === 'auto' && io.stdout.isTTY !== true && deps.agentsDeps?.prompts === undefined
|
|
|
|
|
? 'disabled'
|
|
|
|
|
: args.inputMode,
|
2026-05-10 23:12:26 +02:00
|
|
|
yes: args.yes,
|
|
|
|
|
agents: true,
|
|
|
|
|
...(args.target ? { target: args.target } : {}),
|
|
|
|
|
scope: args.agentScope ?? 'project',
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
mode: 'mcp',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipAgents: false,
|
2026-05-18 18:54:20 -04:00
|
|
|
showNextActions: agentsRequested,
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
|
|
|
|
io,
|
|
|
|
|
);
|
2026-05-18 18:54:20 -04:00
|
|
|
stepResult = agentResult;
|
|
|
|
|
if (agentResult.status === 'ready') {
|
|
|
|
|
agentNextActions = agentResult.nextActions;
|
|
|
|
|
}
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 18:18:47 +02:00
|
|
|
await recordSetupStep({
|
|
|
|
|
projectDir: projectResult.projectDir,
|
|
|
|
|
step,
|
|
|
|
|
status: stepResult.status,
|
|
|
|
|
startedAt: stepStartedAt,
|
|
|
|
|
io,
|
|
|
|
|
cliVersion: args.cliVersion,
|
2026-06-02 17:23:51 +02:00
|
|
|
...(stepResult.errorDetail ? { errorDetail: stepResult.errorDetail } : {}),
|
2026-05-22 18:18:47 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-19 18:18:38 +02:00
|
|
|
if (stepResult.status === 'failed') {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (stepResult.status === 'missing-input') {
|
2026-05-10 23:12:26 +02:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (stepResult.status === 'back') {
|
|
|
|
|
const previousIndex = previousNavigableStepIndex(stepIndex);
|
|
|
|
|
if (previousIndex < 0) {
|
|
|
|
|
if (canShowEntryMenu) {
|
|
|
|
|
continue setupLoop;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
const previousStep = setupSteps[previousIndex];
|
|
|
|
|
if (previousStep) {
|
|
|
|
|
forcePromptSteps.add(previousStep);
|
|
|
|
|
}
|
|
|
|
|
stepIndex = previousIndex;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (step === 'context' && stepResult.status !== 'ready') {
|
|
|
|
|
if (shouldRunAgents && args.skipAgents !== true) {
|
2026-06-03 01:00:21 +02:00
|
|
|
// Context isn't built, so skip agent install — but still reach the
|
|
|
|
|
// completion screen, which states readiness and points at `ktx ingest`.
|
|
|
|
|
break setupLoop;
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
forcePromptSteps.delete(step);
|
|
|
|
|
stepIndex += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 16:56:58 -04:00
|
|
|
await commitSetupConfigChanges(projectResult.projectDir);
|
|
|
|
|
|
2026-05-17 10:27:29 +02:00
|
|
|
const status = await readKtxSetupStatus(projectResult.projectDir, { cliVersion: args.cliVersion });
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
const focusedOnAgents = args.agents || entryAction === 'agents';
|
|
|
|
|
if (!focusedOnAgents) {
|
2026-05-18 18:54:20 -04:00
|
|
|
if (shouldPrintConciseReadySummary(status)) {
|
|
|
|
|
setupUi.note(
|
|
|
|
|
formatKtxSetupCompletionSummary(status, { agentNextActions }),
|
|
|
|
|
agentNextActions ? 'Finish KTX agent setup' : 'KTX project ready',
|
|
|
|
|
io,
|
|
|
|
|
{
|
|
|
|
|
format: (line) => line,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
setupUi.note(formatKtxSetupStatus(status).trimEnd(), 'Project status', io, {
|
|
|
|
|
format: (line) => line,
|
|
|
|
|
});
|
|
|
|
|
setupUi.note(
|
|
|
|
|
formatSetupNextStepLines({
|
|
|
|
|
setupReady: setupStatusReady(status),
|
|
|
|
|
hasContextTargets: setupHasContextTargets(status),
|
|
|
|
|
contextReady: setupContextReady(status),
|
|
|
|
|
agentIntegrationReady: status.agents.some((agent) => agent.ready),
|
|
|
|
|
}).join('\n'),
|
|
|
|
|
'What you can do next',
|
|
|
|
|
io,
|
|
|
|
|
);
|
|
|
|
|
}
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
}
|
2026-05-10 23:12:26 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|