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

@ -2,33 +2,33 @@ import { mkdir, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { resolveKloConfigReference, resolveKloHomePath } from './config-reference.js';
import { resolveKtxConfigReference, resolveKtxHomePath } from './config-reference.js';
describe('KLO config references', () => {
describe('KTX config references', () => {
it('resolves env references without returning empty values', () => {
expect(resolveKloConfigReference('env:AI_GATEWAY_API_KEY', { AI_GATEWAY_API_KEY: ' gateway-key ' })).toBe(
expect(resolveKtxConfigReference('env:AI_GATEWAY_API_KEY', { AI_GATEWAY_API_KEY: ' gateway-key ' })).toBe(
'gateway-key',
);
expect(resolveKloConfigReference('env:AI_GATEWAY_API_KEY', { AI_GATEWAY_API_KEY: ' ' })).toBeUndefined();
expect(resolveKloConfigReference('env:AI_GATEWAY_API_KEY', {})).toBeUndefined();
expect(resolveKtxConfigReference('env:AI_GATEWAY_API_KEY', { AI_GATEWAY_API_KEY: ' ' })).toBeUndefined();
expect(resolveKtxConfigReference('env:AI_GATEWAY_API_KEY', {})).toBeUndefined();
});
it('resolves file references and trims file content', async () => {
const dir = join(tmpdir(), `klo-config-reference-${process.pid}`);
const dir = join(tmpdir(), `ktx-config-reference-${process.pid}`);
await mkdir(dir, { recursive: true });
const keyPath = join(dir, 'gateway-key.txt');
await writeFile(keyPath, 'file-gateway-key\n', 'utf8');
expect(resolveKloConfigReference(`file:${keyPath}`, {})).toBe('file-gateway-key');
expect(resolveKtxConfigReference(`file:${keyPath}`, {})).toBe('file-gateway-key');
});
it('returns literal values unchanged after trimming blank-only values', () => {
expect(resolveKloConfigReference('provider/model', {})).toBe('provider/model');
expect(resolveKloConfigReference(' ', {})).toBeUndefined();
expect(resolveKloConfigReference(undefined, {})).toBeUndefined();
expect(resolveKtxConfigReference('provider/model', {})).toBe('provider/model');
expect(resolveKtxConfigReference(' ', {})).toBeUndefined();
expect(resolveKtxConfigReference(undefined, {})).toBeUndefined();
});
it('resolves home-prefixed paths', () => {
expect(resolveKloHomePath('~/klo/key.txt')).toContain('/klo/key.txt');
expect(resolveKtxHomePath('~/ktx/key.txt')).toContain('/ktx/key.txt');
});
});

View file

@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { resolve } from 'node:path';
export function resolveKloHomePath(path: string): string {
export function resolveKtxHomePath(path: string): string {
if (path === '~') {
return homedir();
}
@ -14,7 +14,7 @@ export function resolveKloHomePath(path: string): string {
return resolve(path);
}
export function resolveKloConfigReference(value: string | undefined, env: NodeJS.ProcessEnv): string | undefined {
export function resolveKtxConfigReference(value: string | undefined, env: NodeJS.ProcessEnv): string | undefined {
if (!value) {
return undefined;
}
@ -26,7 +26,7 @@ export function resolveKloConfigReference(value: string | undefined, env: NodeJS
}
if (value.startsWith('file:')) {
const filePath = resolveKloHomePath(value.slice('file:'.length).trim());
const filePath = resolveKtxHomePath(value.slice('file:'.length).trim());
const fileValue = readFileSync(filePath, 'utf8').trim();
return fileValue.length > 0 ? fileValue : undefined;
}

View file

@ -1,10 +1,10 @@
export interface KloStorageConfig {
export interface KtxStorageConfig {
configDir?: string;
homeDir?: string;
worktreesDir?: string;
}
export interface KloGitConfig {
export interface KtxGitConfig {
userName: string;
userEmail: string;
bootstrapMessage?: string;
@ -12,31 +12,31 @@ export interface KloGitConfig {
bootstrapAuthorEmail?: string;
}
export interface KloCoreConfig {
storage: KloStorageConfig;
git: KloGitConfig;
export interface KtxCoreConfig {
storage: KtxStorageConfig;
git: KtxGitConfig;
}
export interface KloLogger {
export interface KtxLogger {
debug(message: string): void;
log(message: string): void;
warn(message: string): void;
error(message: string, error?: unknown): void;
}
export const noopLogger: KloLogger = {
export const noopLogger: KtxLogger = {
debug: () => undefined,
log: () => undefined,
warn: () => undefined,
error: () => undefined,
};
export function resolveConfigDir(config: KloCoreConfig): string {
export function resolveConfigDir(config: KtxCoreConfig): string {
const homeDir = config.storage.homeDir ?? '/tmp';
return config.storage.configDir ?? `${homeDir}/klo/config`;
return config.storage.configDir ?? `${homeDir}/ktx/config`;
}
export function resolveWorktreesDir(config: KloCoreConfig): string {
export function resolveWorktreesDir(config: KtxCoreConfig): string {
const homeDir = config.storage.homeDir ?? '/tmp';
return config.storage.worktreesDir ?? `${homeDir}/.worktrees`;
}

View file

@ -1,4 +1,4 @@
export interface KloEmbeddingPort {
export interface KtxEmbeddingPort {
maxBatchSize: number;
computeEmbedding(text: string): Promise<number[]>;
computeEmbeddingsBulk(texts: string[]): Promise<number[][]>;

View file

@ -1,18 +1,18 @@
export interface KloFileWriteResult {
export interface KtxFileWriteResult {
commitHash?: string | null;
[key: string]: unknown;
}
export interface KloFileReadResult {
export interface KtxFileReadResult {
content: string;
[key: string]: unknown;
}
export interface KloFileListResult {
export interface KtxFileListResult {
files: string[];
}
export interface KloFileHistoryEntry {
export interface KtxFileHistoryEntry {
sha?: string;
message?: string;
author?: string;
@ -20,7 +20,7 @@ export interface KloFileHistoryEntry {
[key: string]: unknown;
}
export interface KloFileStorePort<TSelf = unknown> {
export interface KtxFileStorePort<TSelf = unknown> {
writeFile(
path: string,
content: string,
@ -28,16 +28,16 @@ export interface KloFileStorePort<TSelf = unknown> {
authorEmail: string,
commitMessage: string,
options?: { skipLock?: boolean },
): Promise<KloFileWriteResult>;
readFile(path: string): Promise<KloFileReadResult>;
): Promise<KtxFileWriteResult>;
readFile(path: string): Promise<KtxFileReadResult>;
deleteFile(
path: string,
author: string,
authorEmail: string,
commitMessage: string,
options?: { skipLock?: boolean },
): Promise<KloFileWriteResult | null>;
listFiles(path: string, recursive?: boolean): Promise<KloFileListResult>;
getFileHistory(path: string): Promise<KloFileHistoryEntry[] | unknown>;
): Promise<KtxFileWriteResult | null>;
listFiles(path: string, recursive?: boolean): Promise<KtxFileListResult>;
getFileHistory(path: string): Promise<KtxFileHistoryEntry[] | unknown>;
forWorktree(workdir: string): TSelf;
}

View file

@ -3,7 +3,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { SimpleGit } from 'simple-git';
import type { KloCoreConfig } from './config.js';
import type { KtxCoreConfig } from './config.js';
import { createSimpleGit } from './git-env.js';
import { GitService } from './git.service.js';
@ -21,7 +21,7 @@ describe('GitService.assertWorktreeClean', () => {
await writeFile(join(workdir, 'init'), 'init');
await git.add('.');
await git.commit('init');
const coreConfig: KloCoreConfig = {
const coreConfig: KtxCoreConfig = {
storage: { configDir: workdir, homeDir: workdir },
git: { userName: 'Test', userEmail: 't@test' },
};

View file

@ -3,7 +3,7 @@ import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { SimpleGit } from 'simple-git';
import type { KloCoreConfig } from './config.js';
import type { KtxCoreConfig } from './config.js';
import { createSimpleGit } from './git-env.js';
import { GitService } from './git.service.js';
@ -22,7 +22,7 @@ describe('GitService.deleteDirectories', () => {
await git.add('.');
await git.commit('init');
const coreConfig: KloCoreConfig = {
const coreConfig: KtxCoreConfig = {
storage: { configDir: workdir, homeDir: workdir },
git: { userName: 'Test', userEmail: 't@test' },
};

View file

@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { SimpleGit } from 'simple-git';
import type { KloCoreConfig } from './config.js';
import type { KtxCoreConfig } from './config.js';
import { createSimpleGit } from './git-env.js';
import { GitService } from './git.service.js';
@ -21,7 +21,7 @@ describe('GitService.resetHardTo', () => {
await writeFile(join(workdir, 'init'), 'init');
await git.add('.');
await git.commit('init');
const coreConfig: KloCoreConfig = {
const coreConfig: KtxCoreConfig = {
storage: { configDir: workdir, homeDir: workdir },
git: { userName: 'Test', userEmail: 't@test' },
};

View file

@ -2,7 +2,7 @@ import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { KloCoreConfig } from './config.js';
import type { KtxCoreConfig } from './config.js';
import { GitService } from './git.service.js';
// These tests drive a real git repo inside a temp directory — simple-git shells out to the
@ -15,7 +15,7 @@ describe('GitService', () => {
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'git-service-spec-'));
const coreConfig: KloCoreConfig = {
const coreConfig: KtxCoreConfig = {
storage: { configDir: tempDir, homeDir: tempDir },
git: {
userName: 'Test User',

View file

@ -1,7 +1,7 @@
import { promises as fs } from 'node:fs';
import { join } from 'node:path';
import type { SimpleGit } from 'simple-git';
import { noopLogger, resolveConfigDir, type KloCoreConfig, type KloLogger } from './config.js';
import { noopLogger, resolveConfigDir, type KtxCoreConfig, type KtxLogger } from './config.js';
import { createSimpleGit } from './git-env.js';
export interface GitCommitInfo {
@ -32,13 +32,13 @@ export type SquashMergeResult =
| { ok: false; conflict: true; conflictPaths: string[] };
export class GitService {
private readonly logger: KloLogger;
private readonly logger: KtxLogger;
private git!: SimpleGit;
private configDir: string;
constructor(
private readonly config: KloCoreConfig,
logger?: KloLogger,
private readonly config: KtxCoreConfig,
logger?: KtxLogger,
) {
this.logger = logger ?? noopLogger;
this.configDir = resolveConfigDir(config);
@ -73,10 +73,10 @@ export class GitService {
// can rely on `revParseHead()` returning a SHA. Idempotent: skip if HEAD already exists.
const head = await this.revParseHead();
if (!head) {
await this.git.commit(this.config.git.bootstrapMessage ?? 'Initialize klo project repository', {
await this.git.commit(this.config.git.bootstrapMessage ?? 'Initialize ktx project repository', {
'--allow-empty': null,
'--author': `${this.config.git.bootstrapAuthor ?? 'klo system'} <${
this.config.git.bootstrapAuthorEmail ?? 'system@klo.local'
'--author': `${this.config.git.bootstrapAuthor ?? 'ktx system'} <${
this.config.git.bootstrapAuthorEmail ?? 'system@ktx.local'
}>`,
});
this.logger.log('Wrote bootstrap commit to config repo');
@ -676,7 +676,7 @@ export class GitService {
/**
* Remove the worktree entry and its on-disk directory. Uses `--force` because session
* worktrees are klo-internal a clean working tree is not required.
* worktrees are ktx-internal a clean working tree is not required.
*/
async removeWorktree(path: string): Promise<void> {
try {

View file

@ -1,19 +1,19 @@
export type { KloCoreConfig, KloGitConfig, KloLogger, KloStorageConfig } from './config.js';
export type { KtxCoreConfig, KtxGitConfig, KtxLogger, KtxStorageConfig } from './config.js';
export { noopLogger, resolveConfigDir, resolveWorktreesDir } from './config.js';
export { resolveKloConfigReference, resolveKloHomePath } from './config-reference.js';
export type { KloEmbeddingPort } from './embedding.js';
export { resolveKtxConfigReference, resolveKtxHomePath } from './config-reference.js';
export type { KtxEmbeddingPort } from './embedding.js';
export {
REDACTED_KLO_CREDENTIAL_VALUE,
redactKloSensitiveMetadata,
redactKloSensitiveText,
redactKloSensitiveValue,
REDACTED_KTX_CREDENTIAL_VALUE,
redactKtxSensitiveMetadata,
redactKtxSensitiveText,
redactKtxSensitiveValue,
} from './redaction.js';
export type {
KloFileHistoryEntry,
KloFileListResult,
KloFileReadResult,
KloFileStorePort,
KloFileWriteResult,
KtxFileHistoryEntry,
KtxFileListResult,
KtxFileReadResult,
KtxFileStorePort,
KtxFileWriteResult,
} from './file-store.js';
export type { GitCommitInfo, SquashMergeResult, WorktreeEntry } from './git.service.js';
export { GitService } from './git.service.js';

View file

@ -1,4 +1,4 @@
export const REDACTED_KLO_CREDENTIAL_VALUE = '<redacted>';
export const REDACTED_KTX_CREDENTIAL_VALUE = '<redacted>';
const SENSITIVE_FIELD_NAME = /(password|secret|token|api[_-]?key|private[_-]?key|passphrase|credential|authorization|url)/i;
const URL_CREDENTIAL_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)([^@\s/]+)(@)/gi;
@ -11,37 +11,37 @@ function isSensitiveField(key: string): boolean {
return SENSITIVE_FIELD_NAME.test(key);
}
export function redactKloSensitiveValue(key: string, value: unknown): unknown {
export function redactKtxSensitiveValue(key: string, value: unknown): unknown {
if (isSensitiveField(key)) {
return REDACTED_KLO_CREDENTIAL_VALUE;
return REDACTED_KTX_CREDENTIAL_VALUE;
}
if (Array.isArray(value)) {
return value.map((item) => redactKloSensitiveValue(key, item));
return value.map((item) => redactKtxSensitiveValue(key, item));
}
if (isRecord(value)) {
return redactKloSensitiveMetadata(value);
return redactKtxSensitiveMetadata(value);
}
return value;
}
export function redactKloSensitiveMetadata(metadata: Record<string, unknown>): Record<string, unknown> {
export function redactKtxSensitiveMetadata(metadata: Record<string, unknown>): Record<string, unknown> {
const redacted: Record<string, unknown> = {};
for (const [key, value] of Object.entries(metadata)) {
if (Array.isArray(value)) {
redacted[key] = value.map((item) =>
isRecord(item) ? redactKloSensitiveMetadata(item) : redactKloSensitiveValue(key, item),
isRecord(item) ? redactKtxSensitiveMetadata(item) : redactKtxSensitiveValue(key, item),
);
continue;
}
if (isRecord(value)) {
redacted[key] = redactKloSensitiveValue(key, value);
redacted[key] = redactKtxSensitiveValue(key, value);
continue;
}
redacted[key] = redactKloSensitiveValue(key, value);
redacted[key] = redactKtxSensitiveValue(key, value);
}
return redacted;
}
export function redactKloSensitiveText(value: string): string {
return value.replace(URL_CREDENTIAL_PATTERN, `$1${REDACTED_KLO_CREDENTIAL_VALUE}$3`);
export function redactKtxSensitiveText(value: string): string {
return value.replace(URL_CREDENTIAL_PATTERN, `$1${REDACTED_KTX_CREDENTIAL_VALUE}$3`);
}

View file

@ -2,7 +2,7 @@ import { mkdtemp, realpath, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { KloCoreConfig } from './config.js';
import type { KtxCoreConfig } from './config.js';
import { GitService } from './git.service.js';
import { SessionWorktreeService, type WorktreeConfigPort } from './session-worktree.service.js';
@ -20,7 +20,7 @@ describe('SessionWorktreeService', () => {
homeDir = await mkdtemp(join(tmpdir(), 'sws-spec-'));
homeDir = await realpath(homeDir);
const coreConfig: KloCoreConfig = {
const coreConfig: KtxCoreConfig = {
storage: { configDir: homeDir, homeDir },
git: {
userName: 'System User',
@ -113,7 +113,7 @@ describe('SessionWorktreeService', () => {
await expect(stat(session.workdir)).resolves.toBeTruthy();
const { readFile } = await import('node:fs/promises');
const raw = await readFile(join(session.workdir, '.klo-outcome'), 'utf-8');
const raw = await readFile(join(session.workdir, '.ktx-outcome'), 'utf-8');
const parsed = JSON.parse(raw);
expect(parsed.outcome).toBe('conflict');
expect(parsed.chatId).toBe('chat-cleanup-conflict');

View file

@ -1,6 +1,6 @@
import { mkdir, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { noopLogger, resolveWorktreesDir, type KloCoreConfig, type KloLogger } from './config.js';
import { noopLogger, resolveWorktreesDir, type KtxCoreConfig, type KtxLogger } from './config.js';
import { GitService } from './git.service.js';
export type SessionOutcome = 'success' | 'empty' | 'conflict' | 'crash';
@ -28,14 +28,14 @@ export interface SessionWorktree<TConfig> {
}
export interface SessionWorktreeServiceDeps<TConfig extends WorktreeConfigPort<TConfig>> {
coreConfig: KloCoreConfig;
coreConfig: KtxCoreConfig;
gitService: GitService;
configService: TConfig;
logger?: KloLogger;
logger?: KtxLogger;
}
export class SessionWorktreeService<TConfig extends WorktreeConfigPort<TConfig> = WorktreeConfigPort<never>> {
private readonly logger: KloLogger;
private readonly logger: KtxLogger;
private readonly worktreesRoot: string;
constructor(private readonly deps: SessionWorktreeServiceDeps<TConfig>) {
@ -101,7 +101,7 @@ export class SessionWorktreeService<TConfig extends WorktreeConfigPort<TConfig>
...(extra?.conflictPaths ? { conflictPaths: extra.conflictPaths } : {}),
};
try {
await writeFile(join(session.workdir, '.klo-outcome'), JSON.stringify(payload, null, 2), 'utf-8');
await writeFile(join(session.workdir, '.ktx-outcome'), JSON.stringify(payload, null, 2), 'utf-8');
} catch (error) {
this.logger.warn(
`cleanup(${outcome}) failed to write sentinel for ${session.chatId}: ${