rename klo to ktx

This commit is contained in:
Andrey Avtomonov 2026-05-10 23:51:24 +02:00
parent 1a42152e6f
commit 3ce510b55b
704 changed files with 10205 additions and 10255 deletions

View file

@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest';
import { buildDefaultKloProjectConfig, parseKloProjectConfig, serializeKloProjectConfig } from './config.js';
import { buildDefaultKtxProjectConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from './config.js';
describe('KLO project config', () => {
describe('KTX project config', () => {
it('builds the default standalone project config', () => {
expect(buildDefaultKloProjectConfig('warehouse')).toEqual({
expect(buildDefaultKtxProjectConfig('warehouse')).toEqual({
project: 'warehouse',
connections: {},
storage: {
@ -11,7 +11,7 @@ describe('KLO project config', () => {
search: 'sqlite-fts5',
git: {
auto_commit: true,
author: 'klo <klo@example.com>',
author: 'ktx <ktx@example.com>',
},
},
llm: {
@ -63,8 +63,8 @@ describe('KLO project config', () => {
});
it('round-trips through YAML with stable defaults', () => {
const serialized = serializeKloProjectConfig(buildDefaultKloProjectConfig('warehouse'));
const parsed = parseKloProjectConfig(serialized);
const serialized = serializeKtxProjectConfig(buildDefaultKtxProjectConfig('warehouse'));
const parsed = parseKtxProjectConfig(serialized);
expect(serialized).toContain('project: warehouse');
expect(serialized).toContain('live-database');
@ -82,7 +82,7 @@ describe('KLO project config', () => {
});
it('parses and serializes setup wizard metadata', () => {
const config = parseKloProjectConfig(`
const config = parseKtxProjectConfig(`
project: revenue
setup:
database_connection_ids:
@ -102,14 +102,14 @@ connections:
completed_steps: ['project', 'llm'],
});
const serialized = serializeKloProjectConfig(config);
const serialized = serializeKtxProjectConfig(config);
expect(serialized).toContain('setup:');
expect(serialized).toContain('database_connection_ids:');
expect(serialized).toContain('completed_steps:');
});
it('parses global direct Anthropic LLM config', () => {
const config = parseKloProjectConfig(`
const config = parseKtxProjectConfig(`
project: demo
llm:
provider:
@ -149,7 +149,7 @@ ingest:
});
it('parses global Vertex LLM config', () => {
const config = parseKloProjectConfig(`
const config = parseKtxProjectConfig(`
project: demo
llm:
provider:
@ -171,7 +171,7 @@ llm:
});
it('parses gateway LLM, OpenAI scan embeddings, and sentence-transformers ingest embeddings', () => {
const config = parseKloProjectConfig(`
const config = parseKtxProjectConfig(`
project: demo
llm:
provider:
@ -215,7 +215,7 @@ scan:
});
it('parses scan relationship settings', () => {
const config = parseKloProjectConfig(`
const config = parseKtxProjectConfig(`
project: demo
scan:
relationships:
@ -243,20 +243,20 @@ scan:
validationConcurrency: 2,
validationBudget: 0,
});
expect(serializeKloProjectConfig(config)).toContain('enabled: false');
expect(serializeKloProjectConfig(config)).toContain('llmProposals: false');
expect(serializeKloProjectConfig(config)).toContain('validationRequiredForManifest: true');
expect(serializeKloProjectConfig(config)).toContain('acceptThreshold: 0.91');
expect(serializeKloProjectConfig(config)).toContain('reviewThreshold: 0.61');
expect(serializeKloProjectConfig(config)).toContain('maxLlmTablesPerBatch: 12');
expect(serializeKloProjectConfig(config)).toContain('maxCandidatesPerColumn: 7');
expect(serializeKloProjectConfig(config)).toContain('profileSampleRows: 500');
expect(serializeKloProjectConfig(config)).toContain('validationConcurrency: 2');
expect(serializeKloProjectConfig(config)).toContain('validationBudget: 0');
expect(serializeKtxProjectConfig(config)).toContain('enabled: false');
expect(serializeKtxProjectConfig(config)).toContain('llmProposals: false');
expect(serializeKtxProjectConfig(config)).toContain('validationRequiredForManifest: true');
expect(serializeKtxProjectConfig(config)).toContain('acceptThreshold: 0.91');
expect(serializeKtxProjectConfig(config)).toContain('reviewThreshold: 0.61');
expect(serializeKtxProjectConfig(config)).toContain('maxLlmTablesPerBatch: 12');
expect(serializeKtxProjectConfig(config)).toContain('maxCandidatesPerColumn: 7');
expect(serializeKtxProjectConfig(config)).toContain('profileSampleRows: 500');
expect(serializeKtxProjectConfig(config)).toContain('validationConcurrency: 2');
expect(serializeKtxProjectConfig(config)).toContain('validationBudget: 0');
});
it('parses the scan relationship validation budget sentinel', () => {
const config = parseKloProjectConfig(`
const config = parseKtxProjectConfig(`
project: demo
scan:
relationships:
@ -264,11 +264,11 @@ scan:
`);
expect(config.scan.relationships.validationBudget).toBe('all');
expect(serializeKloProjectConfig(config)).toContain('validationBudget: all');
expect(serializeKtxProjectConfig(config)).toContain('validationBudget: all');
});
it('falls back to safe scan relationship defaults for invalid numeric settings', () => {
const config = parseKloProjectConfig(`
const config = parseKtxProjectConfig(`
project: demo
scan:
relationships:
@ -293,7 +293,7 @@ scan:
});
it('falls back for invalid scan relationship validation budget strings', () => {
const config = parseKloProjectConfig(`
const config = parseKtxProjectConfig(`
project: demo
scan:
relationships:
@ -305,7 +305,7 @@ scan:
it('rejects legacy local LLM and embedding fields', () => {
expect(() =>
parseKloProjectConfig(`
parseKtxProjectConfig(`
project: demo
ingest:
llm:
@ -314,7 +314,7 @@ ingest:
).toThrow('Unsupported ingest.llm: use top-level llm.provider, llm.models, and ingest.workUnits');
expect(() =>
parseKloProjectConfig(`
parseKtxProjectConfig(`
project: demo
scan:
enrichment:
@ -323,7 +323,7 @@ scan:
).toThrow('Unsupported scan.enrichment.backend: use scan.enrichment.mode');
expect(() =>
parseKloProjectConfig(`
parseKtxProjectConfig(`
project: demo
scan:
enrichment:
@ -334,7 +334,7 @@ scan:
).toThrow('Unsupported scan.enrichment.llm: use top-level llm.provider and llm.models');
expect(() =>
parseKloProjectConfig(`
parseKtxProjectConfig(`
project: demo
ingest:
embeddings:
@ -346,7 +346,7 @@ ingest:
it('rejects gateway embedding configs', () => {
expect(() =>
parseKloProjectConfig(`
parseKtxProjectConfig(`
project: demo
ingest:
embeddings:
@ -357,7 +357,7 @@ ingest:
).toThrow('Unsupported ingest.embeddings.backend: gateway');
expect(() =>
parseKloProjectConfig(`
parseKtxProjectConfig(`
project: demo
scan:
enrichment:
@ -371,9 +371,9 @@ scan:
});
it('fills optional sections when a minimal config is loaded', () => {
const config = parseKloProjectConfig('project: local\n');
const config = parseKtxProjectConfig('project: local\n');
expect(config).toEqual(buildDefaultKloProjectConfig('local'));
expect(config).toEqual(buildDefaultKtxProjectConfig('local'));
expect(config.ingest.embeddings).toEqual({
backend: 'deterministic',
model: 'deterministic',
@ -382,10 +382,10 @@ scan:
});
it('rejects configs without an object root', () => {
expect(() => parseKloProjectConfig('- nope\n')).toThrow('klo.yaml must contain a YAML object');
expect(() => parseKtxProjectConfig('- nope\n')).toThrow('ktx.yaml must contain a YAML object');
});
it('rejects configs with a missing project name', () => {
expect(() => parseKloProjectConfig('connections: {}\n')).toThrow('klo.yaml field "project" is required');
expect(() => parseKtxProjectConfig('connections: {}\n')).toThrow('ktx.yaml field "project" is required');
});
});

View file

@ -1,35 +1,35 @@
import type { KloEmbeddingBackend, KloLlmBackend, KloModelRole, KloPromptCacheTtl } from '@klo/llm';
import type { KtxEmbeddingBackend, KtxLlmBackend, KtxModelRole, KtxPromptCacheTtl } from '@ktx/llm';
import YAML from 'yaml';
export type KloStorageState = 'postgres' | 'sqlite';
export type KloSearchBackend = 'postgres-hybrid' | 'sqlite-fts5';
type KloLocalLlmBackend = KloLlmBackend | 'none';
type KloLocalEmbeddingBackend = KloEmbeddingBackend | 'none';
type KloScanEnrichmentMode = 'none' | 'deterministic' | 'llm';
export type KtxStorageState = 'postgres' | 'sqlite';
export type KtxSearchBackend = 'postgres-hybrid' | 'sqlite-fts5';
type KtxLocalLlmBackend = KtxLlmBackend | 'none';
type KtxLocalEmbeddingBackend = KtxEmbeddingBackend | 'none';
type KtxScanEnrichmentMode = 'none' | 'deterministic' | 'llm';
interface KloProjectPromptCachingConfig {
interface KtxProjectPromptCachingConfig {
enabled?: boolean;
systemTtl?: KloPromptCacheTtl;
toolsTtl?: KloPromptCacheTtl;
historyTtl?: KloPromptCacheTtl;
systemTtl?: KtxPromptCacheTtl;
toolsTtl?: KtxPromptCacheTtl;
historyTtl?: KtxPromptCacheTtl;
vertexFallbackTo5m?: boolean;
}
export interface KloProjectLlmProviderConfig {
backend: KloLocalLlmBackend;
export interface KtxProjectLlmProviderConfig {
backend: KtxLocalLlmBackend;
vertex?: { project?: string; location: string };
anthropic?: { api_key?: string; base_url?: string };
gateway?: { api_key?: string; base_url?: string };
}
export interface KloProjectLlmConfig {
provider: KloProjectLlmProviderConfig;
models: Partial<Record<KloModelRole, string>> & { default?: string };
promptCaching?: KloProjectPromptCachingConfig;
export interface KtxProjectLlmConfig {
provider: KtxProjectLlmProviderConfig;
models: Partial<Record<KtxModelRole, string>> & { default?: string };
promptCaching?: KtxProjectPromptCachingConfig;
}
export interface KloProjectEmbeddingConfig {
backend: KloLocalEmbeddingBackend;
export interface KtxProjectEmbeddingConfig {
backend: KtxLocalEmbeddingBackend;
model?: string;
dimensions: number;
openai?: { api_key?: string; base_url?: string };
@ -37,18 +37,18 @@ export interface KloProjectEmbeddingConfig {
batchSize?: number;
}
export interface KloScanEnrichmentConfig {
mode: KloScanEnrichmentMode;
embeddings?: KloProjectEmbeddingConfig;
export interface KtxScanEnrichmentConfig {
mode: KtxScanEnrichmentMode;
embeddings?: KtxProjectEmbeddingConfig;
}
export interface KloIngestWorkUnitsConfig {
export interface KtxIngestWorkUnitsConfig {
stepBudget: number;
maxConcurrency: number;
failureMode: 'abort' | 'continue';
}
export interface KloScanRelationshipConfig {
export interface KtxScanRelationshipConfig {
enabled: boolean;
llmProposals: boolean;
validationRequiredForManifest: boolean;
@ -61,40 +61,40 @@ export interface KloScanRelationshipConfig {
validationBudget?: number | 'all';
}
export interface KloProjectScanConfig {
enrichment: KloScanEnrichmentConfig;
relationships: KloScanRelationshipConfig;
export interface KtxProjectScanConfig {
enrichment: KtxScanEnrichmentConfig;
relationships: KtxScanRelationshipConfig;
}
export interface KloProjectConnectionConfig {
export interface KtxProjectConnectionConfig {
driver: string;
url?: string;
readonly?: boolean;
[key: string]: unknown;
}
export interface KloProjectSetupConfig {
export interface KtxProjectSetupConfig {
database_connection_ids: string[];
completed_steps: string[];
}
export interface KloProjectConfig {
export interface KtxProjectConfig {
project: string;
setup?: KloProjectSetupConfig;
connections: Record<string, KloProjectConnectionConfig>;
setup?: KtxProjectSetupConfig;
connections: Record<string, KtxProjectConnectionConfig>;
storage: {
state: KloStorageState;
search: KloSearchBackend;
state: KtxStorageState;
search: KtxSearchBackend;
git: {
auto_commit: boolean;
author: string;
};
};
llm: KloProjectLlmConfig;
llm: KtxProjectLlmConfig;
ingest: {
adapters: string[];
embeddings: KloProjectEmbeddingConfig;
workUnits: KloIngestWorkUnitsConfig;
embeddings: KtxProjectEmbeddingConfig;
workUnits: KtxIngestWorkUnitsConfig;
};
agent: {
run_research: {
@ -106,7 +106,7 @@ export interface KloProjectConfig {
memory: {
auto_commit: boolean;
};
scan: KloProjectScanConfig;
scan: KtxProjectScanConfig;
}
function isRecord(value: unknown): value is Record<string, unknown> {
@ -167,7 +167,7 @@ function ratioConfigValue(value: unknown, fallback: number): number {
return value;
}
function localLlmBackend(value: unknown, fallback: KloLocalLlmBackend, section = 'llm.provider'): KloLocalLlmBackend {
function localLlmBackend(value: unknown, fallback: KtxLocalLlmBackend, section = 'llm.provider'): KtxLocalLlmBackend {
if (value == null) {
return fallback;
}
@ -181,9 +181,9 @@ function localLlmBackend(value: unknown, fallback: KloLocalLlmBackend, section =
function localEmbeddingBackend(
value: unknown,
fallback: KloLocalEmbeddingBackend,
fallback: KtxLocalEmbeddingBackend,
section = 'ingest.embeddings',
): KloLocalEmbeddingBackend {
): KtxLocalEmbeddingBackend {
if (value == null) {
return fallback;
}
@ -200,7 +200,7 @@ function localEmbeddingBackend(
throw new Error(`Unsupported ${section}.backend: ${String(value)}`);
}
function scanEnrichmentMode(value: unknown, fallback: KloScanEnrichmentMode): KloScanEnrichmentMode {
function scanEnrichmentMode(value: unknown, fallback: KtxScanEnrichmentMode): KtxScanEnrichmentMode {
if (value == null) {
return fallback;
}
@ -239,26 +239,26 @@ function optionalProviderConfig(value: unknown): { api_key?: string; base_url?:
};
}
function parseModels(value: unknown): KloProjectLlmConfig['models'] {
function parseModels(value: unknown): KtxProjectLlmConfig['models'] {
if (!isRecord(value)) {
return {};
}
const models: KloProjectLlmConfig['models'] = {};
const models: KtxProjectLlmConfig['models'] = {};
for (const [role, model] of Object.entries(value)) {
const modelName = optionalNonEmptyString(model);
if (modelName) {
models[role as KloModelRole] = modelName;
models[role as KtxModelRole] = modelName;
}
}
return models;
}
function promptCacheTtl(value: unknown): KloPromptCacheTtl | undefined {
function promptCacheTtl(value: unknown): KtxPromptCacheTtl | undefined {
return value === '5m' || value === '1h' ? value : undefined;
}
function parsePromptCaching(value: unknown): KloProjectPromptCachingConfig | undefined {
function parsePromptCaching(value: unknown): KtxProjectPromptCachingConfig | undefined {
if (!isRecord(value)) {
return undefined;
}
@ -274,9 +274,9 @@ function parsePromptCaching(value: unknown): KloProjectPromptCachingConfig | und
function parseProjectLlmProviderConfig(
raw: Record<string, unknown>,
defaults: KloProjectLlmProviderConfig,
defaults: KtxProjectLlmProviderConfig,
section: string,
): KloProjectLlmProviderConfig {
): KtxProjectLlmProviderConfig {
rejectLegacyProvider(section, raw.provider);
const vertex = isRecord(raw.vertex)
@ -296,7 +296,7 @@ function parseProjectLlmProviderConfig(
};
}
function parseProjectLlmConfig(raw: Record<string, unknown>, defaults: KloProjectLlmConfig): KloProjectLlmConfig {
function parseProjectLlmConfig(raw: Record<string, unknown>, defaults: KtxProjectLlmConfig): KtxProjectLlmConfig {
const provider = isRecord(raw.provider) ? raw.provider : {};
return {
provider: parseProjectLlmProviderConfig(provider, defaults.provider, 'llm.provider'),
@ -307,9 +307,9 @@ function parseProjectLlmConfig(raw: Record<string, unknown>, defaults: KloProjec
function parseProjectEmbeddingConfig(
raw: Record<string, unknown>,
defaults: KloProjectEmbeddingConfig,
defaults: KtxProjectEmbeddingConfig,
section: string,
): KloProjectEmbeddingConfig {
): KtxProjectEmbeddingConfig {
rejectLegacyProvider(section, raw.provider);
const openai = optionalProviderConfig(raw.openai);
@ -338,8 +338,8 @@ function parseProjectEmbeddingConfig(
function parseScanRelationshipConfig(
raw: Record<string, unknown>,
defaults: KloScanRelationshipConfig,
): KloScanRelationshipConfig {
defaults: KtxScanRelationshipConfig,
): KtxScanRelationshipConfig {
const validationBudget = validationBudgetConfigValue(
raw.validation_budget ?? raw.validationBudget,
defaults.validationBudget,
@ -380,8 +380,8 @@ function workUnitFailureMode(value: unknown, fallback: 'abort' | 'continue'): 'a
function parseIngestWorkUnitsConfig(
raw: Record<string, unknown>,
defaults: KloIngestWorkUnitsConfig,
): KloIngestWorkUnitsConfig {
defaults: KtxIngestWorkUnitsConfig,
): KtxIngestWorkUnitsConfig {
return {
stepBudget: positiveIntegerConfigValue(raw.stepBudget, defaults.stepBudget),
maxConcurrency: positiveIntegerConfigValue(raw.maxConcurrency, defaults.maxConcurrency),
@ -389,7 +389,7 @@ function parseIngestWorkUnitsConfig(
};
}
export function buildDefaultKloProjectConfig(projectName = 'klo-project'): KloProjectConfig {
export function buildDefaultKtxProjectConfig(projectName = 'ktx-project'): KtxProjectConfig {
return {
project: projectName,
connections: {},
@ -398,7 +398,7 @@ export function buildDefaultKloProjectConfig(projectName = 'klo-project'): KloPr
search: 'sqlite-fts5',
git: {
auto_commit: true,
author: 'klo <klo@example.com>',
author: 'ktx <ktx@example.com>',
},
},
llm: {
@ -449,18 +449,18 @@ export function buildDefaultKloProjectConfig(projectName = 'klo-project'): KloPr
};
}
export function parseKloProjectConfig(raw: string): KloProjectConfig {
export function parseKtxProjectConfig(raw: string): KtxProjectConfig {
const parsed = YAML.parse(raw) as unknown;
if (!isRecord(parsed)) {
throw new Error('klo.yaml must contain a YAML object');
throw new Error('ktx.yaml must contain a YAML object');
}
const project = parsed.project;
if (typeof project !== 'string' || project.trim().length === 0) {
throw new Error('klo.yaml field "project" is required');
throw new Error('ktx.yaml field "project" is required');
}
const defaults = buildDefaultKloProjectConfig(project.trim());
const defaults = buildDefaultKtxProjectConfig(project.trim());
const llm = isRecord(parsed.llm) ? parsed.llm : {};
const storage = isRecord(parsed.storage) ? parsed.storage : {};
const storageGit = isRecord(storage.git) ? storage.git : {};
@ -496,7 +496,7 @@ export function parseKloProjectConfig(raw: string): KloProjectConfig {
defaults.ingest.embeddings,
'scan.enrichment.embeddings',
);
const parsedScanEnrichment: KloScanEnrichmentConfig = {
const parsedScanEnrichment: KtxScanEnrichmentConfig = {
mode: scanEnrichmentMode(scanEnrichment.mode, defaults.scan.enrichment.mode),
...(isRecord(scanEnrichment.embeddings) ? { embeddings: scanEmbeddings } : {}),
};
@ -513,7 +513,7 @@ export function parseKloProjectConfig(raw: string): KloProjectConfig {
}
: {}),
connections: isRecord(parsed.connections)
? (parsed.connections as Record<string, KloProjectConnectionConfig>)
? (parsed.connections as Record<string, KtxProjectConnectionConfig>)
: defaults.connections,
storage: {
state: storage.state === 'sqlite' ? 'sqlite' : defaults.storage.state,
@ -546,6 +546,6 @@ export function parseKloProjectConfig(raw: string): KloProjectConfig {
};
}
export function serializeKloProjectConfig(config: KloProjectConfig): string {
export function serializeKtxProjectConfig(config: KtxProjectConfig): string {
return `${YAML.stringify(config, { indent: 2, lineWidth: 0 }).trimEnd()}\n`;
}

View file

@ -1,15 +1,15 @@
export type {
KloProjectConfig,
KloProjectConnectionConfig,
KloProjectEmbeddingConfig,
KloProjectLlmConfig,
KloSearchBackend,
KloStorageState,
KtxProjectConfig,
KtxProjectConnectionConfig,
KtxProjectEmbeddingConfig,
KtxProjectLlmConfig,
KtxSearchBackend,
KtxStorageState,
} from './config.js';
export { buildDefaultKloProjectConfig, parseKloProjectConfig, serializeKloProjectConfig } from './config.js';
export { buildDefaultKtxProjectConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from './config.js';
export type { LocalGitFileStoreDeps } from './local-git-file-store.js';
export { LocalGitFileStore } from './local-git-file-store.js';
export { kloLocalStateDbPath } from './local-state-db.js';
export { ktxLocalStateDbPath } from './local-state-db.js';
export type {
ConnectionMappingBootstrap,
LookerMappingBootstrap,
@ -22,12 +22,12 @@ export {
parseLookmlMappingBootstrap,
parseMetabaseMappingBootstrap,
} from './mappings-yaml-schema.js';
export type { InitKloProjectOptions, InitKloProjectResult, KloLocalProject, LoadKloProjectOptions } from './project.js';
export { initKloProject, loadKloProject } from './project.js';
export type { KloSetupStep } from './setup-config.js';
export type { InitKtxProjectOptions, InitKtxProjectResult, KtxLocalProject, LoadKtxProjectOptions } from './project.js';
export { initKtxProject, loadKtxProject } from './project.js';
export type { KtxSetupStep } from './setup-config.js';
export {
KLO_SETUP_STEPS,
markKloSetupStepComplete,
mergeKloSetupGitignoreEntries,
setKloSetupDatabaseConnectionIds,
KTX_SETUP_STEPS,
markKtxSetupStepComplete,
mergeKtxSetupGitignoreEntries,
setKtxSetupDatabaseConnectionIds,
} from './setup-config.js';

View file

@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { GitService, type KloCoreConfig } from '../core/index.js';
import { GitService, type KtxCoreConfig } from '../core/index.js';
import { LocalGitFileStore } from './local-git-file-store.js';
describe('LocalGitFileStore', () => {
@ -10,15 +10,15 @@ describe('LocalGitFileStore', () => {
let store: LocalGitFileStore;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-local-store-'));
const coreConfig: KloCoreConfig = {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-local-store-'));
const coreConfig: KtxCoreConfig = {
storage: { configDir: tempDir, homeDir: tempDir },
git: {
userName: 'klo',
userEmail: 'klo@example.com',
userName: 'ktx',
userEmail: 'ktx@example.com',
bootstrapMessage: 'Initialize test project',
bootstrapAuthor: 'klo',
bootstrapAuthorEmail: 'klo@example.com',
bootstrapAuthor: 'ktx',
bootstrapAuthorEmail: 'ktx@example.com',
},
};
const git = new GitService(coreConfig);

View file

@ -3,11 +3,11 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import type {
GitCommitInfo,
GitService,
KloFileHistoryEntry,
KloFileListResult,
KloFileReadResult,
KloFileStorePort,
KloFileWriteResult,
KtxFileHistoryEntry,
KtxFileListResult,
KtxFileReadResult,
KtxFileStorePort,
KtxFileWriteResult,
} from '../core/index.js';
export interface LocalGitFileStoreDeps {
@ -19,7 +19,7 @@ function normalizeRelativePath(filePath: string): string {
return filePath.replaceAll('\\', '/').replace(/^\.\/+/, '');
}
function gitInfoToWriteResult(info: GitCommitInfo): KloFileWriteResult {
function gitInfoToWriteResult(info: GitCommitInfo): KtxFileWriteResult {
return {
success: true,
commitHash: info.commitHash,
@ -31,7 +31,7 @@ function gitInfoToWriteResult(info: GitCommitInfo): KloFileWriteResult {
};
}
export class LocalGitFileStore implements KloFileStorePort<LocalGitFileStore> {
export class LocalGitFileStore implements KtxFileStorePort<LocalGitFileStore> {
private readonly rootDir: string;
private readonly git: GitService;
@ -51,7 +51,7 @@ export class LocalGitFileStore implements KloFileStorePort<LocalGitFileStore> {
authorEmail: string,
commitMessage: string,
options?: { skipLock?: boolean },
): Promise<KloFileWriteResult> {
): Promise<KtxFileWriteResult> {
const relativePath = this.safeRelativePath(path);
const absolutePath = this.absolutePath(relativePath);
await fs.mkdir(dirname(absolutePath), { recursive: true });
@ -65,7 +65,7 @@ export class LocalGitFileStore implements KloFileStorePort<LocalGitFileStore> {
return { ...gitInfoToWriteResult(info), path: relativePath, operation: 'write' };
}
async readFile(path: string): Promise<KloFileReadResult> {
async readFile(path: string): Promise<KtxFileReadResult> {
const relativePath = this.safeRelativePath(path);
const absolutePath = this.absolutePath(relativePath);
const content = await fs.readFile(absolutePath, 'utf-8');
@ -84,7 +84,7 @@ export class LocalGitFileStore implements KloFileStorePort<LocalGitFileStore> {
authorEmail: string,
commitMessage: string,
options?: { skipLock?: boolean },
): Promise<KloFileWriteResult | null> {
): Promise<KtxFileWriteResult | null> {
const relativePath = this.safeRelativePath(path);
const absolutePath = this.absolutePath(relativePath);
try {
@ -103,7 +103,7 @@ export class LocalGitFileStore implements KloFileStorePort<LocalGitFileStore> {
return { ...gitInfoToWriteResult(info), path: relativePath, operation: 'delete' };
}
async listFiles(path = '', stripPrefix = false): Promise<KloFileListResult> {
async listFiles(path = '', stripPrefix = false): Promise<KtxFileListResult> {
const relativePath = path ? this.safeRelativePath(path) : '';
const searchRoot = relativePath ? this.absolutePath(relativePath) : this.rootDir;
let files: string[];
@ -121,14 +121,14 @@ export class LocalGitFileStore implements KloFileStorePort<LocalGitFileStore> {
const relativeFiles = files
.map((file) => normalizeRelativePath(relative(this.rootDir, file)))
.filter((file) => !file.startsWith('.git/') && !file.includes('/.git/'))
.filter((file) => !file.startsWith('.klo/cache/'))
.filter((file) => !file.startsWith('.ktx/cache/'))
.map((file) => (stripPrefix && prefix && file.startsWith(prefix) ? file.slice(prefix.length) : file))
.sort();
return { files: relativeFiles };
}
async getFileHistory(path: string): Promise<KloFileHistoryEntry[]> {
async getFileHistory(path: string): Promise<KtxFileHistoryEntry[]> {
const relativePath = this.safeRelativePath(path);
const history = await this.git.getFileHistory(relativePath);
return history.map((entry) => ({

View file

@ -1,6 +1,6 @@
import { join } from 'node:path';
import type { KloLocalProject } from './project.js';
import type { KtxLocalProject } from './project.js';
export function kloLocalStateDbPath(project: Pick<KloLocalProject, 'projectDir'>): string {
return join(project.projectDir, '.klo', 'db.sqlite');
export function ktxLocalStateDbPath(project: Pick<KtxLocalProject, 'projectDir'>): string {
return join(project.projectDir, '.ktx', 'db.sqlite');
}

View file

@ -6,7 +6,7 @@ import {
parseMetabaseMappingBootstrap,
} from './mappings-yaml-schema.js';
describe('klo.yaml mapping bootstrap schema', () => {
describe('ktx.yaml mapping bootstrap schema', () => {
it('parses Metabase mapping intent with CLI syncMode default ALL', () => {
const bootstrap = parseMetabaseMappingBootstrap('prod-metabase', {
driver: 'metabase',
@ -14,7 +14,7 @@ describe('klo.yaml mapping bootstrap schema', () => {
databaseMappings: { '1': 'prod-warehouse', '2': null },
syncEnabled: { '1': true, '2': false },
selections: { collections: [12], items: [345] },
defaultTagNames: ['klo', 'prod'],
defaultTagNames: ['ktx', 'prod'],
},
});
@ -25,7 +25,7 @@ describe('klo.yaml mapping bootstrap schema', () => {
syncEnabled: { '1': true, '2': false },
syncMode: 'ALL',
selections: { collections: [12], items: [345] },
defaultTagNames: ['klo', 'prod'],
defaultTagNames: ['ktx', 'prod'],
});
});

View file

@ -1,5 +1,5 @@
import * as z from 'zod';
import type { KloProjectConnectionConfig } from './config.js';
import type { KtxProjectConnectionConfig } from './config.js';
const metabaseSyncModeSchema = z.enum(['ALL', 'ONLY', 'EXCEPT']);
const positiveIntegerValueSchema = z.number().int().positive();
@ -66,13 +66,13 @@ function assertPositiveIntegerKeys(field: string, record: Record<string, unknown
}
}
function driverOf(connection: KloProjectConnectionConfig): string {
function driverOf(connection: KtxProjectConnectionConfig): string {
return String(connection.driver ?? '').toLowerCase();
}
export function parseMetabaseMappingBootstrap(
connectionId: string,
connection: KloProjectConnectionConfig,
connection: KtxProjectConnectionConfig,
): MetabaseMappingBootstrap {
const rawMappings = recordValue(connection.mappings);
assertPositiveIntegerKeys('databaseMappings', recordValue(rawMappings.databaseMappings));
@ -91,7 +91,7 @@ export function parseMetabaseMappingBootstrap(
export function parseLookerMappingBootstrap(
connectionId: string,
connection: KloProjectConnectionConfig,
connection: KtxProjectConnectionConfig,
): LookerMappingBootstrap {
const parsed = lookerMappingsSchema.parse(recordValue(connection.mappings));
return {
@ -103,7 +103,7 @@ export function parseLookerMappingBootstrap(
export function parseLookmlMappingBootstrap(
connectionId: string,
connection: KloProjectConnectionConfig,
connection: KtxProjectConnectionConfig,
): LookmlMappingBootstrap {
const parsed = lookmlMappingsSchema.parse(recordValue(connection.mappings));
return {
@ -115,7 +115,7 @@ export function parseLookmlMappingBootstrap(
export function parseConnectionMappingBootstrap(
connectionId: string,
connection: KloProjectConnectionConfig,
connection: KtxProjectConnectionConfig,
): ConnectionMappingBootstrap | null {
if (!connection.mappings || typeof connection.mappings !== 'object' || Array.isArray(connection.mappings)) {
return null;

View file

@ -2,13 +2,13 @@ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { initKloProject, loadKloProject } from './project.js';
import { initKtxProject, loadKtxProject } from './project.js';
describe('KLO local project runtime', () => {
describe('KTX local project runtime', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-project-runtime-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-project-runtime-'));
});
afterEach(async () => {
@ -18,7 +18,7 @@ describe('KLO local project runtime', () => {
it('initializes the standalone project layout and commits it', async () => {
const projectDir = join(tempDir, 'warehouse');
const result = await initKloProject({
const result = await initKtxProject({
projectDir,
projectName: 'warehouse',
authorName: 'Agent',
@ -28,8 +28,8 @@ describe('KLO local project runtime', () => {
expect(result.projectDir).toBe(projectDir);
expect(result.config.project).toBe('warehouse');
expect(result.commitHash).toMatch(/^[0-9a-f]{40}$/);
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
const gitignore = await readFile(join(projectDir, '.klo/.gitignore'), 'utf-8');
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
const gitignore = await readFile(join(projectDir, '.ktx/.gitignore'), 'utf-8');
expect(gitignore).toContain('cache/');
expect(gitignore).toContain('db.sqlite');
expect(gitignore).toContain('secrets/');
@ -44,9 +44,9 @@ describe('KLO local project runtime', () => {
it('loads an initialized project with a working file store', async () => {
const projectDir = join(tempDir, 'warehouse');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const loaded = await loadKloProject({ projectDir });
const loaded = await loadKtxProject({ projectDir });
await loaded.fileStore.writeFile(
'knowledge/global/revenue.md',
'# Revenue\n',
@ -63,13 +63,13 @@ describe('KLO local project runtime', () => {
it('rejects reinitializing an existing project unless force is set', async () => {
const projectDir = join(tempDir, 'warehouse');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await expect(initKloProject({ projectDir, projectName: 'warehouse' })).rejects.toThrow(
'Project already contains klo.yaml',
await expect(initKtxProject({ projectDir, projectName: 'warehouse' })).rejects.toThrow(
'Project already contains ktx.yaml',
);
await expect(initKloProject({ projectDir, projectName: 'warehouse-v2', force: true })).resolves.toMatchObject({
await expect(initKtxProject({ projectDir, projectName: 'warehouse-v2', force: true })).resolves.toMatchObject({
config: {
project: 'warehouse-v2',
},

View file

@ -1,59 +1,59 @@
import { promises as fs } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { GitService, type KloCoreConfig, type KloLogger, noopLogger } from '../core/index.js';
import type { KloProjectConfig } from './config.js';
import { buildDefaultKloProjectConfig, parseKloProjectConfig, serializeKloProjectConfig } from './config.js';
import { GitService, type KtxCoreConfig, type KtxLogger, noopLogger } from '../core/index.js';
import type { KtxProjectConfig } from './config.js';
import { buildDefaultKtxProjectConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from './config.js';
import { LocalGitFileStore } from './local-git-file-store.js';
export interface InitKloProjectOptions {
export interface InitKtxProjectOptions {
projectDir: string;
projectName?: string;
force?: boolean;
authorName?: string;
authorEmail?: string;
logger?: KloLogger;
logger?: KtxLogger;
}
export interface LoadKloProjectOptions {
export interface LoadKtxProjectOptions {
projectDir: string;
authorName?: string;
authorEmail?: string;
logger?: KloLogger;
logger?: KtxLogger;
}
export interface KloLocalProject {
export interface KtxLocalProject {
projectDir: string;
configPath: string;
config: KloProjectConfig;
coreConfig: KloCoreConfig;
config: KtxProjectConfig;
coreConfig: KtxCoreConfig;
git: GitService;
fileStore: LocalGitFileStore;
}
export interface InitKloProjectResult extends KloLocalProject {
export interface InitKtxProjectResult extends KtxLocalProject {
commitHash: string | null;
}
const TRACKED_SCAFFOLD_FILES: Array<{ path: string; content: string }> = [
{ path: '.klo/.gitignore', content: 'cache/\ndb.sqlite\nsecrets/\nsetup/\nagents/\n' },
{ path: '.klo/prompts/.gitkeep', content: '' },
{ path: '.klo/skills/.gitkeep', content: '' },
{ path: '.ktx/.gitignore', content: 'cache/\ndb.sqlite\nsecrets/\nsetup/\nagents/\n' },
{ path: '.ktx/prompts/.gitkeep', content: '' },
{ path: '.ktx/skills/.gitkeep', content: '' },
{ path: 'knowledge/global/.gitkeep', content: '' },
{ path: 'semantic-layer/.gitkeep', content: '' },
{ path: 'raw-sources/.gitkeep', content: '' },
];
function createCoreConfig(projectDir: string, authorName: string, authorEmail: string): KloCoreConfig {
function createCoreConfig(projectDir: string, authorName: string, authorEmail: string): KtxCoreConfig {
return {
storage: {
configDir: projectDir,
homeDir: dirname(projectDir),
worktreesDir: join(projectDir, '.klo/worktrees'),
worktreesDir: join(projectDir, '.ktx/worktrees'),
},
git: {
userName: authorName,
userEmail: authorEmail,
bootstrapMessage: 'Initialize klo project repository',
bootstrapMessage: 'Initialize ktx project repository',
bootstrapAuthor: authorName,
bootstrapAuthorEmail: authorEmail,
},
@ -77,18 +77,18 @@ async function writeProjectFile(projectDir: string, relativePath: string, conten
async function createRuntime(
projectDir: string,
config: KloProjectConfig,
config: KtxProjectConfig,
authorName: string,
authorEmail: string,
logger: KloLogger,
): Promise<KloLocalProject> {
logger: KtxLogger,
): Promise<KtxLocalProject> {
const coreConfig = createCoreConfig(projectDir, authorName, authorEmail);
const git = new GitService(coreConfig, logger);
await git.onModuleInit();
return {
projectDir,
configPath: join(projectDir, 'klo.yaml'),
configPath: join(projectDir, 'ktx.yaml'),
config,
coreConfig,
git,
@ -96,31 +96,31 @@ async function createRuntime(
};
}
export async function initKloProject(options: InitKloProjectOptions): Promise<InitKloProjectResult> {
export async function initKtxProject(options: InitKtxProjectOptions): Promise<InitKtxProjectResult> {
const projectDir = resolve(options.projectDir);
const projectName = options.projectName?.trim() || basename(projectDir) || 'klo-project';
const authorName = options.authorName ?? 'klo';
const authorEmail = options.authorEmail ?? 'klo@example.com';
const projectName = options.projectName?.trim() || basename(projectDir) || 'ktx-project';
const authorName = options.authorName ?? 'ktx';
const authorEmail = options.authorEmail ?? 'ktx@example.com';
const logger = options.logger ?? noopLogger;
const configPath = join(projectDir, 'klo.yaml');
const configPath = join(projectDir, 'ktx.yaml');
await fs.mkdir(projectDir, { recursive: true });
if (!options.force && (await fileExists(configPath))) {
throw new Error(`Project already contains klo.yaml: ${configPath}`);
throw new Error(`Project already contains ktx.yaml: ${configPath}`);
}
const config = buildDefaultKloProjectConfig(projectName);
const config = buildDefaultKtxProjectConfig(projectName);
const runtime = await createRuntime(projectDir, config, authorName, authorEmail, logger);
await writeProjectFile(projectDir, 'klo.yaml', serializeKloProjectConfig(config));
await fs.mkdir(join(projectDir, '.klo/cache'), { recursive: true });
await writeProjectFile(projectDir, 'ktx.yaml', serializeKtxProjectConfig(config));
await fs.mkdir(join(projectDir, '.ktx/cache'), { recursive: true });
for (const file of TRACKED_SCAFFOLD_FILES) {
await writeProjectFile(projectDir, file.path, file.content);
}
const commit = await runtime.git.commitFiles(
['klo.yaml', ...TRACKED_SCAFFOLD_FILES.map((file) => file.path)],
`Initialize KLO project: ${projectName}`,
['ktx.yaml', ...TRACKED_SCAFFOLD_FILES.map((file) => file.path)],
`Initialize KTX project: ${projectName}`,
authorName,
authorEmail,
);
@ -131,13 +131,13 @@ export async function initKloProject(options: InitKloProjectOptions): Promise<In
};
}
export async function loadKloProject(options: LoadKloProjectOptions): Promise<KloLocalProject> {
export async function loadKtxProject(options: LoadKtxProjectOptions): Promise<KtxLocalProject> {
const projectDir = resolve(options.projectDir);
const authorName = options.authorName ?? 'klo';
const authorEmail = options.authorEmail ?? 'klo@example.com';
const authorName = options.authorName ?? 'ktx';
const authorEmail = options.authorEmail ?? 'ktx@example.com';
const logger = options.logger ?? noopLogger;
const configPath = join(projectDir, 'klo.yaml');
const configPath = join(projectDir, 'ktx.yaml');
const raw = await fs.readFile(configPath, 'utf-8');
const config = parseKloProjectConfig(raw);
const config = parseKtxProjectConfig(raw);
return createRuntime(projectDir, config, authorName, authorEmail, logger);
}

View file

@ -1,19 +1,19 @@
import { describe, expect, it } from 'vitest';
import { buildDefaultKloProjectConfig } from './config.js';
import { buildDefaultKtxProjectConfig } from './config.js';
import {
markKloSetupStepComplete,
mergeKloSetupGitignoreEntries,
setKloSetupDatabaseConnectionIds,
markKtxSetupStepComplete,
mergeKtxSetupGitignoreEntries,
setKtxSetupDatabaseConnectionIds,
} from './setup-config.js';
describe('KLO setup config helpers', () => {
describe('KTX setup config helpers', () => {
it('marks setup steps complete without duplicating existing state', () => {
const config = buildDefaultKloProjectConfig('warehouse');
const config = buildDefaultKtxProjectConfig('warehouse');
const withProject = markKloSetupStepComplete(config, 'project');
const withProjectAgain = markKloSetupStepComplete(withProject, 'project');
const withLlm = markKloSetupStepComplete(withProjectAgain, 'llm');
const withContext = markKloSetupStepComplete(withLlm, 'context');
const withProject = markKtxSetupStepComplete(config, 'project');
const withProjectAgain = markKtxSetupStepComplete(withProject, 'project');
const withLlm = markKtxSetupStepComplete(withProjectAgain, 'llm');
const withContext = markKtxSetupStepComplete(withLlm, 'context');
expect(withProject.setup).toEqual({
database_connection_ids: [],
@ -27,23 +27,23 @@ describe('KLO setup config helpers', () => {
it('preserves database connection ids while marking a step complete', () => {
const config = {
...buildDefaultKloProjectConfig('warehouse'),
...buildDefaultKtxProjectConfig('warehouse'),
setup: {
database_connection_ids: ['warehouse'],
completed_steps: ['databases'],
},
};
expect(markKloSetupStepComplete(config, 'project').setup).toEqual({
expect(markKtxSetupStepComplete(config, 'project').setup).toEqual({
database_connection_ids: ['warehouse'],
completed_steps: ['databases', 'project'],
});
});
it('sets setup database connection ids without duplicates', () => {
const config = buildDefaultKloProjectConfig('warehouse');
const config = buildDefaultKtxProjectConfig('warehouse');
const withDatabases = setKloSetupDatabaseConnectionIds(config, ['warehouse', 'analytics', 'warehouse']);
const withDatabases = setKtxSetupDatabaseConnectionIds(config, ['warehouse', 'analytics', 'warehouse']);
expect(withDatabases.setup).toEqual({
database_connection_ids: ['warehouse', 'analytics'],
@ -53,10 +53,10 @@ describe('KLO setup config helpers', () => {
});
it('marks databases complete only when requested', () => {
const config = markKloSetupStepComplete(buildDefaultKloProjectConfig('warehouse'), 'project');
const config = markKtxSetupStepComplete(buildDefaultKtxProjectConfig('warehouse'), 'project');
const withDatabases = setKloSetupDatabaseConnectionIds(config, ['warehouse'], { complete: true });
const withDatabasesAgain = setKloSetupDatabaseConnectionIds(withDatabases, ['warehouse'], { complete: true });
const withDatabases = setKtxSetupDatabaseConnectionIds(config, ['warehouse'], { complete: true });
const withDatabasesAgain = setKtxSetupDatabaseConnectionIds(withDatabases, ['warehouse'], { complete: true });
expect(withDatabases.setup).toEqual({
database_connection_ids: ['warehouse'],
@ -66,10 +66,10 @@ describe('KLO setup config helpers', () => {
});
it('merges setup-local gitignore entries without removing existing lines', () => {
expect(mergeKloSetupGitignoreEntries('cache/\ndb.sqlite\n')).toBe(
expect(mergeKtxSetupGitignoreEntries('cache/\ndb.sqlite\n')).toBe(
['cache/', 'db.sqlite', 'secrets/', 'setup/', 'agents/', ''].join('\n'),
);
expect(mergeKloSetupGitignoreEntries('cache/\nsecrets/\n')).toBe(
expect(mergeKtxSetupGitignoreEntries('cache/\nsecrets/\n')).toBe(
['cache/', 'secrets/', 'setup/', 'agents/', ''].join('\n'),
);
});

View file

@ -1,12 +1,12 @@
import type { KloProjectConfig } from './config.js';
import type { KtxProjectConfig } from './config.js';
export const KLO_SETUP_STEPS = ['project', 'llm', 'embeddings', 'databases', 'sources', 'context', 'agents'] as const;
export const KTX_SETUP_STEPS = ['project', 'llm', 'embeddings', 'databases', 'sources', 'context', 'agents'] as const;
export type KloSetupStep = (typeof KLO_SETUP_STEPS)[number];
export type KtxSetupStep = (typeof KTX_SETUP_STEPS)[number];
const SETUP_GITIGNORE_ENTRIES = ['secrets/', 'setup/', 'agents/'] as const;
export function markKloSetupStepComplete(config: KloProjectConfig, step: KloSetupStep): KloProjectConfig {
export function markKtxSetupStepComplete(config: KtxProjectConfig, step: KtxSetupStep): KtxProjectConfig {
const databaseConnectionIds = config.setup?.database_connection_ids ?? [];
const completedSteps = config.setup?.completed_steps ?? [];
return {
@ -18,11 +18,11 @@ export function markKloSetupStepComplete(config: KloProjectConfig, step: KloSetu
};
}
export function setKloSetupDatabaseConnectionIds(
config: KloProjectConfig,
export function setKtxSetupDatabaseConnectionIds(
config: KtxProjectConfig,
connectionIds: string[],
options: { complete?: boolean } = {},
): KloProjectConfig {
): KtxProjectConfig {
const uniqueConnectionIds = [...new Set(connectionIds.filter((connectionId) => connectionId.trim().length > 0))];
const completedSteps = config.setup?.completed_steps ?? [];
const nextCompletedSteps =
@ -39,7 +39,7 @@ export function setKloSetupDatabaseConnectionIds(
};
}
export function mergeKloSetupGitignoreEntries(content: string): string {
export function mergeKtxSetupGitignoreEntries(content: string): string {
const lines = content
.split(/\r?\n/)
.map((line) => line.trimEnd())