Split the fused directory concept into projectDir (what the agent config
references) and installRoot (where project-scoped files are written), so
users can install .claude/, .mcp.json, skills, and rules where they open
their agent instead of only in the ktx project directory.
- Add --install-dir <path> (resolved against cwd, created if missing,
mutually exclusive with --global/--local, rejected for claude-desktop).
- Add an interactive directory menu: ktx project dir / Current directory
(hidden when it equals the project dir) / Custom directory… / Global
scope (shown only when every target supports it).
- Expand a leading ~ in typed/quoted paths so the ~/… menu hints round-trip.
- Record installRoot in the install manifest and merge key; thread it
through file planning, MCP config paths, summaries, and next actions.
- Refresh uv.lock to 0.12.0 for the editable ktx-sl and ktx-daemon packages.
Fixes a production crash-loop (PostHog issue 019eb68e): ktx mcp start
--foreground on a uv-less container eagerly installed the managed Python
runtime at boot, failed, and was restarted by its supervisor every ~62s
(122 exceptions from one install).
- MCP server factory now wires a lazy semantic-layer compute port that
defers the runtime install to the first call, mirroring the already-lazy
SQL-analysis port; the server boots and serves non-Python tools without
the runtime.
- ktx no longer requires uv on PATH: it downloads its own pinned,
sha256-verified uv build under the runtime root (KTX_RUNTIME_ROOT aware),
always musl-static on Linux. PATH uv is never consulted.
- uv is acquired before the version dir is wiped, so a failed download
cannot destroy an existing runtime.
- Acquisition failures (offline, intercepted download, unsupported
platform) throw KtxExpectedError and stay out of Error Tracking; a
missing binary inside a checksum-verified archive remains a plain Error.
- scripts/refresh-uv-manifest.mjs regenerates the pinned manifest
(packages/cli/src/managed-uv-release.ts) on uv bumps.
- Setup consent prompt now discloses the uv download; docs updated.
* 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.
Move the live GitHub stars widget into the sidebar footer pill as a
type:"icon" link, sitting opposite the Slack mark (space-between) and
beside the theme switcher. Render it as inner content (GitHub mark +
star + compact count) so fumadocs supplies the anchor.
Replace the default fumadocs theme switcher with a custom two-icon
control where each icon selects its own theme. The built-in
"light-dark" mode is a single blind toggle that flips on any click, so
clicking the sun while already in light mode jumps to dark. useTheme is
sourced from fumadocs-ui/provider/base and the icons are inlined to
avoid bare next-themes / lucide-react imports.
* fix(cli): survive ktx.yaml version skew and derive repo ownership from disk
Loading ktx.yaml is now tolerant of keys this ktx version does not
recognize: they are stripped from the in-memory config (the file on disk
is never rewritten) and reported by ktx status as non-blocking warnings,
while invalid values on recognized fields still fail hard. Repo
ownership is derived from observed state (a .git directory plus a root
ktx.yaml) instead of a ktx.managed git-config marker, so projects
created by any past or future ktx classify identically. initKtxProject
now runs an explicit foreign-repo pre-check and writes ktx.yaml before
initializing git, so an interrupted init leaves only recoverable
residue instead of a bare .git misread as foreign.
* style(cli): trim comment blocks to constraint-only notes
* docs(agents): require constraint-only code comments
One ingest integration hiccup no longer discards a whole source:
- Replace the duplicated gate-repair and textual-resolver loops with one
shared constrained-repair loop whose success criterion is re-running
the failed check (verify), not whether the agent edited files. Verify
failures feed the retry prompt; maxAttempts is 2.
- Let the resolver declare a conflicting patch redundant: a verified
no-change resolution is accepted as subsumed instead of failing the
source (duplicate wiki-page creation from parallel work units).
- Carry per-source validation errors through validateWuTouchedSources
into gate messages and work-unit failure reasons instead of
discarding them.
- Move join-neighbor expansion into the shared validation path so
work-unit validation and integration gates check the same set.
- Reject joins whose target resolves to no source, at sl_write time and
in the gates, attributed to the declaring source. Resolution mirrors
the Python engine exactly (case-sensitive name within the
connection), with a case-mismatch hint for the writing agent.
Align the tree with AGENTS.md/CLAUDE.md conventions:
- Rewrite user-facing strings, docs, and tests to lowercase `ktx`
(no bare uppercase `KTX` tokens remain outside literal identifiers).
- Drop the legacy `historicSql` migration path and its now-unused
helpers, per the no-backward-compat rule.
- Remove `as unknown as` / `any` casts: narrow `BaseTool` generics to
`z.ZodObject`, add a typed `createLookerClient`, and delete the dead
`getParametersSchema`/`toAnthropicFormat` pre-AI-SDK helpers.
- Use `InvalidArgumentError` for Commander parse failures.
- Finish the adapter→connector prose conversion in the `ktx.yaml` docs
while keeping the literal `adapters` config key.
Verified the dbt, MetricFlow, LookML, Metabase, Looker, and Notion
sections of context-sources.mdx against the adapter code and fixed
claims that did not match the implementation:
- dbt: replace "test coverage" framing with the actual constraint/enum/
join derivation; name both overlay and wiki outputs; fix work-unit
granularity (per models/ schema file above 25 YAML files).
- MetricFlow: relationships come from entities (not dimensions); surface
the join edges they produce.
- LookML: chunking is one work unit per model (not connected component);
add the wiki output; note that a connection: mismatch disables SL writes.
- Metabase: dashboards are never fetched (no dashboard endpoint); work
units are per collection; "usage patterns" is really card output schema.
- Looker: drop invented "purpose/audience" framing; describe triage as a
prioritization gate; include Looks alongside explores and dashboards.
- Notion: not knowledge-only (it writes SL sources for mapped non-Notion
targets); remove the nonexistent database-schema extraction; reframe
"What it provides" as inputs; document root_data_source_ids.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Render a lowercase ktx half-block wordmark with the brand-orange gradient
above the `ktx setup` intro on interactive TTYs. The banner degrades
through truecolor, xterm-256, and monochrome, and is skipped on non-TTY,
non-Unicode, or too-narrow terminals.
Extract shared color/Unicode capability detection into io helpers
(shouldUseColorOutput, colorDepthForOutput, unicodeSupported) so the
banner and doctor report route through one implementation.
The "prints provider setup guidance when a skip-llm setup project runs
ingest" test drives runKtxSetup in --no-input (inputMode: disabled) mode
with a postgres warehouse but databaseSchemas: []. PR #286 changed the
no-input contract to require an explicit database scope instead of
auto-scanning, so setup now exits 1 and the test's first assertion
(resolves.toBe(0)) fails. #286 updated setup-databases.test.ts but missed
this runKtxSetup call in ingest.test.ts. Pass databaseSchemas: ['public']
to satisfy the new contract, matching #286's own test updates.
* fix(cli): isolate ktx project git repos
* fix(cli): remove inert auto commit config
* test(cli): drop stale auto commit fixtures
* docs: document isolated ktx project repos
* test(cli): keep stale config grep clean
* fix(cli): guide setup away from foreign repos at the project dir
ktx owns the git repo rooted at the project dir and refuses to adopt one it
did not create (the Finding 3 isolation invariant). But setup steered users
straight into that failure: the interactive menu offers "Current directory"
first, and `--no-input --yes --project-dir <repo-root>` created directly in
place — both then threw a generic "Failed to initialize git repository:"
wrapper from deep in GitService.initialize().
Extract the ownership rule into a shared `classifyKtxRepoOwnership(dir)` used by
both GitService.initialize() (the invariant) and the setup wizard (pre-flight
guidance), so the decision derives from one rule. Setup now detects a foreign
repo before constructing GitService and: interactively re-prompts (the user
picks the existing `ktx-project` subfolder), or non-interactively returns a
clean missing-input with the actionable message. The typed foreign-repo error
is also surfaced verbatim instead of being buried under the generic wrapper.
Empty/non-repo current directories still work — only foreign repos are blocked.
* fix(cli): keep classifyKtxRepoOwnership total for non-directory paths
The setup ownership guard runs before the existing not-a-directory check, so
pointing a custom/--project-dir path at a file made classifyKtxRepoOwnership
lstat `<file>/.git`, hit ENOTDIR, and throw — crashing the setup step instead
of returning the friendly "path exists and is not a directory" result.
A path that is a file (or missing) holds no git repo for ktx to avoid, so treat
ENOTDIR like ENOENT and return 'unowned'. The downstream existingFolderState
check still rejects a non-directory with its friendly message, and the
classifier no longer throws raw errno for any caller.
* fix: read semantic sources safely
* test: retarget reindex per-scope error case to a broken manifest
Reading a broken standalone source was made non-fatal in de1f1a8d (it is
surfaced for repair instead of throwing), so the reindex per-scope error
test no longer captured an error. Point it at a corrupt manifest shard,
which is the remaining fatal read failure the per-scope catch must
isolate, and assert the captured error names the offending file.
* fix(sl): decouple semantic-layer file names from warehouse naming rules
The in-file `name:` field is now the sole source identity; the filename is
a derived label that never participates in identity. This removes the
"Unsafe semantic-layer source name" failure class entirely: any warehouse
identifier (Snowflake's uppercase SIGNED_UP, EVENT$LOG, dotted names) can
be read, overlaid, edited, and deleted.
- New `source-files.ts`: one total filename derivation (safe lowercase
names verbatim; otherwise slug + sha256-hash suffix, immune to
case-insensitive-filesystem collisions) and one by-name file resolver.
- Reads resolve by name everywhere; the path-from-name fast path and
`assertSafeSourceName` are gone.
- Writes resolve-then-write: rewrites land on the file that declares the
name (human renames survive); new sources get a derived filename; a
derived path occupied by a different source fails instead of clobbering.
- `readSourceFile` returns null for missing files instead of forcing every
caller to launder IO errors; `deleteSource` distinguishes manifest-backed
sources from not-found instead of silently succeeding.
- `sl_write_source` accepts verbatim warehouse identifiers (snake_case is
now a recommendation for new sources) and rejects sourceName/source.name
mismatches; `sl_edit_source` rejects name-changing edits.
- Ingest projection commits, gate-repair allowlists, and touched-source
derivation use resolved paths / in-file names instead of interpolating
`<connId>/<name>.yaml`.
- Collapsed the five parallel path derivations and duplicated path-token
helpers onto the shared module; dropped dead service methods.
* fix(sl): resolve sources by declared name end-to-end and gate warehouse SQL with the parser-backed validator
- Key broken/renamed semantic-layer files by their recoverable in-file
name (slSourceNameForFile) so mid-edit sources stay reachable under
their real identity in reads, listings, and search
- Derive finalization touched sources from composed-source diffs and
recover deleted files' declared names from the pre-change commit
instead of parsing hash-derived filenames
- Resolve revert/rollback paths against history (listFilesAtCommit) so
human-renamed files are restored where they lived at preHead
- Validate ingest sql_execution through the daemon's sqlglot
validateReadOnly in the connection's dialect, sharing one
driver-to-dialect map (sql-analysis/dialect.ts) across MCP and ingest
- Harden the local read-only SQL backstop: accept leading comments,
reject smuggled second statements, and strip trailing
semicolons/comments before row-limit wrapping
GitService.initialize() used checkIsRepo(), which is true whenever the project
dir sits anywhere inside a git working tree. So when a ktx project lived in a
subdirectory of an enclosing repo, ktx skipped `git init` and silently adopted
the enclosing repo as its store.
Every ktx relative path assumes the project dir IS the working-tree root. During
ingest, wiki/SL pages are written through a session worktree (whose root is the
worktree dir, so the page is recorded at repo-relative `wiki/global/<key>.md`)
and then squash-merged into the main worktree. With an adopted enclosing repo,
the main worktree's root is the enclosing git root, so the merge wrote the page
to `<gitRoot>/wiki/global/` — outside the project dir. reindex scans
`<projectDir>/wiki/global/`, found nothing, and wiki_search silently returned
empty (knowledge_pages = 0) even though ingest reported success.
Detect the project dir's own root with checkIsRepo(IS_REPO_ROOT) and initialize
a dedicated repo there unless the project dir is already a repo root. This keeps
adopting a user-created repo when the project dir IS that repo's root, fixes the
silent wiki/SL/memory divergence at its source for every writer, and stops ktx
from committing its scaffold into the user's enclosing repo.
Regression tests cover both layers: a project nested in an enclosing repo gets
its own .git (and the enclosing repo stays untouched), and a wiki page written
through a session worktree + squash-merge lands in the project dir and is
discovered by reindex.
* fix(cli): fail clearly when ktx setup has no LLM backend under --no-input
Non-interactive `ktx setup` silently defaulted the LLM backend to `anthropic`
and then failed with `Missing Anthropic API key: pass --anthropic-api-key-env
or --anthropic-api-key-file` — confusing for users who selected a different
provider (e.g. `--target claude-code`) and never asked for the Anthropic API
backend.
That silent default could never succeed: it was reached only when no backend,
Anthropic key, or Vertex flag was supplied, and in exactly that case the
Anthropic credential resolver always failed (no env fallback in disabled mode).
Unlike embeddings, the LLM has no credential-free default (anthropic needs a
key, vertex needs gcloud ADC, claude-code/codex need a logged-in local CLI), so
there is nothing safe to assume.
`chooseBackend` now fails clearly in disabled mode with no backend, naming the
(hidden) `--llm-backend` flag and its choices and noting each backend's
credential needs. `--llm-backend` stays hidden in `--help`, consistent with the
rest of the documented automation surface; the error message is the discovery
path.
- Add a unit test (no backend, disabled -> clear message) and a CLI/integration
test (`--target claude-code --no-input` -> exit 1, clear message, not the
Anthropic red herring).
- Document the no-default behavior and add a Common-errors row in
docs-site ktx-setup.mdx.
* refactor(cli): single source of truth for setup LLM backends
The set of LLM backends a user can pick during `ktx setup` (claude-code,
codex, anthropic, vertex) was hand-enumerated in five places: the
`--llm-backend` arg parser, the `KtxSetupLlmBackend` union, the interactive
prompt's narrowing, the prompt options, and the missing-backend error. Only
some had TypeScript coverage, so adding a backend could silently drift (e.g.
a valid value rejected by the parser, or routed to anthropic by the prompt's
`? : 'anthropic'` fallback).
Collapse them onto one `KTX_SETUP_LLM_BACKENDS` list:
- `KtxSetupLlmBackend` is derived from it.
- `isKtxSetupLlmBackend` is the shared validator; the arg parser and the
prompt both route through it instead of re-listing literals.
- The prompt options derive from the list, with a `Record<KtxSetupLlmBackend,
string>` label map so a new backend fails to compile until it has a label.
- The missing-backend error builds its choice list from the same source.
Behavior-preserving: identical accepted values and parse error, identical
prompt options (asserted by an existing test), and the prompt's unreachable
fallback now cancels rather than silently assuming anthropic.
pnpm 11 no longer reads the package.json "pnpm" field and warns on it.
The build allowlist already lives in pnpm-workspace.yaml via allowBuilds
(better-sqlite3, esbuild, sharp), so this block was redundant noise.
* fix(cli): classify ktx setup abandonment as aborted, not a blank error
ktx setup returned a non-zero exit code without throwing when a user
abandoned the interactive wizard, so the command telemetry recorded
outcome=error with no errorClass/errorDetail — an unactionable blank in
the errors dashboard, where most ktx setup "errors" were really people
backing out of the wizard.
Add annotateCommandOutcome() to the command span so the setup flow (the
decision-maker) records the true outcome: genuine step failures and
--no-input missing input become outcome=error with a self-diagnosing
reason, while interactive abandonment and project cancellation become
outcome=aborted and drop out of the error view.
Unify the exit code and telemetry through setupTerminalOutcome() so they
can never diverge: aborts now exit 0 (matching the entry-menu Exit,
project cancel, and a confirmed Ctrl+C), while failures and automation
errors still exit 1.
* fix(cli): treat non-TTY setup missing-input as an error, not an abort
setupTerminalOutcome classified `missing-input` by `args.inputMode`, but
`auto` only means "interactive if a TTY is attached". A piped/CI `ktx
setup` without `--no-input` and without `--yes` is still `auto`, yet the
project and agents steps return `missing-input` there without ever
prompting (e.g. "pass --yes to create a project outside an interactive
terminal"). Classifying that as `aborted` made a broken automation run
exit 0 — a silent failure.
Key the classification off actual interactivity instead: input enabled
AND `io.stdout.isTTY === true`. Non-interactive missing-input now exits
1 with a `KtxSetupMissingInput` reason; only a genuine interactive abort
exits 0. Adds a non-TTY regression test and fixes the abandonment test
to use a real TTY.
* feat(cli): show Slack CTA on help and unexpected errors
* feat(cli): show Slack CTA after crashes
* feat(setup): show Slack community note after setup
* chore: refresh Python lockfile versions
* fix(cli): ensure git committer identity during ktx setup
ktx setup threw "Failed to initialize git repository" when the project
directory was already a git repo with no commits and the machine had no
configured git identity (e.g. a fresh Mac with no ~/.gitconfig).
GitService only set the identity on the path where it created the repo
itself, so the bootstrap commit had no resolvable committer.
Carry ktx's identity via GIT_AUTHOR/GIT_COMMITTER env on the shared
git client so every commit succeeds regardless of whether ktx created
the repo, without mutating the user's repo config. Also preserve the
underlying git error when rethrowing so the failure is diagnosable in
telemetry and actionable for the user.
* chore: sync uv.lock ktx-daemon and ktx-sl versions to 0.10.0
The introduction page is special-cased as a hero in the docs route, so it
hand-rolls its own title and subtitle instead of using Fumadocs'
DocsTitle/DocsDescription. The subtitle was capped at max-w-2xl (672px),
wrapping ~230px narrower than the heading and body, which read as
misaligned compared to every other docs page. Match the heading's
max-w-full so the subtitle fills the same content column.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: consolidate AI Resources into a single page
The AI Resources section was four pages (agent-quickstart, markdown-access,
agent-instructions, prompt-recipes) that repeated the same docs-consumption
guidance. Collapse them into one page at /docs/ai-resources covering markdown
endpoints, retrieval order, the task router, agent instructions, prompts, and
guardrails.
Also fix a stale claim: the page actions are a single "Copy as Markdown"
button, not the documented "Copy MD / View MD / Copy MDX" trio.
Update the cross-references in README, the introduction cards, the quickstart,
and the llms.txt entry points to the consolidated page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(docs-site): redirect retired AI Resources slugs, preserving .md route
Redirect the retired per-page slugs (/docs/ai-resources/*) to the consolidated
page. Because Next evaluates redirects before the .md rewrite, a single
catch-all would 308 a cached per-page Markdown URL to the HTML page and break
the agent Markdown contract. Match the .md variant first and keep its suffix so
it lands on /docs/ai-resources.md.
Extend the routing test to assert both the HTML and .md redirects, and that
following the .md URL end to end serves text/markdown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: move AI Resources under the Community & Resources section
As a single page, AI Resources rendered as an orphaned, unbolded link wedged
between the top-level multi-page sections instead of as a section of its own.
Move it under Community (renamed "Community & Resources") so it renders as a
normal child link, consistent with how the single-page Configuration section
already works.
Redirect the former top-level URL and the retired per-page slugs (HTML and .md,
the .md variants first so cached Markdown URLs keep their suffix) to the new
home, relabel the llms.txt group to match, and repoint the README, introduction,
quickstart, and llms.txt cross-links.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: document upgrading to the latest ktx version
Install instructions existed but nothing told users how to upgrade an
existing install. Add a minimal upgrade note to the README and to the
quickstart install section showing `npm install -g @kaelio/ktx@latest`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* nitpick
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The docs site nav carried a "Products" dropdown listing both ktx and
the legacy Kaelio agent platform. On the ktx docs, a co-equal product
switcher framed ktx as one of two products and gave the unrelated
legacy product equal billing. Remove it so the ktx docs stay focused;
cross-product discovery belongs at the docs.kaelio.com apex, not in the
ktx nav.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The auto-generated semantic-layer overlay description embedded
measure/segment/column counts that were computed once and never
recomputed, so the summary drifted and misreported its source after
measures were later appended. Make the auto fallback count-free, since
those counts are already rendered live from the body at `ktx sl list`/
`read` time; this removes the drift class without ever overwriting
human-authored descriptions (the fill-only-when-empty guard is untouched).
Adds a regression test that fails on main and passes after the fix, plus
guards for description preservation and the no-measures fallback.
* feat(setup): write per-role llm model presets
* feat(setup): remove llm model setup flag
* chore(setup): update llm preset guidance
* docs(setup): document llm model presets
* chore(release): sync uv.lock to 0.9.0
* fix(cli): make sl query --execute work on secret-backed connections
sl query --execute used a parallel SQL executor (createDefaultLocalQueryExecutor)
that passed connection.url verbatim into pg, so file:/env: secret references
failed with "SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string".
Collapse onto the connector-based executor already used by MCP and ingest
(createKtxCliIngestQueryExecutor), which resolves secret references and supports
every driver. Delete the now-dead local/postgres/sqlite query executors, their
tests, and the orphaned hasLocalQueryExecutor driver flag.
* docs(agents): require one implementation per capability
Add a design-reasoning default and a matching self-check question telling agents
to route callers through a single shared implementation of a capability rather
than forking a parallel one, and to fix the shared layer rather than patch one
branch. Encodes the lesson from a divergent SQL-execution-path bug, stated
generally.
CLAUDE.md is a symlink to AGENTS.md, so both agent-instruction files are covered.
The ingest HUD showed "step 70/40" because the Claude subscription runtime
re-derived a per-turn counter that could not match the SDK's num_turns and
overshot the maxTurns budget. Replace the turn-based work_unit_step heartbeat
with a real, observed tool-call count (no denominator), report
metrics.stepCount from the SDK's authoritative num_turns, and delete the
brittle countsAsAssistantTurn denylist plus the now-unused onStepFinish
callback across the runtime port and all three runtimes. Reconcile and curator
progress move to the same tool-call heartbeat.
* feat(mysql): implement columnStats using INFORMATION_SCHEMA.STATISTICS
Enable column cardinality statistics for the MySQL connector by querying
INFORMATION_SCHEMA.STATISTICS, which provides index-based cardinality
estimates without requiring additional permissions.
- Add generateColumnStatisticsQuery() to KtxMysqlDialect
- Add getColumnStatistics() and columnStats() to KtxMysqlScanConnector
- Flip columnStats capability from false to true
- Add MysqlStatsRow and KtxMysqlColumnStatisticsResult interfaces
- Add tests for dialect query generation and connector stats retrieval
- Update dialect conformance fixture for mysql
* fix(mysql): filter to leading index columns to avoid inflated cardinality
Add AND SEQ_IN_INDEX = 1 to INFORMATION_SCHEMA.STATISTICS query to
ensure only leading index columns are returned. For composite indexes,
non-leading columns report the cardinality of the index prefix rather
than the column's own distinct count, which inflates distinctCount.
Add regression test asserting SEQ_IN_INDEX = 1 is present in the query.
* fix: add trailing newline to dialect.test.ts
---------
Co-authored-by: Andrey Avtomonov <andreybavt@gmail.com>
- Link the Y Combinator badge and the docs "by Kaelio" label
- Add a maintainer line to the README
- Set the npm author field on @kaelio/ktx
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): show cached update notices after commands
* docs(cli): describe update notices
* fix(cli): type update check environment
* fix(cli): decouple update notice display from refresh and harden suppression
Display a cached "update available" notice based solely on the lastNoticeAt
24h throttle, independent of checkedAt refresh freshness, matching the design's
independent display/refresh decisions. Suppress the check unconditionally under
--json, CI, and non-TTY before consulting output-mode preferences, so a
KTX_OUTPUT=pretty override can no longer make CI/non-TTY contexts phone npm.
* feat(docs): add serving-phase diagram to the introduction page
The introduction's "How ktx works" section described both the ingest and serve sides but only rendered the ingestion diagram. Add a live, theme-aware React Flow diagram for the serving phase (agent <-> ktx via MCP -> context layer + database) so both phases are shown, with a matching content test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(diagram-studio): relabel context edge and use right-angle routing
The hub->context edge searches and reads definitions, not just searches; relabel it "search + read". Route the serving search/read-only edges with smoothstep (right angles) to match the docs diagram. (The README PNG is a baked export and is unchanged until re-exported from the studio.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(docs): point product-mechanics assertions at the FlowCanvas wrapper
product-mechanics renders via the shared FlowCanvas wrapper, so the ReactFlow config (nodesDraggable, zoomOnScroll, etc.) lives there now. Update the stale assertions that still expected those literals inline, fixing a pre-existing test failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(serving-diagram): shrink the boxes and drop OpenCode from the agent list
Reduce node dimensions, font sizes, padding, and the canvas height so the serving diagram renders ~25% smaller and more compact. Remove OpenCode from the agent's listed clients.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ktx.sh/ and docs.ktx.sh/ redirected to
https://docs.kaelio.com/ktx/ktx/docs/... (note the doubled /ktx) and 404'd.
The host-agnostic `source: "/"` redirect ran before the alias-host
canonicalizers, so it injected the /ktx basePath into the path on the alias
domains, which the alias catch-all then prepended a second time.
Reorder redirects() so alias-host canonicalization runs first, leaving the
generic root/docs rules for the local/canonical host only. The /stars
exclusion stays because redirects run before beforeFiles rewrites.
Add Host-spoofing regression tests (the prior tests only used localhost,
which never exercised the alias-host rules) and remove the vestigial
website/vercel.json, which the live ktx.sh routing already bypasses.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): add ingest rate limit governor
* feat(cli): wire ingest rate-limit config
* feat(cli): report provider rate-limit signals
* feat(cli): show ingest rate-limit waits
* fix(cli): complete rate-limit event coverage
* fix(cli): abort ingest provider calls cleanly
* fix(cli): propagate ingest cancellation
* fix(cli): reject pre-aborted ingest rate-limit waits
* fix(cli): honor Claude rate-limit reset waits
* fix(cli): retry thrown Codex rate-limit failures
* fix(cli): type Claude rate-limit result details
* fix(cli): emit ingest rate-limit countdowns from rejected signals
* fix(cli): report ai sdk rate-limit header utilization
* fix(cli): gate LLM rate-limit retries on the governor budget
The AI SDK and Codex runtimes retried 429 / opaque rate-limit failures up
to 6-7 times with no backoff when constructed without a RateLimitGovernor
(scan, memory, setup) or with pacing disabled, ignoring Retry-After and
worsening the limit. The outer retry loop only cooperates with the
governor's pause, so without active pacing there is no backoff to apply.
Route the retry bound through a single source: RateLimitGovernor
.maxRetryAttempts(), which returns retry.maxAttempts when enabled and 1
(no outer retry) when absent or disabled. All three runtimes (ai-sdk,
codex, claude-code) now use it, so ingest.rateLimit.retry.maxAttempts
genuinely controls attempts and the hard-coded 6 (plus Codex's off-by-one
extra attempt) is gone. Backend-native retry (e.g. the AI SDK's maxRetries)
still handles transient 429s.
Also correct the ktx.yaml docs for maxWaitMs (caps each wait, not the whole
run) and maxAttempts, and sync uv.lock ktx-sl/ktx-daemon to 0.9.0.