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

@ -3,8 +3,8 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
KLO_AGENT_MAX_ROWS_CAP,
createKloAgentRuntime,
KTX_AGENT_MAX_ROWS_CAP,
createKtxAgentRuntime,
parseAgentMaxRows,
readAgentJsonFile,
writeAgentJson,
@ -28,7 +28,7 @@ describe('agent runtime helpers', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-agent-runtime-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-runtime-'));
});
afterEach(async () => {
@ -69,13 +69,13 @@ describe('agent runtime helpers', () => {
expect(parseAgentMaxRows(100)).toBe(100);
expect(() => parseAgentMaxRows(undefined)).toThrow('maxRows is required');
expect(() => parseAgentMaxRows(0)).toThrow('positive integer');
expect(() => parseAgentMaxRows(KLO_AGENT_MAX_ROWS_CAP + 1)).toThrow(String(KLO_AGENT_MAX_ROWS_CAP));
expect(() => parseAgentMaxRows(KTX_AGENT_MAX_ROWS_CAP + 1)).toThrow(String(KTX_AGENT_MAX_ROWS_CAP));
});
it('constructs local context ports with semantic compute and query executor', async () => {
const project = {
projectDir: tempDir,
configPath: join(tempDir, 'klo.yaml'),
configPath: join(tempDir, 'ktx.yaml'),
config: { project: 'revenue', connections: {} },
coreConfig: {},
git: {},
@ -88,7 +88,7 @@ describe('agent runtime helpers', () => {
const createContextTools = vi.fn(() => ports);
await expect(
createKloAgentRuntime(
createKtxAgentRuntime(
{ projectDir: tempDir, enableSemanticCompute: true, enableQueryExecution: true },
{
loadProject,

View file

@ -1,38 +1,38 @@
import { readFile } from 'node:fs/promises';
import { createDefaultLocalQueryExecutor, type KloSqlQueryExecutorPort } from '@klo/context/connections';
import { createPythonSemanticLayerComputePort, type KloSemanticLayerComputePort } from '@klo/context/daemon';
import { createLocalProjectMcpContextPorts, type KloMcpContextPorts } from '@klo/context/mcp';
import { type KloLocalProject, loadKloProject } from '@klo/context/project';
import type { KloCliIo } from './cli-runtime.js';
import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
import { createPythonSemanticLayerComputePort, type KtxSemanticLayerComputePort } from '@ktx/context/daemon';
import { createLocalProjectMcpContextPorts, type KtxMcpContextPorts } from '@ktx/context/mcp';
import { type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
import type { KtxCliIo } from './cli-runtime.js';
export const KLO_AGENT_MAX_ROWS_CAP = 1000;
export const KTX_AGENT_MAX_ROWS_CAP = 1000;
export interface KloAgentRuntimeOptions {
export interface KtxAgentRuntimeOptions {
projectDir: string;
enableSemanticCompute: boolean;
enableQueryExecution: boolean;
}
export interface KloAgentRuntime {
project: KloLocalProject;
ports: KloMcpContextPorts;
semanticLayerCompute?: KloSemanticLayerComputePort;
queryExecutor?: KloSqlQueryExecutorPort;
export interface KtxAgentRuntime {
project: KtxLocalProject;
ports: KtxMcpContextPorts;
semanticLayerCompute?: KtxSemanticLayerComputePort;
queryExecutor?: KtxSqlQueryExecutorPort;
}
export interface KloAgentRuntimeDeps {
loadProject?: typeof loadKloProject;
export interface KtxAgentRuntimeDeps {
loadProject?: typeof loadKtxProject;
createContextTools?: typeof createLocalProjectMcpContextPorts;
createSemanticLayerCompute?: () => KloSemanticLayerComputePort;
createQueryExecutor?: () => KloSqlQueryExecutorPort;
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
}
export function writeAgentJson(io: KloCliIo, value: unknown): void {
export function writeAgentJson(io: KtxCliIo, value: unknown): void {
io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
}
export function writeAgentJsonError(
io: KloCliIo,
io: KtxCliIo,
message: string,
detail: Record<string, unknown> = {},
): void {
@ -51,17 +51,17 @@ export function parseAgentMaxRows(value: number | undefined): number {
if (!Number.isInteger(value) || value === undefined || value <= 0) {
throw new Error('maxRows is required and must be a positive integer.');
}
if (value > KLO_AGENT_MAX_ROWS_CAP) {
throw new Error(`maxRows must be less than or equal to ${KLO_AGENT_MAX_ROWS_CAP}.`);
if (value > KTX_AGENT_MAX_ROWS_CAP) {
throw new Error(`maxRows must be less than or equal to ${KTX_AGENT_MAX_ROWS_CAP}.`);
}
return value;
}
export async function createKloAgentRuntime(
options: KloAgentRuntimeOptions,
deps: KloAgentRuntimeDeps = {},
): Promise<KloAgentRuntime> {
const project = await (deps.loadProject ?? loadKloProject)({ projectDir: options.projectDir });
export async function createKtxAgentRuntime(
options: KtxAgentRuntimeOptions,
deps: KtxAgentRuntimeDeps = {},
): Promise<KtxAgentRuntime> {
const project = await (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
const semanticLayerCompute = options.enableSemanticCompute
? (deps.createSemanticLayerCompute ?? createPythonSemanticLayerComputePort)()
: undefined;

View file

@ -9,40 +9,40 @@ import {
describe('agent semantic-layer search readiness guidance', () => {
it('formats missing project guidance with exact recovery commands', () => {
expect(missingProjectSlSearchReadiness('/tmp/klo-search', 'gross revenue')).toEqual({
expect(missingProjectSlSearchReadiness('/tmp/ktx-search', 'gross revenue')).toEqual({
code: 'agent_sl_search_missing_project',
message: 'Semantic-layer search needs an initialized KLO project at /tmp/klo-search.',
message: 'Semantic-layer search needs an initialized KTX project at /tmp/ktx-search.',
nextSteps: [
'klo demo',
'klo setup --project-dir /tmp/klo-search',
'klo ingest <connection>',
'klo agent sl list --json --query "gross revenue" --project-dir /tmp/klo-search',
'ktx demo',
'ktx setup --project-dir /tmp/ktx-search',
'ktx ingest <connection>',
'ktx agent sl list --json --query "gross revenue" --project-dir /tmp/ktx-search',
],
});
});
it('formats no-connection and no-index guidance without hiding the project path', () => {
expect(noConnectionsSlSearchReadiness('/tmp/klo-search', 'revenue')).toMatchObject({
expect(noConnectionsSlSearchReadiness('/tmp/ktx-search', 'revenue')).toMatchObject({
code: 'agent_sl_search_no_connections',
message: 'Semantic-layer search found no configured connections in /tmp/klo-search.',
message: 'Semantic-layer search found no configured connections in /tmp/ktx-search.',
});
expect(noIndexedSourcesSlSearchReadiness('/tmp/klo-search', 'orders')).toMatchObject({
expect(noIndexedSourcesSlSearchReadiness('/tmp/ktx-search', 'orders')).toMatchObject({
code: 'agent_sl_search_no_indexed_sources',
message: 'Semantic-layer search found no indexed semantic-layer sources in /tmp/klo-search.',
message: 'Semantic-layer search found no indexed semantic-layer sources in /tmp/ktx-search.',
});
});
it('formats unknown connection guidance', () => {
expect(missingConnectionSlSearchReadiness('/tmp/klo-search', 'warehouse', 'revenue')).toMatchObject({
expect(missingConnectionSlSearchReadiness('/tmp/ktx-search', 'warehouse', 'revenue')).toMatchObject({
code: 'agent_sl_search_unknown_connection',
message: 'Semantic-layer search connection "warehouse" is not configured in /tmp/klo-search.',
message: 'Semantic-layer search connection "warehouse" is not configured in /tmp/ktx-search.',
});
});
it('detects missing klo.yaml read errors', () => {
it('detects missing ktx.yaml read errors', () => {
const error = Object.assign(new Error('ENOENT: no such file or directory'), {
code: 'ENOENT',
path: '/tmp/klo-search/klo.yaml',
path: '/tmp/ktx-search/ktx.yaml',
});
expect(isMissingProjectConfigError(error)).toBe(true);

View file

@ -1,11 +1,11 @@
export type KloAgentSlSearchReadinessCode =
export type KtxAgentSlSearchReadinessCode =
| 'agent_sl_search_missing_project'
| 'agent_sl_search_no_connections'
| 'agent_sl_search_unknown_connection'
| 'agent_sl_search_no_indexed_sources';
export interface KloAgentSlSearchReadinessDetail {
code: KloAgentSlSearchReadinessCode;
export interface KtxAgentSlSearchReadinessDetail {
code: KtxAgentSlSearchReadinessCode;
message: string;
nextSteps: string[];
}
@ -16,14 +16,14 @@ function queryForCommand(query: string | undefined): string {
}
function projectSearchCommand(projectDir: string, query: string | undefined): string {
return `klo agent sl list --json --query ${JSON.stringify(queryForCommand(query))} --project-dir ${projectDir}`;
return `ktx agent sl list --json --query ${JSON.stringify(queryForCommand(query))} --project-dir ${projectDir}`;
}
function baseNextSteps(projectDir: string, query: string | undefined): string[] {
return [
'klo demo',
`klo setup --project-dir ${projectDir}`,
'klo ingest <connection>',
'ktx demo',
`ktx setup --project-dir ${projectDir}`,
'ktx ingest <connection>',
projectSearchCommand(projectDir, query),
];
}
@ -31,10 +31,10 @@ function baseNextSteps(projectDir: string, query: string | undefined): string[]
export function missingProjectSlSearchReadiness(
projectDir: string,
query: string | undefined,
): KloAgentSlSearchReadinessDetail {
): KtxAgentSlSearchReadinessDetail {
return {
code: 'agent_sl_search_missing_project',
message: `Semantic-layer search needs an initialized KLO project at ${projectDir}.`,
message: `Semantic-layer search needs an initialized KTX project at ${projectDir}.`,
nextSteps: baseNextSteps(projectDir, query),
};
}
@ -42,7 +42,7 @@ export function missingProjectSlSearchReadiness(
export function noConnectionsSlSearchReadiness(
projectDir: string,
query: string | undefined,
): KloAgentSlSearchReadinessDetail {
): KtxAgentSlSearchReadinessDetail {
return {
code: 'agent_sl_search_no_connections',
message: `Semantic-layer search found no configured connections in ${projectDir}.`,
@ -54,7 +54,7 @@ export function missingConnectionSlSearchReadiness(
projectDir: string,
connectionId: string,
query: string | undefined,
): KloAgentSlSearchReadinessDetail {
): KtxAgentSlSearchReadinessDetail {
return {
code: 'agent_sl_search_unknown_connection',
message: `Semantic-layer search connection "${connectionId}" is not configured in ${projectDir}.`,
@ -65,7 +65,7 @@ export function missingConnectionSlSearchReadiness(
export function noIndexedSourcesSlSearchReadiness(
projectDir: string,
query: string | undefined,
): KloAgentSlSearchReadinessDetail {
): KtxAgentSlSearchReadinessDetail {
return {
code: 'agent_sl_search_no_indexed_sources',
message: `Semantic-layer search found no indexed semantic-layer sources in ${projectDir}.`,
@ -90,5 +90,5 @@ function errorPath(error: unknown): string | undefined {
}
export function isMissingProjectConfigError(error: unknown): boolean {
return errorCode(error) === 'ENOENT' && (errorPath(error)?.endsWith('klo.yaml') ?? false);
return errorCode(error) === 'ENOENT' && (errorPath(error)?.endsWith('ktx.yaml') ?? false);
}

View file

@ -1,10 +1,10 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { buildDefaultKloProjectConfig } from '@klo/context/project';
import { buildDefaultKtxProjectConfig } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloAgent } from './agent.js';
import type { KloAgentRuntime } from './agent-runtime.js';
import { runKtxAgent } from './agent.js';
import type { KtxAgentRuntime } from './agent-runtime.js';
function makeIo() {
let stdout = '';
@ -19,21 +19,21 @@ function makeIo() {
};
}
function runtime(overrides: Record<string, unknown> = {}): KloAgentRuntime {
const config = buildDefaultKloProjectConfig('revenue');
function runtime(overrides: Record<string, unknown> = {}): KtxAgentRuntime {
const config = buildDefaultKtxProjectConfig('revenue');
return {
project: {
projectDir: '/tmp/revenue',
configPath: '/tmp/revenue/klo.yaml',
configPath: '/tmp/revenue/ktx.yaml',
config: {
...config,
connections: {
warehouse: { driver: 'sqlite', path: 'warehouse.sqlite', readonly: true as const },
},
},
coreConfig: {} as KloAgentRuntime['project']['coreConfig'],
git: {} as KloAgentRuntime['project']['git'],
fileStore: {} as KloAgentRuntime['project']['fileStore'],
coreConfig: {} as KtxAgentRuntime['project']['coreConfig'],
git: {} as KtxAgentRuntime['project']['git'],
fileStore: {} as KtxAgentRuntime['project']['fileStore'],
},
ports: {
connections: { list: vi.fn(async () => [{ id: 'warehouse', name: 'warehouse', connectionType: 'sqlite' }]) },
@ -86,7 +86,7 @@ function runtime(overrides: Record<string, unknown> = {}): KloAgentRuntime {
};
}
function runtimeWithoutConnections(): KloAgentRuntime {
function runtimeWithoutConnections(): KtxAgentRuntime {
const base = runtime();
return {
...base,
@ -107,11 +107,11 @@ function runtimeWithoutConnections(): KloAgentRuntime {
};
}
describe('runKloAgent', () => {
describe('runKtxAgent', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-agent-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-'));
});
afterEach(async () => {
@ -121,7 +121,7 @@ describe('runKloAgent', () => {
it('prints tool discovery with every stable command', async () => {
const io = makeIo();
await expect(runKloAgent({ command: 'tools', projectDir: tempDir, json: true }, io.io)).resolves.toBe(0);
await expect(runKtxAgent({ command: 'tools', projectDir: tempDir, json: true }, io.io)).resolves.toBe(0);
const body = JSON.parse(io.stdout());
expect(body.projectDir).toBe(tempDir);
@ -143,7 +143,7 @@ describe('runKloAgent', () => {
const readSetupStatus = vi.fn(async () => ({ project: { path: tempDir, ready: true }, agents: [] }));
await expect(
runKloAgent({ command: 'context', projectDir: tempDir, json: true }, io.io, { createRuntime, readSetupStatus }),
runKtxAgent({ command: 'context', projectDir: tempDir, json: true }, io.io, { createRuntime, readSetupStatus }),
).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toMatchObject({
@ -168,7 +168,7 @@ describe('runKloAgent', () => {
{ command: 'wiki-read' as const, projectDir: tempDir, json: true as const, pageId: 'page-1' },
]) {
const io = makeIo();
await expect(runKloAgent(args, io.io, { createRuntime: async () => runtime() })).resolves.toBe(0);
await expect(runKtxAgent(args, io.io, { createRuntime: async () => runtime() })).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toBeTruthy();
expect(io.stderr()).toBe('');
}
@ -199,7 +199,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent({ command: 'wiki-search', projectDir: tempDir, json: true, query: 'paid order', limit: 5 }, io.io, {
runKtxAgent({ command: 'wiki-search', projectDir: tempDir, json: true, query: 'paid order', limit: 5 }, io.io, {
createRuntime: async () => fakeRuntime,
}),
).resolves.toBe(0);
@ -222,7 +222,7 @@ describe('runKloAgent', () => {
await writeFile(queryFile, '{"measures":["total_revenue"],"dimensions":[]}', 'utf-8');
await expect(
runKloAgent(
runKtxAgent(
{
command: 'sl-query',
projectDir: tempDir,
@ -247,7 +247,7 @@ describe('runKloAgent', () => {
await writeFile(sqlFile, 'select 1', 'utf-8');
await expect(
runKloAgent(
runKtxAgent(
{
command: 'sql-execute',
projectDir: tempDir,
@ -274,11 +274,11 @@ describe('runKloAgent', () => {
const io = makeIo();
const missingProjectError = Object.assign(new Error('ENOENT: no such file or directory'), {
code: 'ENOENT',
path: join(tempDir, 'klo.yaml'),
path: join(tempDir, 'ktx.yaml'),
});
await expect(
runKloAgent(
runKtxAgent(
{ command: 'sl-list', projectDir: tempDir, json: true, query: 'gross revenue' },
io.io,
{ createRuntime: vi.fn(async () => Promise.reject(missingProjectError)) },
@ -289,12 +289,12 @@ describe('runKloAgent', () => {
ok: false,
error: {
code: 'agent_sl_search_missing_project',
message: `Semantic-layer search needs an initialized KLO project at ${tempDir}.`,
message: `Semantic-layer search needs an initialized KTX project at ${tempDir}.`,
nextSteps: [
'klo demo',
`klo setup --project-dir ${tempDir}`,
'klo ingest <connection>',
`klo agent sl list --json --query "gross revenue" --project-dir ${tempDir}`,
'ktx demo',
`ktx setup --project-dir ${tempDir}`,
'ktx ingest <connection>',
`ktx agent sl list --json --query "gross revenue" --project-dir ${tempDir}`,
],
},
});
@ -305,7 +305,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent(
runKtxAgent(
{ command: 'sl-list', projectDir: tempDir, json: true, query: 'revenue' },
io.io,
{ createRuntime: async () => runtimeWithoutConnections() },
@ -318,10 +318,10 @@ describe('runKloAgent', () => {
code: 'agent_sl_search_no_connections',
message: `Semantic-layer search found no configured connections in ${tempDir}.`,
nextSteps: [
'klo demo',
`klo setup --project-dir ${tempDir}`,
'klo ingest <connection>',
`klo agent sl list --json --query "revenue" --project-dir ${tempDir}`,
'ktx demo',
`ktx setup --project-dir ${tempDir}`,
'ktx ingest <connection>',
`ktx agent sl list --json --query "revenue" --project-dir ${tempDir}`,
],
},
});
@ -331,7 +331,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent(
runKtxAgent(
{ command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'missing', query: 'revenue' },
io.io,
{ createRuntime: async () => runtime() },
@ -357,7 +357,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent(
runKtxAgent(
{ command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'warehouse', query: 'revenue' },
io.io,
{ createRuntime: async () => fakeRuntime },
@ -377,7 +377,7 @@ describe('runKloAgent', () => {
const io = makeIo();
await expect(
runKloAgent({ command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'missing' }, io.io, {
runKtxAgent({ command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'missing' }, io.io, {
createRuntime: async () =>
runtime({
ports: { knowledge: { read: vi.fn(async () => null) } },

View file

@ -1,13 +1,13 @@
import { readFile } from 'node:fs/promises';
import type { KloCliIo } from './cli-runtime.js';
import type { KtxCliIo } from './cli-runtime.js';
import {
createKloAgentRuntime,
createKtxAgentRuntime,
parseAgentMaxRows,
readAgentJsonFile,
writeAgentJson,
writeAgentJsonError,
type KloAgentRuntime,
type KloAgentRuntimeDeps,
type KtxAgentRuntime,
type KtxAgentRuntimeDeps,
} from './agent-runtime.js';
import {
isMissingProjectConfigError,
@ -15,11 +15,11 @@ import {
missingProjectSlSearchReadiness,
noConnectionsSlSearchReadiness,
noIndexedSourcesSlSearchReadiness,
type KloAgentSlSearchReadinessDetail,
type KtxAgentSlSearchReadinessDetail,
} from './agent-search-readiness.js';
import { readKloSetupStatus, type KloSetupStatus } from './setup.js';
import { readKtxSetupStatus, type KtxSetupStatus } from './setup.js';
export type KloAgentArgs =
export type KtxAgentArgs =
| { command: 'tools'; projectDir: string; json: true }
| { command: 'context'; projectDir: string; json: true }
| { command: 'sl-list'; projectDir: string; json: true; connectionId?: string; query?: string }
@ -37,38 +37,38 @@ export type KloAgentArgs =
| { command: 'wiki-read'; projectDir: string; json: true; pageId: string }
| { command: 'sql-execute'; projectDir: string; json: true; connectionId: string; sqlFile: string; maxRows?: number };
export interface KloAgentDeps extends KloAgentRuntimeDeps {
export interface KtxAgentDeps extends KtxAgentRuntimeDeps {
createRuntime?: (options: {
projectDir: string;
enableSemanticCompute: boolean;
enableQueryExecution: boolean;
}) => Promise<KloAgentRuntime>;
}) => Promise<KtxAgentRuntime>;
readSetupStatus?: (
projectDir: string,
) => Promise<KloSetupStatus | { project: { path?: string; ready: boolean }; agents: unknown[] }>;
) => Promise<KtxSetupStatus | { project: { path?: string; ready: boolean }; agents: unknown[] }>;
}
const AGENT_TOOLS = [
{ name: 'context', command: 'klo agent context --json' },
{ name: 'sl.list', command: 'klo agent sl list --json [--connection-id <id>] [--query <text>]' },
{ name: 'sl.read', command: 'klo agent sl read <sourceName> --json [--connection-id <id>]' },
{ name: 'context', command: 'ktx agent context --json' },
{ name: 'sl.list', command: 'ktx agent sl list --json [--connection-id <id>] [--query <text>]' },
{ name: 'sl.read', command: 'ktx agent sl read <sourceName> --json [--connection-id <id>]' },
{
name: 'sl.query',
command: 'klo agent sl query --json --connection-id <id> --query-file <path> --execute --max-rows 100',
command: 'ktx agent sl query --json --connection-id <id> --query-file <path> --execute --max-rows 100',
},
{ name: 'wiki.search', command: 'klo agent wiki search <query> --json [--limit 10]' },
{ name: 'wiki.read', command: 'klo agent wiki read <pageId> --json' },
{ name: 'wiki.search', command: 'ktx agent wiki search <query> --json [--limit 10]' },
{ name: 'wiki.read', command: 'ktx agent wiki read <pageId> --json' },
{
name: 'sql.execute',
command: 'klo agent sql execute --json --connection-id <id> --sql-file <path> --max-rows 100',
command: 'ktx agent sql execute --json --connection-id <id> --sql-file <path> --max-rows 100',
},
] as const;
function writeAgentSlSearchReadinessError(io: KloCliIo, detail: KloAgentSlSearchReadinessDetail): void {
function writeAgentSlSearchReadinessError(io: KtxCliIo, detail: KtxAgentSlSearchReadinessDetail): void {
writeAgentJsonError(io, detail.message, { code: detail.code, nextSteps: detail.nextSteps });
}
async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAgentRuntime> {
async function runtimeFor(args: KtxAgentArgs, deps: KtxAgentDeps): Promise<KtxAgentRuntime> {
const needsSemanticCompute = args.command === 'sl-query';
const needsQueryExecution = args.command === 'sql-execute' || (args.command === 'sl-query' && args.execute);
return deps.createRuntime
@ -77,7 +77,7 @@ async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAg
enableSemanticCompute: needsSemanticCompute,
enableQueryExecution: needsQueryExecution,
})
: createKloAgentRuntime(
: createKtxAgentRuntime(
{
projectDir: args.projectDir,
enableSemanticCompute: needsSemanticCompute,
@ -87,14 +87,14 @@ async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAg
);
}
function connectionIdForSource(runtime: KloAgentRuntime, requested: string | undefined): string {
function connectionIdForSource(runtime: KtxAgentRuntime, requested: string | undefined): string {
if (requested) return requested;
const ids = Object.keys(runtime.project.config.connections ?? {});
if (ids.length === 1) return ids[0] as string;
throw new Error('Use --connection-id when the project has zero or multiple connections.');
}
export async function runKloAgent(args: KloAgentArgs, io: KloCliIo, deps: KloAgentDeps = {}): Promise<number> {
export async function runKtxAgent(args: KtxAgentArgs, io: KtxCliIo, deps: KtxAgentDeps = {}): Promise<number> {
try {
if (args.command === 'tools') {
writeAgentJson(io, { projectDir: args.projectDir, tools: AGENT_TOOLS });
@ -105,7 +105,7 @@ export async function runKloAgent(args: KloAgentArgs, io: KloCliIo, deps: KloAge
if (args.command === 'context') {
const [status, connections, semanticLayer] = await Promise.all([
(deps.readSetupStatus ?? readKloSetupStatus)(args.projectDir),
(deps.readSetupStatus ?? readKtxSetupStatus)(args.projectDir),
runtime.ports.connections?.list() ?? [],
runtime.ports.semanticLayer?.listSources({}) ?? { sources: [], totalSources: 0 },
]);

View file

@ -4,6 +4,6 @@ import { installStartupProfileReporter, profileMark, profileSpan } from './start
installStartupProfileReporter();
profileMark('bin:entry');
const { runKloCli } = await profileSpan('import ./cli-runtime.js', () => import('./cli-runtime.js'));
profileMark('bin:runKloCli');
process.exitCode = await runKloCli(process.argv.slice(2));
const { runKtxCli } = await profileSpan('import ./cli-runtime.js', () => import('./cli-runtime.js'));
profileMark('bin:runKtxCli');
process.exitCode = await runKtxCli(process.argv.slice(2));

View file

@ -1,11 +1,11 @@
import { spinner } from '@clack/prompts';
export interface KloCliSpinner {
export interface KtxCliSpinner {
start(message: string): void;
stop(message: string): void;
error(message: string): void;
}
export function createClackSpinner(): KloCliSpinner {
export function createClackSpinner(): KtxCliSpinner {
return spinner();
}

View file

@ -1,5 +1,5 @@
import { Command, InvalidArgumentError } from '@commander-js/extra-typings';
import type { KloCliDeps, KloCliIo, KloCliPackageInfo } from './cli-runtime.js';
import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js';
import { registerAgentCommands } from './commands/agent-commands.js';
import { registerConnectionCommands } from './commands/connection-commands.js';
import { registerWikiCommands } from './commands/knowledge-commands.js';
@ -9,16 +9,16 @@ import { registerSetupCommands } from './commands/setup-commands.js';
import { registerSlCommands } from './commands/sl-commands.js';
import { registerStatusCommands } from './commands/status-commands.js';
import { registerDevCommands } from './dev.js';
import { findNearestKloProjectDir, resolveKloProjectDir } from './project-resolver.js';
import { findNearestKtxProjectDir, resolveKtxProjectDir } from './project-resolver.js';
import { profileMark, profileSpan } from './startup-profile.js';
profileMark('module:cli-program');
export interface KloCliCommandContext {
io: KloCliIo;
deps: KloCliDeps;
export interface KtxCliCommandContext {
io: KtxCliIo;
deps: KtxCliDeps;
setExitCode: (code: number) => void;
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KloCliIo) => Promise<number>;
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
writeDebug?: (command: string, commandContext: CommandWithGlobalOptions) => void;
}
@ -29,13 +29,13 @@ export interface OutputModeOptions {
input?: boolean;
}
interface KloCommanderProgramOptions {
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KloCliIo) => Promise<number>;
interface KtxCommanderProgramOptions {
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
}
type CommanderExitLike = { exitCode: number; code: string; message: string };
interface KloGlobalOptionValues {
interface KtxGlobalOptionValues {
projectDir?: string;
debug?: boolean;
}
@ -104,7 +104,7 @@ export function parseNonEmptyAssignmentOption(value: string): { key: string; val
};
}
function optionsWithGlobals(command: CommandWithGlobalOptions): KloGlobalOptionValues {
function optionsWithGlobals(command: CommandWithGlobalOptions): KtxGlobalOptionValues {
const options = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
const values = options as { projectDir?: unknown; debug?: unknown };
return {
@ -114,25 +114,25 @@ function optionsWithGlobals(command: CommandWithGlobalOptions): KloGlobalOptionV
}
export function resolveCommandProjectDir(command: CommandWithGlobalOptions): string {
return resolveKloProjectDir({ explicitProjectDir: optionsWithGlobals(command).projectDir });
return resolveKtxProjectDir({ explicitProjectDir: optionsWithGlobals(command).projectDir });
}
export function resolveCommandProjectDirOverride(command: CommandWithGlobalOptions): string | undefined {
return optionsWithGlobals(command).projectDir ?? process.env.KLO_PROJECT_DIR;
return optionsWithGlobals(command).projectDir ?? process.env.KTX_PROJECT_DIR;
}
function createBaseProgram(info: KloCliPackageInfo, io: KloCliIo): Command {
function createBaseProgram(info: KtxCliPackageInfo, io: KtxCliIo): Command {
return new Command()
.name('klo')
.description('Standalone KLO developer CLI')
.option('--project-dir <path>', 'KLO project directory (default: KLO_PROJECT_DIR, nearest klo.yaml, or cwd)')
.name('ktx')
.description('Standalone KTX developer CLI')
.option('--project-dir <path>', 'KTX project directory (default: KTX_PROJECT_DIR, nearest ktx.yaml, or cwd)')
.option('--debug', 'Enable diagnostic logging to stderr')
.version(`${info.name} ${info.version}`, '-v, --version', 'Show CLI version')
.helpOption('-h, --help', 'Show this help text')
.configureHelp({ showGlobalOptions: true })
.addHelpText(
'after',
'\nAdvanced:\n klo dev Low-level diagnostics, scans, adapter commands, and mapping tools.\n',
'\nAdvanced:\n ktx dev Low-level diagnostics, scans, adapter commands, and mapping tools.\n',
)
.showHelpAfterError()
.exitOverride()
@ -143,7 +143,7 @@ function createBaseProgram(info: KloCliPackageInfo, io: KloCliIo): Command {
});
}
function writeDebug(io: KloCliIo, commandContext: CommandWithGlobalOptions, command: string): void {
function writeDebug(io: KtxCliIo, commandContext: CommandWithGlobalOptions, command: string): void {
const global = optionsWithGlobals(commandContext);
if (global.debug !== true) {
return;
@ -158,18 +158,18 @@ function formatCliError(error: unknown): string {
async function runBareInteractiveCommand(
program: Command,
io: KloCliIo,
context: KloCliCommandContext,
io: KtxCliIo,
context: KtxCliCommandContext,
): Promise<number> {
const nearestProjectDir = findNearestKloProjectDir(process.cwd());
const envProjectDir = process.env.KLO_PROJECT_DIR;
const runner = context.deps.setup ?? (await import('./setup.js')).runKloSetup;
const nearestProjectDir = findNearestKtxProjectDir(process.cwd());
const envProjectDir = process.env.KTX_PROJECT_DIR;
const runner = context.deps.setup ?? (await import('./setup.js')).runKtxSetup;
if (!nearestProjectDir && !envProjectDir) {
return await runner(
{
command: 'run',
projectDir: resolveKloProjectDir(),
projectDir: resolveKtxProjectDir(),
mode: 'auto',
agents: false,
agentScope: 'project',
@ -191,18 +191,18 @@ async function runBareInteractiveCommand(
return 0;
}
export async function runCommanderKloCli(
export async function runCommanderKtxCli(
argv: string[],
io: KloCliIo,
deps: KloCliDeps,
info: KloCliPackageInfo,
options: KloCommanderProgramOptions,
io: KtxCliIo,
deps: KtxCliDeps,
info: KtxCliPackageInfo,
options: KtxCommanderProgramOptions,
): Promise<number> {
profileMark('commander:entry');
let exitCode = 0;
const program = createBaseProgram(info, io);
profileMark('commander:base-program');
const context: KloCliCommandContext = {
const context: KtxCliCommandContext = {
io,
deps,
setExitCode: (code: number) => {

View file

@ -1,67 +1,67 @@
import type { KloConnectionMetabaseSetupArgs } from './commands/connection-metabase-setup.js';
import type { KloConnectionNotionArgs } from './commands/connection-notion.js';
import type { KloAgentArgs } from './agent.js';
import type { KloConnectionArgs } from './connection.js';
import type { KloDemoArgs } from './demo.js';
import type { KloDoctorArgs } from './doctor.js';
import type { KloIngestArgs } from './ingest.js';
import type { KloKnowledgeArgs } from './knowledge.js';
import type { KloPublicIngestArgs } from './public-ingest.js';
import type { KloScanArgs } from './scan.js';
import type { KloServeArgs } from './serve.js';
import type { KloSetupArgs } from './setup.js';
import type { KloSlArgs } from './sl.js';
import type { KtxConnectionMetabaseSetupArgs } from './commands/connection-metabase-setup.js';
import type { KtxConnectionNotionArgs } from './commands/connection-notion.js';
import type { KtxAgentArgs } from './agent.js';
import type { KtxConnectionArgs } from './connection.js';
import type { KtxDemoArgs } from './demo.js';
import type { KtxDoctorArgs } from './doctor.js';
import type { KtxIngestArgs } from './ingest.js';
import type { KtxKnowledgeArgs } from './knowledge.js';
import type { KtxPublicIngestArgs } from './public-ingest.js';
import type { KtxScanArgs } from './scan.js';
import type { KtxServeArgs } from './serve.js';
import type { KtxSetupArgs } from './setup.js';
import type { KtxSlArgs } from './sl.js';
import { profileMark, profileSpan } from './startup-profile.js';
profileMark('module:cli-runtime');
export interface KloCliPackageInfo {
name: '@klo/cli';
export interface KtxCliPackageInfo {
name: '@ktx/cli';
version: '0.0.0-private';
contextPackageName: '@klo/context';
contextPackageName: '@ktx/context';
}
export interface KloCliIo {
export interface KtxCliIo {
stdout: { isTTY?: boolean; write(chunk: string): void };
stderr: { write(chunk: string): void };
}
export interface KloCliDeps {
serveStdio?: (args: KloServeArgs) => Promise<number>;
setup?: (args: KloSetupArgs, io: KloCliIo) => Promise<number>;
agent?: (args: KloAgentArgs, io: KloCliIo) => Promise<number>;
connection?: (args: KloConnectionArgs, io: KloCliIo) => Promise<number>;
connectionNotion?: (args: KloConnectionNotionArgs, io: KloCliIo) => Promise<number>;
connectionMetabaseSetup?: (args: KloConnectionMetabaseSetupArgs, io: KloCliIo) => Promise<number>;
demo?: (args: KloDemoArgs, io: KloCliIo) => Promise<number>;
doctor?: (args: KloDoctorArgs, io: KloCliIo) => Promise<number>;
ingest?: (args: KloIngestArgs, io: KloCliIo) => Promise<number>;
publicIngest?: (args: KloPublicIngestArgs, io: KloCliIo) => Promise<number>;
scan?: (args: KloScanArgs, io: KloCliIo) => Promise<number>;
knowledge?: (args: KloKnowledgeArgs, io: KloCliIo) => Promise<number>;
sl?: (args: KloSlArgs, io: KloCliIo) => Promise<number>;
export interface KtxCliDeps {
serveStdio?: (args: KtxServeArgs) => Promise<number>;
setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise<number>;
agent?: (args: KtxAgentArgs, io: KtxCliIo) => Promise<number>;
connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise<number>;
connectionNotion?: (args: KtxConnectionNotionArgs, io: KtxCliIo) => Promise<number>;
connectionMetabaseSetup?: (args: KtxConnectionMetabaseSetupArgs, io: KtxCliIo) => Promise<number>;
demo?: (args: KtxDemoArgs, io: KtxCliIo) => Promise<number>;
doctor?: (args: KtxDoctorArgs, io: KtxCliIo) => Promise<number>;
ingest?: (args: KtxIngestArgs, io: KtxCliIo) => Promise<number>;
publicIngest?: (args: KtxPublicIngestArgs, io: KtxCliIo) => Promise<number>;
scan?: (args: KtxScanArgs, io: KtxCliIo) => Promise<number>;
knowledge?: (args: KtxKnowledgeArgs, io: KtxCliIo) => Promise<number>;
sl?: (args: KtxSlArgs, io: KtxCliIo) => Promise<number>;
}
export function getKloCliPackageInfo(): KloCliPackageInfo {
export function getKtxCliPackageInfo(): KtxCliPackageInfo {
return {
name: '@klo/cli',
name: '@ktx/cli',
version: '0.0.0-private',
contextPackageName: '@klo/context',
contextPackageName: '@ktx/context',
};
}
async function runInit(
args: { projectDir: string; projectName?: string; force: boolean },
io: KloCliIo,
io: KtxCliIo,
): Promise<number> {
const { initKloProject } = await import('@klo/context/project');
const result = await initKloProject({
const { initKtxProject } = await import('@ktx/context/project');
const result = await initKtxProject({
projectDir: args.projectDir,
projectName: args.projectName,
force: args.force,
});
io.stdout.write(`Initialized KLO project at ${result.projectDir}\n`);
io.stdout.write(`Initialized KTX project at ${result.projectDir}\n`);
io.stdout.write(`Config: ${result.configPath}\n`);
io.stdout.write(`Commit: ${result.commitHash ?? 'none'}\n`);
return 0;
@ -69,21 +69,21 @@ async function runInit(
export async function runInitForCommander(
args: { projectDir: string; projectName?: string; force: boolean },
io: KloCliIo,
io: KtxCliIo,
): Promise<number> {
return await runInit(args, io);
}
export async function runKloCli(
export async function runKtxCli(
argv = process.argv.slice(2),
io: KloCliIo = process,
deps: KloCliDeps = {},
io: KtxCliIo = process,
deps: KtxCliDeps = {},
): Promise<number> {
const info = getKloCliPackageInfo();
profileMark('runtime:runKloCli');
const { runCommanderKloCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
const info = getKtxCliPackageInfo();
profileMark('runtime:runKtxCli');
const { runCommanderKtxCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
return await runCommanderKloCli(argv, io, deps, info, {
return await runCommanderKtxCli(argv, io, deps, info, {
runInit: runInitForCommander,
});
}

View file

@ -1,10 +1,10 @@
import { Option, type Command } from '@commander-js/extra-typings';
import type { KloAgentArgs } from '../agent.js';
import type { KloCliCommandContext } from '../cli-program.js';
import type { KtxAgentArgs } from '../agent.js';
import type { KtxCliCommandContext } from '../cli-program.js';
import { parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
async function runAgent(context: KloCliCommandContext, args: KloAgentArgs): Promise<void> {
const runner = context.deps.agent ?? (await import('../agent.js')).runKloAgent;
async function runAgent(context: KtxCliCommandContext, args: KtxAgentArgs): Promise<void> {
const runner = context.deps.agent ?? (await import('../agent.js')).runKtxAgent;
context.setExitCode(await runner(args, context.io));
}
@ -12,10 +12,10 @@ function jsonOption(): Option {
return new Option('--json', 'Print JSON output').makeOptionMandatory();
}
export function registerAgentCommands(program: Command, context: KloCliCommandContext): void {
export function registerAgentCommands(program: Command, context: KtxCliCommandContext): void {
const agent = program
.command('agent', { hidden: true })
.description('Machine-readable KLO commands for coding agents')
.description('Machine-readable KTX commands for coding agents')
.showHelpAfterError();
agent.hook('preAction', (_thisCommand, actionCommand) => {
@ -24,7 +24,7 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
agent
.command('tools')
.description('Print available agent-facing KLO tools')
.description('Print available agent-facing KTX tools')
.addOption(jsonOption())
.action(async (_options, command) => {
await runAgent(context, { command: 'tools', projectDir: resolveCommandProjectDir(command), json: true });
@ -91,10 +91,10 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
},
);
const wiki = agent.command('wiki').description('KLO wiki agent commands');
const wiki = agent.command('wiki').description('KTX wiki agent commands');
wiki
.command('search')
.description('Search KLO wiki pages')
.description('Search KTX wiki pages')
.argument('<query>')
.addOption(jsonOption())
.option('--limit <number>', 'Maximum search results', parsePositiveIntegerOption, 10)
@ -109,7 +109,7 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
});
wiki
.command('read')
.description('Read one KLO wiki page')
.description('Read one KTX wiki page')
.argument('<pageId>')
.addOption(jsonOption())
.action(async (pageId: string, _options, command) => {

View file

@ -1,10 +1,10 @@
import type { CommandUnknownOpts } from '@commander-js/extra-typings';
import type { KloCliCommandContext } from '../cli-program.js';
import type { KtxCliCommandContext } from '../cli-program.js';
import { completeCommanderInput, installZshCompletion, zshCompletionScript } from '../completion.js';
export function registerCompletionCommands(
program: CommandUnknownOpts,
context: KloCliCommandContext,
context: KtxCliCommandContext,
completionRoot: CommandUnknownOpts = program,
): void {
program

View file

@ -1,7 +1,7 @@
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
import {
collectOption,
type KloCliCommandContext,
type KtxCliCommandContext,
parseBooleanStringOption,
parseNonEmptyAssignmentOption,
parseNonNegativeIntegerOption,
@ -10,9 +10,9 @@ import {
resolveCommandProjectDir,
} from '../cli-program.js';
import { connectionAddCommandSchema } from '../command-schemas.js';
import type { KloConnectionArgs } from '../connection.js';
import type { KtxConnectionArgs } from '../connection.js';
import { profileMark } from '../startup-profile.js';
import type { KloConnectionMappingArgs } from './connection-mapping.js';
import type { KtxConnectionMappingArgs } from './connection-mapping.js';
import { registerConnectionMetabaseCommands } from './connection-metabase-commands.js';
import { registerConnectionNotionCommands } from './connection-notion-commands.js';
@ -42,24 +42,24 @@ function parseMappingFieldOption(value: string): 'databaseMappings' | 'connectio
throw new InvalidArgumentError('must be databaseMappings or connectionMappings');
}
async function runConnectionArgs(context: KloCliCommandContext, args: KloConnectionArgs): Promise<void> {
const runner = context.deps.connection ?? (await import('../connection.js')).runKloConnection;
async function runConnectionArgs(context: KtxCliCommandContext, args: KtxConnectionArgs): Promise<void> {
const runner = context.deps.connection ?? (await import('../connection.js')).runKtxConnection;
context.setExitCode(await runner(args, context.io));
}
async function runMappingArgs(context: KloCliCommandContext, args: KloConnectionMappingArgs): Promise<void> {
const { runKloConnectionMapping } = await import('./connection-mapping.js');
context.setExitCode(await runKloConnectionMapping(args, context.io));
async function runMappingArgs(context: KtxCliCommandContext, args: KtxConnectionMappingArgs): Promise<void> {
const { runKtxConnectionMapping } = await import('./connection-mapping.js');
context.setExitCode(await runKtxConnectionMapping(args, context.io));
}
export function registerConnectionCommands(program: Command, context: KloCliCommandContext, commandName = 'connection'): void {
export function registerConnectionCommands(program: Command, context: KtxCliCommandContext, commandName = 'connection'): void {
const connection = program
.command(commandName)
.description('Add, list, test, and map data sources')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the nearest klo.yaml or current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the nearest ktx.yaml or current working directory.\n',
);
connection.hook('preAction', (_thisCommand, actionCommand) => {
context.writeDebug?.(commandName, actionCommand);
@ -75,7 +75,7 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
connection
.command('test')
.description('Test a configured connection')
.argument('<connectionId>', 'KLO connection id')
.argument('<connectionId>', 'KTX connection id')
.action(async (connectionId: string, _options: unknown, command) => {
await runConnectionArgs(context, {
command: 'test',
@ -88,12 +88,12 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
.command('add')
.description('Add or replace a configured connection')
.argument('<driver>', 'Connection driver')
.argument('<connectionId>', 'KLO connection id')
.argument('<connectionId>', 'KTX connection id')
.option('--url <url>', 'Connection URL, env:NAME, or file:/path reference')
.option('--schema <schema>', 'Schema to include; repeatable', collectOption, [])
.option('--readonly', 'Mark the connection as read-only', false)
.option('--force', 'Replace an existing connection', false)
.option('--allow-literal-credentials', 'Allow writing a literal credential URL to klo.yaml', false)
.option('--allow-literal-credentials', 'Allow writing a literal credential URL to ktx.yaml', false)
.addOption(new Option('--token-env <name>', 'Environment variable containing Notion auth token').conflicts('tokenFile'))
.addOption(new Option('--token-file <path>', 'File containing Notion auth token').conflicts('tokenEnv'))
.addOption(
@ -155,8 +155,8 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
connection
.command('remove')
.description('Remove a configured connection from klo.yaml')
.argument('<connectionId>', 'KLO connection id')
.description('Remove a configured connection from ktx.yaml')
.argument('<connectionId>', 'KTX connection id')
.option('--force', 'Remove without prompting', false)
.option('--no-input', 'Disable interactive terminal input')
.action(async (connectionId: string, options: { force?: boolean; input?: boolean }, command) => {
@ -188,14 +188,14 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
registerConnectionNotionCommands(connection, context);
}
export function registerConnectionMappingCommands(connection: Command, context: KloCliCommandContext): void {
export function registerConnectionMappingCommands(connection: Command, context: KtxCliCommandContext): void {
const mapping = connection
.command('mapping')
.description('Manage Metabase warehouse mappings')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
mapping

View file

@ -1,10 +1,10 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { LocalMetabaseSourceStateReader } from '@klo/context/ingest';
import { initKloProject, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
import { LocalMetabaseSourceStateReader } from '@ktx/context/ingest';
import { initKtxProject, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloConnectionMapping } from './connection-mapping.js';
import { runKtxConnectionMapping } from './connection-mapping.js';
function makeIo() {
let stdout = '';
@ -27,18 +27,18 @@ function makeIo() {
};
}
describe('runKloConnectionMapping', () => {
describe('runKtxConnectionMapping', () => {
let tempDir: string;
let projectDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-metabase-mapping-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-metabase-mapping-'));
projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'mapping' });
const project = await loadKloProject({ projectDir });
await initKtxProject({ projectDir, projectName: 'mapping' });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections: {
'prod-metabase': {
@ -53,22 +53,22 @@ describe('runKloConnectionMapping', () => {
},
},
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Seed Metabase mapping test connections',
);
});
async function replaceConnections(connections: Record<string, { driver: string; [key: string]: unknown }>) {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections,
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Replace mapping test connections',
);
}
@ -80,7 +80,7 @@ describe('runKloConnectionMapping', () => {
it('sets, lists, disables, and clears local Metabase mappings', async () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'set',
projectDir,
@ -95,13 +95,13 @@ describe('runKloConnectionMapping', () => {
const listIo = makeIo();
await expect(
runKloConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-metabase', json: false }, listIo.io),
runKtxConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-metabase', json: false }, listIo.io),
).resolves.toBe(0);
expect(listIo.stdout()).toContain('1 -> prod-warehouse');
expect(listIo.stdout()).toContain('unhydrated');
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'set-sync-enabled',
projectDir,
@ -114,7 +114,7 @@ describe('runKloConnectionMapping', () => {
).resolves.toBe(0);
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'clear',
projectDir,
@ -127,12 +127,12 @@ describe('runKloConnectionMapping', () => {
});
it('lists Metabase yaml mapping bootstrap rows before any SQLite command writes', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'klo-cli-yaml-mapping-'));
await initKloProject({ projectDir, projectName: 'yaml-mapping' });
const project = await loadKloProject({ projectDir });
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-cli-yaml-mapping-'));
await initKtxProject({ projectDir, projectName: 'yaml-mapping' });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections: {
'prod-metabase': {
@ -145,21 +145,21 @@ describe('runKloConnectionMapping', () => {
'prod-warehouse': { driver: 'postgres', url: 'postgresql://readonly@db.test/analytics' },
},
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Seed yaml mappings',
);
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{ command: 'list', projectDir, connectionId: 'prod-metabase', json: false },
io.io,
),
).resolves.toBe(0);
expect(io.stdout()).toContain('1 -> prod-warehouse');
expect(io.stdout()).toContain('source: klo.yaml');
expect(io.stdout()).toContain('source: ktx.yaml');
});
it('refreshes Metabase discovery metadata through the injected runtime client', async () => {
@ -178,7 +178,7 @@ describe('runKloConnectionMapping', () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'refresh',
projectDir,
@ -194,7 +194,7 @@ describe('runKloConnectionMapping', () => {
expect(io.stdout()).toContain('Discovery: 1 database');
expect(client.cleanup).toHaveBeenCalledTimes(1);
const store = new LocalMetabaseSourceStateReader({ dbPath: join(projectDir, '.klo', 'db.sqlite') });
const store = new LocalMetabaseSourceStateReader({ dbPath: join(projectDir, '.ktx', 'db.sqlite') });
await expect(store.listDatabaseMappings('prod-metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 1, metabaseDatabaseName: 'Analytics', source: 'refresh' },
]);
@ -215,7 +215,7 @@ describe('runKloConnectionMapping', () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'set',
projectDir,
@ -228,7 +228,7 @@ describe('runKloConnectionMapping', () => {
),
).resolves.toBe(0);
await expect(
runKloConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-looker', json: false }, io.io),
runKtxConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-looker', json: false }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain('analytics -> prod-warehouse');
@ -242,7 +242,7 @@ describe('runKloConnectionMapping', () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{
command: 'set',
projectDir,
@ -273,7 +273,7 @@ describe('runKloConnectionMapping', () => {
const io = makeIo();
await expect(
runKloConnectionMapping(
runKtxConnectionMapping(
{ command: 'refresh', projectDir, connectionId: 'prod-looker', autoAccept: true },
io.io,
{
@ -298,12 +298,12 @@ describe('runKloConnectionMapping', () => {
});
it('validates Looker mappings through the canonical local warehouse descriptor', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'klo-cli-descriptor-validation-'));
await initKloProject({ projectDir, projectName: 'descriptor-validation' });
const project = await loadKloProject({ projectDir });
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-cli-descriptor-validation-'));
await initKtxProject({ projectDir, projectName: 'descriptor-validation' });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections: {
'prod-looker': {
@ -313,14 +313,14 @@ describe('runKloConnectionMapping', () => {
'prod-warehouse': { driver: 'postgresql', url: 'postgresql://readonly@db.test/analytics' },
},
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Seed descriptor validation',
);
const io = makeIo();
await expect(
runKloConnectionMapping({ command: 'validate', projectDir, connectionId: 'prod-looker' }, io.io),
runKtxConnectionMapping({ command: 'validate', projectDir, connectionId: 'prod-looker' }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain('Mapping validation passed: prod-looker');

View file

@ -1,5 +1,5 @@
import { readFile } from 'node:fs/promises';
import { localConnectionToWarehouseDescriptor } from '@klo/context/connections';
import { localConnectionToWarehouseDescriptor } from '@ktx/context/connections';
import {
DEFAULT_METABASE_CLIENT_CONFIG,
DefaultLookerConnectionClientFactory,
@ -12,20 +12,20 @@ import {
discoverMetabaseDatabases,
lookerCredentialsFromLocalConnection,
metabaseRuntimeConfigFromLocalConnection,
seedLocalMappingStateFromKloYaml,
seedLocalMappingStateFromKtxYaml,
validateLookerMappings,
validateMappingPhysicalMatch,
type LookerMappingClient,
type MetabaseRuntimeClient,
type MetabaseSyncMode,
} from '@klo/context/ingest';
import { type KloLocalProject, kloLocalStateDbPath, loadKloProject } from '@klo/context/project';
import type { KloCliIo } from '../index.js';
} from '@ktx/context/ingest';
import { type KtxLocalProject, ktxLocalStateDbPath, loadKtxProject } from '@ktx/context/project';
import type { KtxCliIo } from '../index.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/connection-mapping');
export type KloConnectionMappingArgs =
export type KtxConnectionMappingArgs =
| { command: 'list'; projectDir: string; connectionId: string; json: boolean }
| {
command: 'set';
@ -57,13 +57,13 @@ export type KloConnectionMappingArgs =
| { command: 'validate'; projectDir: string; connectionId: string }
| { command: 'clear'; projectDir: string; connectionId: string; metabaseDatabaseId?: number; mappingKey?: string };
interface KloConnectionMappingDeps {
interface KtxConnectionMappingDeps {
createMetabaseClient?: (
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
) => Promise<Pick<MetabaseRuntimeClient, 'getDatabases' | 'cleanup'>>;
createLookerClient?: (
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
) => Promise<Pick<LookerMappingClient, 'listLookerConnections'> & { cleanup?(): Promise<void> }>;
}
@ -85,7 +85,7 @@ function parseId(value: string, label: string): number {
}
async function createDefaultMetabaseClient(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
): Promise<Pick<MetabaseRuntimeClient, 'getDatabases' | 'cleanup'>> {
const factory = new DefaultMetabaseConnectionClientFactory(
@ -97,7 +97,7 @@ async function createDefaultMetabaseClient(
}
async function createDefaultLookerClient(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
): Promise<Pick<LookerMappingClient, 'listLookerConnections'> & { cleanup?(): Promise<void> }> {
const factory = new DefaultLookerConnectionClientFactory({
@ -110,30 +110,30 @@ async function createDefaultLookerClient(
};
}
function isLookerConnection(project: KloLocalProject, connectionId: string): boolean {
function isLookerConnection(project: KtxLocalProject, connectionId: string): boolean {
return String(project.config.connections[connectionId]?.driver ?? '').toLowerCase() === 'looker';
}
function assertLookerConnection(project: KloLocalProject, connectionId: string): void {
function assertLookerConnection(project: KtxLocalProject, connectionId: string): void {
if (!isLookerConnection(project, connectionId)) {
throw new Error(`Connection "${connectionId}" is not a Looker connection`);
}
}
function assertMetabaseConnection(project: KloLocalProject, connectionId: string): void {
function assertMetabaseConnection(project: KtxLocalProject, connectionId: string): void {
const connection = project.config.connections[connectionId];
if (!connection || String(connection.driver).toLowerCase() !== 'metabase') {
throw new Error(`Connection "${connectionId}" is not a Metabase connection`);
}
}
function assertTargetConnection(project: KloLocalProject, connectionId: string): void {
function assertTargetConnection(project: KtxLocalProject, connectionId: string): void {
if (!project.config.connections[connectionId]) {
throw new Error(`Target connection "${connectionId}" does not exist`);
}
}
function targetPhysicalInfo(project: KloLocalProject, connectionId: string) {
function targetPhysicalInfo(project: KtxLocalProject, connectionId: string) {
const descriptor = localConnectionToWarehouseDescriptor(connectionId, project.config.connections[connectionId]);
if (!descriptor) {
return { connection_type: 'UNKNOWN' };
@ -160,22 +160,22 @@ function renderMapping(
}
function renderLookerMapping(row: Awaited<ReturnType<LocalLookerRuntimeStore['listConnectionMappings']>>[number]): string {
const target = row.kloConnectionId ?? '[unmapped]';
const target = row.ktxConnectionId ?? '[unmapped]';
const metadata = [row.lookerDialect, row.lookerHost, row.lookerDatabase].filter(Boolean).join(', ');
return `${row.lookerConnectionName} -> ${target}${metadata ? ` (${metadata}, source: ${row.source})` : ` (source: ${row.source})`}`;
}
export async function runKloConnectionMapping(
args: KloConnectionMappingArgs,
io: KloCliIo = process,
deps: KloConnectionMappingDeps = {},
export async function runKtxConnectionMapping(
args: KtxConnectionMappingArgs,
io: KtxCliIo = process,
deps: KtxConnectionMappingDeps = {},
): Promise<number> {
try {
const project = await loadKloProject({ projectDir: args.projectDir });
await seedLocalMappingStateFromKloYaml(project, args.connectionId);
const project = await loadKtxProject({ projectDir: args.projectDir });
await seedLocalMappingStateFromKtxYaml(project, args.connectionId);
if (isLookerConnection(project, args.connectionId)) {
assertLookerConnection(project, args.connectionId);
const store = new LocalLookerRuntimeStore({ dbPath: kloLocalStateDbPath(project) });
const store = new LocalLookerRuntimeStore({ dbPath: ktxLocalStateDbPath(project) });
if (args.command === 'list') {
const rows = await store.listConnectionMappings(args.connectionId);
@ -191,7 +191,7 @@ export async function runKloConnectionMapping(
await store.upsertConnectionMapping({
lookerConnectionId: args.connectionId,
lookerConnectionName: args.key,
kloConnectionId: args.value,
ktxConnectionId: args.value,
source: 'cli',
});
io.stdout.write(`Set connectionMappings.${args.key} = ${args.value}\n`);
@ -219,13 +219,13 @@ export async function runKloConnectionMapping(
}
if (args.command === 'validate') {
const knownKloConnectionIds = new Set(Object.keys(project.config.connections));
const knownKtxConnectionIds = new Set(Object.keys(project.config.connections));
const knownConnectionTypes = new Map(
Object.entries(project.config.connections).map(([id, _config]) => [id, targetPhysicalInfo(project, id).connection_type]),
);
const validation = validateLookerMappings({
mappings: await store.readMappings(args.connectionId),
knownKloConnectionIds,
knownKtxConnectionIds,
knownConnectionTypes,
});
if (!validation.ok) {
@ -255,7 +255,7 @@ export async function runKloConnectionMapping(
}
assertMetabaseConnection(project, args.connectionId);
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
if (args.command === 'list') {
const rows = await store.listDatabaseMappings(args.connectionId);

View file

@ -1,17 +1,17 @@
import { type Command, Option } from '@commander-js/extra-typings';
import {
type KloCliCommandContext,
type KtxCliCommandContext,
parseNonEmptyAssignmentOption,
parsePositiveIntegerOption,
parseSafeConnectionIdOption,
resolveCommandProjectDir,
} from '../cli-program.js';
import {
type KloConnectionMetabaseSetupArgs,
type KtxConnectionMetabaseSetupArgs,
type MetabaseSetupMappingAssignment,
type MetabaseSetupSyncMode,
runKloConnectionMetabaseSetup,
runKtxConnectionMetabaseSetup,
} from './connection-metabase-setup.js';
const SYNC_MODE_CHOICES = ['ALL', 'ONLY', 'EXCEPT'] as const satisfies readonly MetabaseSetupSyncMode[];
@ -51,21 +51,21 @@ function collectMappingOption(
}
async function runMetabaseSetupArgs(
context: KloCliCommandContext,
args: KloConnectionMetabaseSetupArgs,
context: KtxCliCommandContext,
args: KtxConnectionMetabaseSetupArgs,
): Promise<void> {
const runner = context.deps.connectionMetabaseSetup ?? runKloConnectionMetabaseSetup;
const runner = context.deps.connectionMetabaseSetup ?? runKtxConnectionMetabaseSetup;
context.setExitCode(await runner(args, context.io));
}
export function registerConnectionMetabaseCommands(connection: Command, context: KloCliCommandContext): void {
export function registerConnectionMetabaseCommands(connection: Command, context: KtxCliCommandContext): void {
const metabase = connection
.command('metabase')
.description('Configure Metabase connections')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
metabase.action(() => {
@ -76,7 +76,7 @@ export function registerConnectionMetabaseCommands(connection: Command, context:
metabase
.command('setup')
.description('Guided setup for a Metabase connection')
.option('--id <connectionId>', 'KLO connection id to write', parseSafeConnectionIdOption)
.option('--id <connectionId>', 'KTX connection id to write', parseSafeConnectionIdOption)
.option('--url <url>', 'Metabase API URL')
.addOption(new Option('--api-key <key>', 'Metabase API key').conflicts('mintApiKey'))
.option('--mint-api-key', 'Mint a Metabase API key with credentials', false)
@ -85,10 +85,10 @@ export function registerConnectionMetabaseCommands(connection: Command, context:
.addHelpText(
'after',
'\nGuided equivalent of:\n' +
' klo connection mapping refresh <connectionId> --auto-accept\n' +
' klo connection mapping set <connectionId> databaseMappings <id>=<target>\n' +
' klo connection mapping set-sync-enabled <connectionId> <id> --enabled true\n' +
' klo ingest <connectionId>\n',
' ktx connection mapping refresh <connectionId> --auto-accept\n' +
' ktx connection mapping set <connectionId> databaseMappings <id>=<target>\n' +
' ktx connection mapping set-sync-enabled <connectionId> <id> --enabled true\n' +
' ktx ingest <connectionId>\n',
)
.option(
'--map <metabaseDatabaseId=targetConnectionId>',

View file

@ -1,11 +1,11 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { LocalMetabaseSourceStateReader } from '@klo/context/ingest';
import { initKloProject, kloLocalStateDbPath, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
import { LocalMetabaseSourceStateReader } from '@ktx/context/ingest';
import { initKtxProject, ktxLocalStateDbPath, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloConnectionMetabaseSetup } from './connection-metabase-setup.js';
import { runKtxConnectionMetabaseSetup } from './connection-metabase-setup.js';
const CANCEL_PROMPT = Symbol('cancel');
@ -135,7 +135,7 @@ function makeIo(options: { isTTY?: boolean; stdinIsTTY?: boolean } = {}) {
};
}
describe('runKloConnectionMetabaseSetup', () => {
describe('runKtxConnectionMetabaseSetup', () => {
const fakeMetabaseCredential = 'mb_example';
const existingMetabaseCredential = 'mb_existing';
const fakeAdminCredential = 'pw';
@ -144,9 +144,9 @@ describe('runKloConnectionMetabaseSetup', () => {
let projectDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-metabase-setup-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-metabase-setup-'));
projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'metabase-setup' });
await initKtxProject({ projectDir, projectName: 'metabase-setup' });
});
afterEach(async () => {
@ -154,15 +154,15 @@ describe('runKloConnectionMetabaseSetup', () => {
});
async function writeConnections(connections: Record<string, { driver: string; [key: string]: unknown }>) {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig({
'ktx.yaml',
serializeKtxProjectConfig({
...project.config,
connections,
}),
'klo',
'klo@example.com',
'ktx',
'ktx@example.com',
'Seed Metabase setup test connections',
);
}
@ -208,7 +208,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -230,17 +230,17 @@ describe('runKloConnectionMetabaseSetup', () => {
expect(io.stdout()).toContain('Connection: metabase');
expect(io.stdout()).toContain('Discovered 1 database');
expect(io.stdout()).toContain(`klo ingest metabase --project-dir ${projectDir}`);
expect(io.stdout()).toContain(`ktx ingest metabase --project-dir ${projectDir}`);
expect(io.stdout()).not.toContain('mb_example');
expect(io.stderr()).not.toContain('mb_example');
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
expect(config).toContain('api_url: http://metabase.example.test:3000');
expect(config).toContain('api_key: mb_example');
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{
metabaseDatabaseId: 2,
@ -275,7 +275,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -295,8 +295,8 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
]);
@ -314,7 +314,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -350,7 +350,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -370,8 +370,8 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
]);
@ -384,7 +384,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -412,7 +412,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -440,7 +440,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const missingUsernameIo = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -462,7 +462,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const missingPasswordIo = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -500,7 +500,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const mintingIo = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -537,7 +537,7 @@ describe('runKloConnectionMetabaseSetup', () => {
expect(mintingIo.stdout()).not.toContain(fakeAdminCredential);
expect(mintingIo.stderr()).not.toContain(fakeAdminCredential);
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
expect(config).toContain('api_url: http://metabase.example.test:3000');
expect(config).toContain(`api_key: ${mintedMetabaseCredential}`);
@ -548,7 +548,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -590,7 +590,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -640,7 +640,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -660,8 +660,8 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 1, targetConnectionId: 'orbit', syncEnabled: true },
{ metabaseDatabaseId: 2, targetConnectionId: null, syncEnabled: false },
@ -676,7 +676,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -712,7 +712,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -759,7 +759,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -782,12 +782,12 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(1);
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
expect(io.stderr()).toContain(`klo ingest metabase --project-dir ${projectDir}`);
expect(io.stderr()).toContain(`ktx ingest metabase --project-dir ${projectDir}`);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit' },
]);
@ -810,7 +810,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -857,7 +857,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const interactiveMetabaseCredential = 'mb_interactive_fixture';
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -882,13 +882,13 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('driver: metabase');
expect(config).toContain('api_url: http://metabase.example.test:3000');
expect(config).toContain(`api_key: ${interactiveMetabaseCredential}`);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{
metabaseDatabaseId: 2,
@ -931,7 +931,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const events: string[] = [];
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -958,8 +958,8 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
{ metabaseDatabaseId: 3, targetConnectionId: 'warehouse2', syncEnabled: false },
@ -997,7 +997,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const events: string[] = [];
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -1023,7 +1023,7 @@ describe('runKloConnectionMetabaseSetup', () => {
),
).resolves.toBe(0);
expect(events).toContain('intro:KLO Metabase setup');
expect(events).toContain('intro:KTX Metabase setup');
expect(events.some((event) => event.startsWith('spinner.start:Testing Metabase connection'))).toBe(true);
expect(events.some((event) => event.startsWith('spinner.stop:Metabase reachable'))).toBe(true);
expect(events.some((event) => event.startsWith('spinner.start:Discovering Metabase databases'))).toBe(true);
@ -1053,7 +1053,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const io = makeIo();
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -1081,7 +1081,7 @@ describe('runKloConnectionMetabaseSetup', () => {
},
});
const beforeConfig = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const beforeConfig = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
const metabaseClient = makeMetabaseClient({
testConnectionSuccess: true,
databases: [
@ -1098,7 +1098,7 @@ describe('runKloConnectionMetabaseSetup', () => {
const cancelMetabaseCredential = 'mb_cancel_fixture';
await expect(
runKloConnectionMetabaseSetup(
runKtxConnectionMetabaseSetup(
{
command: 'setup',
projectDir,
@ -1126,11 +1126,11 @@ describe('runKloConnectionMetabaseSetup', () => {
expect(io.stderr()).toContain('Setup cancelled.');
expect(io.stderr()).not.toContain(cancelMetabaseCredential);
const afterConfig = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const afterConfig = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(afterConfig).toBe(beforeConfig);
const updatedProject = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await expect(store.listDatabaseMappings('metabase')).resolves.toEqual([]);
});
});

View file

@ -12,7 +12,7 @@ import {
select,
text,
} from '@clack/prompts';
import { localConnectionToWarehouseDescriptor } from '@klo/context/connections';
import { localConnectionToWarehouseDescriptor } from '@ktx/context/connections';
import {
DEFAULT_METABASE_CLIENT_CONFIG,
DefaultMetabaseConnectionClientFactory,
@ -23,21 +23,21 @@ import {
type MetabaseSyncMode,
metabaseRuntimeConfigFromLocalConnection,
validateMappingPhysicalMatch,
} from '@klo/context/ingest';
} from '@ktx/context/ingest';
import {
type KloLocalProject,
type KloProjectConnectionConfig,
kloLocalStateDbPath,
loadKloProject,
serializeKloProjectConfig,
} from '@klo/context/project';
type KtxLocalProject,
type KtxProjectConnectionConfig,
ktxLocalStateDbPath,
loadKtxProject,
serializeKtxProjectConfig,
} from '@ktx/context/project';
import { createClackSpinner, type KloCliSpinner } from '../clack.js';
import type { KloCliIo } from '../cli-runtime.js';
import { createClackSpinner, type KtxCliSpinner } from '../clack.js';
import type { KtxCliIo } from '../cli-runtime.js';
import { withMenuOptionsSpacing, withMultiselectNavigation } from '../prompt-navigation.js';
import { type KloPublicIngestArgs, runKloPublicIngest } from '../public-ingest.js';
import { type KtxPublicIngestArgs, runKtxPublicIngest } from '../public-ingest.js';
export type KloMetabaseSetupInputMode = 'auto' | 'disabled';
export type KtxMetabaseSetupInputMode = 'auto' | 'disabled';
export type MetabaseSetupSyncMode = MetabaseSyncMode;
@ -56,7 +56,7 @@ export interface MetabaseSetupPromptAdapter {
outro(message?: string): void;
note(message: string, title: string): void;
log: MetabaseSetupLogger;
spinner(): KloCliSpinner;
spinner(): KtxCliSpinner;
select<T extends string>(options: { message: string; options: Array<MetabaseSetupPromptOption<T>> }): Promise<T>;
multiselect<Value extends number | string>(options: {
message: string;
@ -71,7 +71,7 @@ export interface MetabaseSetupPromptAdapter {
cancel(message: string): void;
}
type KloMetabaseSetupInteractiveIo = KloCliIo & {
type KtxMetabaseSetupInteractiveIo = KtxCliIo & {
stdin?: { isTTY?: boolean };
};
@ -86,9 +86,9 @@ export interface MintMetabaseApiKeyArgs {
password: string;
}
export type MintMetabaseApiKey = (args: MintMetabaseApiKeyArgs, io: KloCliIo) => Promise<string>;
export type MintMetabaseApiKey = (args: MintMetabaseApiKeyArgs, io: KtxCliIo) => Promise<string>;
export interface KloConnectionMetabaseSetupArgs {
export interface KtxConnectionMetabaseSetupArgs {
command: 'setup';
projectDir: string;
connectionId?: string;
@ -102,20 +102,20 @@ export interface KloConnectionMetabaseSetupArgs {
syncMode: MetabaseSetupSyncMode;
runIngest: boolean;
yes: boolean;
inputMode: KloMetabaseSetupInputMode;
inputMode: KtxMetabaseSetupInputMode;
}
export interface KloConnectionMetabaseSetupDeps {
export interface KtxConnectionMetabaseSetupDeps {
createMetabaseClient?: (
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
) => Promise<Pick<MetabaseRuntimeClient, 'testConnection' | 'getDatabases' | 'cleanup'>>;
mintMetabaseApiKey?: MintMetabaseApiKey;
prompts?: MetabaseSetupPromptAdapter;
runPublicIngest?: (args: Extract<KloPublicIngestArgs, { command: 'run' }>, io: KloCliIo) => Promise<number>;
runPublicIngest?: (args: Extract<KtxPublicIngestArgs, { command: 'run' }>, io: KtxCliIo) => Promise<number>;
}
function isMetabaseConnection(connection: KloProjectConnectionConfig | undefined): boolean {
function isMetabaseConnection(connection: KtxProjectConnectionConfig | undefined): boolean {
return (
String(connection?.driver ?? '')
.trim()
@ -131,22 +131,22 @@ function uniqueSorted(values: number[]): number[] {
return [...new Set(values)].sort((a, b) => a - b);
}
function resolveMetabaseUrl(connection: KloProjectConnectionConfig | undefined): string | undefined {
function resolveMetabaseUrl(connection: KtxProjectConnectionConfig | undefined): string | undefined {
return stringField(connection?.api_url) ?? stringField(connection?.apiUrl) ?? stringField(connection?.url);
}
function resolveLiteralMetabaseApiKey(connection: KloProjectConnectionConfig | undefined): string | undefined {
function resolveLiteralMetabaseApiKey(connection: KtxProjectConnectionConfig | undefined): string | undefined {
return stringField(connection?.api_key) ?? stringField(connection?.apiKey);
}
function listMetabaseConnectionIds(project: KloLocalProject): string[] {
function listMetabaseConnectionIds(project: KtxLocalProject): string[] {
return Object.entries(project.config.connections)
.filter(([_connectionId, connection]) => isMetabaseConnection(connection))
.map(([connectionId]) => connectionId)
.sort();
}
function listWarehouseConnectionIds(project: KloLocalProject): string[] {
function listWarehouseConnectionIds(project: KtxLocalProject): string[] {
return Object.entries(project.config.connections)
.filter(([connectionId, connection]) => localConnectionToWarehouseDescriptor(connectionId, connection) != null)
.map(([connectionId]) => connectionId)
@ -165,7 +165,7 @@ function redactSecrets(message: string, secrets: string[]): string {
}
async function createDefaultMetabaseClient(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
): Promise<Pick<MetabaseRuntimeClient, 'testConnection' | 'getDatabases' | 'cleanup'>> {
const factory = new DefaultMetabaseConnectionClientFactory(
@ -192,7 +192,7 @@ async function defaultMintMetabaseApiKey(args: MintMetabaseApiKeyArgs): Promise<
const mintedKey = await sessionClient.createApiKey({
groupId: adminGroup.id,
name: `KLO CLI ${new Date().toISOString()}`,
name: `KTX CLI ${new Date().toISOString()}`,
});
const trimmedKey = stringField(mintedKey);
if (!trimmedKey) {
@ -237,7 +237,7 @@ export function createClackMetabaseSetupPromptAdapter(): MetabaseSetupPromptAdap
log.error(message);
},
},
spinner(): KloCliSpinner {
spinner(): KtxCliSpinner {
return createClackSpinner();
},
async select<T extends string>(options: {
@ -271,8 +271,8 @@ export function createClackMetabaseSetupPromptAdapter(): MetabaseSetupPromptAdap
}
function isInteractiveMetabaseSetupIo(
args: Pick<KloConnectionMetabaseSetupArgs, 'inputMode'>,
io: KloMetabaseSetupInteractiveIo,
args: Pick<KtxConnectionMetabaseSetupArgs, 'inputMode'>,
io: KtxMetabaseSetupInteractiveIo,
): boolean {
return args.inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
}
@ -295,7 +295,7 @@ function normalizeDiscoveredDatabases(databases: MetabaseDatabase[]): Array<{
}));
}
function targetPhysicalInfo(project: KloLocalProject, connectionId: string) {
function targetPhysicalInfo(project: KtxLocalProject, connectionId: string) {
const descriptor = localConnectionToWarehouseDescriptor(connectionId, project.config.connections[connectionId]);
if (!descriptor) {
return { connection_type: 'UNKNOWN' };
@ -338,23 +338,23 @@ function noteMetabaseSetupSummary(options: {
);
}
export async function runKloConnectionMetabaseSetup(
args: KloConnectionMetabaseSetupArgs,
io: KloCliIo,
deps: KloConnectionMetabaseSetupDeps = {},
export async function runKtxConnectionMetabaseSetup(
args: KtxConnectionMetabaseSetupArgs,
io: KtxCliIo,
deps: KtxConnectionMetabaseSetupDeps = {},
): Promise<number> {
let apiKeyForRedaction = args.apiKey;
let passwordForRedaction = args.metabasePassword;
const interactiveIo = io as KloMetabaseSetupInteractiveIo;
const interactiveIo = io as KtxMetabaseSetupInteractiveIo;
const isInteractive = isInteractiveMetabaseSetupIo(args, interactiveIo);
const prompts = deps.prompts ?? (isInteractive ? createClackMetabaseSetupPromptAdapter() : undefined);
try {
if (isInteractive && prompts) {
prompts.intro('KLO Metabase setup');
prompts.intro('KTX Metabase setup');
}
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
const existingMetabaseConnectionIds = listMetabaseConnectionIds(project);
let connectionId: string;
@ -491,7 +491,7 @@ export async function runKloConnectionMetabaseSetup(
throw new Error('Metabase API key is required (use --api-key)');
}
const transientConnectionConfig: KloProjectConnectionConfig = {
const transientConnectionConfig: KtxProjectConnectionConfig = {
...(existingConnection ?? {}),
driver: 'metabase',
api_url: url,
@ -504,7 +504,7 @@ export async function runKloConnectionMetabaseSetup(
[connectionId]: transientConnectionConfig,
},
};
const discoveryProject: KloLocalProject = { ...project, config: configWithTransient };
const discoveryProject: KtxLocalProject = { ...project, config: configWithTransient };
for (const mapping of args.mappings) {
if (!configWithTransient.connections[mapping.targetConnectionId]) {
@ -618,7 +618,7 @@ export async function runKloConnectionMetabaseSetup(
}
const targetConnectionId = await prompts.select({
message: `Map Metabase database ${database.id} ("${database.name}") to which KLO connection?`,
message: `Map Metabase database ${database.id} ("${database.name}") to which KTX connection?`,
options: warehouseConnectionIds.map((warehouseId) => ({ value: warehouseId, label: warehouseId })),
});
resolvedMappings.push({ metabaseDatabaseId: databaseId, targetConnectionId });
@ -641,7 +641,7 @@ export async function runKloConnectionMetabaseSetup(
syncEnabledDatabaseIds: resolvedSyncEnabledDatabaseIds,
});
const confirmed = await prompts.confirm({
message: 'Write changes to klo.yaml and enable sync?',
message: 'Write changes to ktx.yaml and enable sync?',
initialValue: true,
});
if (!confirmed) {
@ -675,15 +675,15 @@ export async function runKloConnectionMetabaseSetup(
}
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(configWithTransient),
'klo',
'klo@example.com',
'ktx.yaml',
serializeKtxProjectConfig(configWithTransient),
'ktx',
'ktx@example.com',
`Setup Metabase connection ${connectionId}`,
);
const updatedProject = await loadKloProject({ projectDir: args.projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
const updatedProject = await loadKtxProject({ projectDir: args.projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
await store.refreshDiscoveredDatabases({ connectionId, discovered });
@ -716,7 +716,7 @@ export async function runKloConnectionMetabaseSetup(
const unhydrated = await store.getUnhydratedSyncEnabledMappingIds(connectionId);
if (unhydrated.length > 0) {
io.stderr.write(
`Sync-enabled mappings are missing discovery metadata; run klo connection mapping refresh ${connectionId} --auto-accept\n`,
`Sync-enabled mappings are missing discovery metadata; run ktx connection mapping refresh ${connectionId} --auto-accept\n`,
);
return 1;
}
@ -743,10 +743,10 @@ export async function runKloConnectionMetabaseSetup(
io.stdout.write(`Connection: ${connectionId}\n`);
io.stdout.write(`Discovered ${discovered.length} ${discovered.length === 1 ? 'database' : 'databases'}\n`);
io.stdout.write(`Next: klo ingest ${connectionId} --project-dir ${args.projectDir}\n`);
io.stdout.write(`Next: ktx ingest ${connectionId} --project-dir ${args.projectDir}\n`);
if (args.runIngest) {
const ingestRunner = deps.runPublicIngest ?? runKloPublicIngest;
const ingestRunner = deps.runPublicIngest ?? runKtxPublicIngest;
const exitCode = await ingestRunner(
{
command: 'run',
@ -759,7 +759,7 @@ export async function runKloConnectionMetabaseSetup(
io,
);
if (exitCode !== 0) {
io.stderr.write(`Ingest failed; re-run: klo ingest ${connectionId} --project-dir ${args.projectDir}\n`);
io.stderr.write(`Ingest failed; re-run: ktx ingest ${connectionId} --project-dir ${args.projectDir}\n`);
return 1;
}
}

View file

@ -1,6 +1,6 @@
import { type Command, InvalidArgumentError } from '@commander-js/extra-typings';
import { collectOption, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KloConnectionNotionArgs } from './connection-notion.js';
import { collectOption, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxConnectionNotionArgs } from './connection-notion.js';
interface NotionPickOptions {
input?: boolean;
@ -36,7 +36,7 @@ function normalizeNotionPageId(value: string): string {
return `${lower.slice(0, 8)}-${lower.slice(8, 12)}-${lower.slice(12, 16)}-${lower.slice(16, 20)}-${lower.slice(20)}`;
}
function buildPickArgs(connectionId: string, projectDir: string, options: NotionPickOptions): KloConnectionNotionArgs {
function buildPickArgs(connectionId: string, projectDir: string, options: NotionPickOptions): KtxConnectionNotionArgs {
if (options.input !== false) {
return {
command: 'pick',
@ -59,19 +59,19 @@ function buildPickArgs(connectionId: string, projectDir: string, options: Notion
};
}
async function runConnectionNotionArgs(context: KloCliCommandContext, args: KloConnectionNotionArgs): Promise<void> {
const runner = context.deps.connectionNotion ?? (await import('./connection-notion.js')).runKloConnectionNotion;
async function runConnectionNotionArgs(context: KtxCliCommandContext, args: KtxConnectionNotionArgs): Promise<void> {
const runner = context.deps.connectionNotion ?? (await import('./connection-notion.js')).runKtxConnectionNotion;
context.setExitCode(await runner(args, context.io));
}
export function registerConnectionNotionCommands(connect: Command, context: KloCliCommandContext): void {
export function registerConnectionNotionCommands(connect: Command, context: KtxCliCommandContext): void {
const notion = connect
.command('notion')
.description('Configure Notion source selection')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
notion.action(() => {

View file

@ -10,7 +10,7 @@ import {
type PickerCommand,
type PickerState,
} from './connection-notion-tree.js';
import type { KloCliIo } from '../index.js';
import type { KtxCliIo } from '../index.js';
const COLOR_THEME = {
text: 'white',
@ -28,9 +28,9 @@ const NO_COLOR_THEME = {
type NotionPickerTheme = Record<keyof typeof COLOR_THEME, string>;
export interface NotionPickerTuiIo extends KloCliIo {
export interface NotionPickerTuiIo extends KtxCliIo {
stdin?: { isTTY?: boolean; setRawMode?(value: boolean): void };
stdout: KloCliIo['stdout'] & { isTTY?: boolean; columns?: number; rows?: number };
stdout: KtxCliIo['stdout'] & { isTTY?: boolean; columns?: number; rows?: number };
}
interface InkKey {

View file

@ -2,11 +2,11 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
initKloProject,
loadKloProject,
serializeKloProjectConfig,
type KloProjectConfig,
} from '@klo/context/project';
initKtxProject,
loadKtxProject,
serializeKtxProjectConfig,
type KtxProjectConfig,
} from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
applyNotionPickerWriteback,
@ -14,7 +14,7 @@ import {
notionPickerPageFromSearchResult,
normalizeNotionPageId,
resolveNotionWorkspaceLabel,
runKloConnectionNotion,
runKtxConnectionNotion,
type NotionPickerApi,
type PickerRenderInput,
type PickerRenderResult,
@ -91,24 +91,24 @@ describe('normalizeNotionPageId', () => {
});
});
describe('runKloConnectionNotion', () => {
describe('runKtxConnectionNotion', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-notion-pick-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-notion-pick-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
async function writeProjectConfig(projectDir: string, config: KloProjectConfig): Promise<void> {
const project = await loadKloProject({ projectDir });
async function writeProjectConfig(projectDir: string, config: KtxProjectConfig): Promise<void> {
const project = await loadKtxProject({ projectDir });
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(config),
'klo',
'klo@example.com',
'ktx.yaml',
serializeKtxProjectConfig(config),
'ktx',
'ktx@example.com',
'seed test config',
);
}
@ -120,7 +120,7 @@ describe('runKloConnectionNotion', () => {
});
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir: '/tmp/project',
@ -138,7 +138,7 @@ describe('runKloConnectionNotion', () => {
it('writes selected root_page_ids while preserving every other Notion connection field', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -160,7 +160,7 @@ describe('runKloConnectionNotion', () => {
const io = makeIo();
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir,
@ -175,7 +175,7 @@ describe('runKloConnectionNotion', () => {
),
).resolves.toBe(0);
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(yaml).toContain('crawl_mode: selected_roots');
expect(yaml).toContain('root_page_ids:');
expect(yaml).toContain('11111111-2222-3333-4444-555555555555');
@ -193,7 +193,7 @@ describe('runKloConnectionNotion', () => {
it('rejects empty writeback, missing connections, and non-Notion connections', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -204,7 +204,7 @@ describe('runKloConnectionNotion', () => {
},
},
});
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
await expect(applyNotionPickerWriteback(project, 'warehouse', [])).rejects.toThrow(
'connection notion pick requires at least one root page id',
@ -297,7 +297,7 @@ describe('runKloConnectionNotion', () => {
it('runs interactive discovery, warns about stale roots, renders the TUI, and saves selected roots', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -330,7 +330,7 @@ describe('runKloConnectionNotion', () => {
const io = makeIo();
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir,
@ -346,7 +346,7 @@ describe('runKloConnectionNotion', () => {
),
).resolves.toBe(0);
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(yaml).toContain('crawl_mode: selected_roots');
expect(yaml).toContain(PAGE_IDS.engineering);
expect(yaml).not.toContain(PAGE_IDS.stale);
@ -357,7 +357,7 @@ describe('runKloConnectionNotion', () => {
it('passes partial-discovery warnings into the TUI banner state', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -394,7 +394,7 @@ describe('runKloConnectionNotion', () => {
const io = makeIo();
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir,
@ -422,7 +422,7 @@ describe('runKloConnectionNotion', () => {
it('quits interactive mode without writing when the TUI returns quit', async () => {
const projectDir = join(tempDir, 'project');
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeProjectConfig(projectDir, {
...initialized.config,
connections: {
@ -440,11 +440,11 @@ describe('runKloConnectionNotion', () => {
},
},
});
const before = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const before = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
const io = makeIo();
await expect(
runKloConnectionNotion(
runKtxConnectionNotion(
{
command: 'pick',
projectDir,
@ -460,7 +460,7 @@ describe('runKloConnectionNotion', () => {
),
).resolves.toBe(0);
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toBe(before);
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toBe(before);
expect(io.stdout()).toContain('No changes saved.');
});
});

View file

@ -1,12 +1,12 @@
import { parseNotionConnectionConfig, resolveNotionAuthToken } from '@klo/context/connections';
import { type NotionApi, type NotionBotInfo, NotionClient } from '@klo/context/ingest';
import { parseNotionConnectionConfig, resolveNotionAuthToken } from '@ktx/context/connections';
import { type NotionApi, type NotionBotInfo, NotionClient } from '@ktx/context/ingest';
import {
type KloLocalProject,
type KloProjectConnectionConfig,
loadKloProject,
serializeKloProjectConfig,
} from '@klo/context/project';
import type { KloCliIo } from '../index.js';
type KtxLocalProject,
type KtxProjectConnectionConfig,
loadKtxProject,
serializeKtxProjectConfig,
} from '@ktx/context/project';
import type { KtxCliIo } from '../index.js';
import { profileMark } from '../startup-profile.js';
import { buildInitialState, buildPickerTree, type NotionPickerPageInput } from './connection-notion-tree.js';
import {
@ -18,7 +18,7 @@ import {
profileMark('module:commands/connection-notion');
export type KloConnectionNotionArgs =
export type KtxConnectionNotionArgs =
| {
command: 'pick';
projectDir: string;
@ -36,9 +36,9 @@ export type KloConnectionNotionArgs =
export type NotionPickerApi = Pick<NotionApi, 'search' | 'retrieveBotUser'>;
export type { PickerRenderInput, PickerRenderResult };
interface KloConnectionNotionDeps {
interface KtxConnectionNotionDeps {
env?: Record<string, string | undefined>;
loadProject?: typeof loadKloProject;
loadProject?: typeof loadKtxProject;
createNotionApi?: (authToken: string) => NotionPickerApi;
renderPicker?: (input: PickerRenderInput, io: NotionPickerTuiIo) => Promise<PickerRenderResult>;
}
@ -168,7 +168,7 @@ export async function resolveNotionWorkspaceLabel(api: NotionPickerApi, connecti
}
}
function notionConnection(project: KloLocalProject, connectionId: string): KloProjectConnectionConfig {
function notionConnection(project: KtxLocalProject, connectionId: string): KtxProjectConnectionConfig {
const connection = project.config.connections[connectionId];
if (!connection) {
throw new Error(`Connection "${connectionId}" not found`);
@ -180,7 +180,7 @@ function notionConnection(project: KloLocalProject, connectionId: string): KloPr
}
export async function applyNotionPickerWriteback(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
rootPageIds: string[],
): Promise<void> {
@ -202,22 +202,22 @@ export async function applyNotionPickerWriteback(
};
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(nextConfig),
'klo',
'klo@example.com',
'ktx.yaml',
serializeKtxProjectConfig(nextConfig),
'ktx',
'ktx@example.com',
`Pick Notion roots: ${connectionId} (${rootPageIds.length} pages)`,
);
}
export async function runKloConnectionNotion(
args: KloConnectionNotionArgs,
io: KloCliIo = process,
deps: KloConnectionNotionDeps = {},
export async function runKtxConnectionNotion(
args: KtxConnectionNotionArgs,
io: KtxCliIo = process,
deps: KtxConnectionNotionDeps = {},
): Promise<number> {
try {
assertSafeConnectionId(args.connectionId);
const loadProject = deps.loadProject ?? loadKloProject;
const loadProject = deps.loadProject ?? loadKtxProject;
if (args.mode === 'interactive') {
const project = await loadProject({ projectDir: args.projectDir });

View file

@ -1,14 +1,14 @@
import { type Command, Option } from '@commander-js/extra-typings';
import {
type CommandWithGlobalOptions,
type KloCliCommandContext,
type KtxCliCommandContext,
resolveCommandProjectDirOverride,
} from '../cli-program.js';
import {
type KloDemoArgs,
type KloDemoInputMode,
type KloDemoMode,
type KloDemoOutputMode,
type KtxDemoArgs,
type KtxDemoInputMode,
type KtxDemoMode,
type KtxDemoOutputMode,
} from '../demo.js';
import { defaultDemoProjectDir } from '../demo-assets.js';
import { resolveProjectDir } from '../project-dir.js';
@ -23,7 +23,7 @@ interface DemoOptions {
projectDir?: string;
}
function demoOutputMode(options: { plain?: boolean; json?: boolean }): KloDemoOutputMode {
function demoOutputMode(options: { plain?: boolean; json?: boolean }): KtxDemoOutputMode {
if (options.json === true) {
return 'json';
}
@ -37,14 +37,14 @@ function demoDoctorOutputMode(options: { json?: boolean }): 'plain' | 'json' {
return options.json === true ? 'json' : 'plain';
}
function demoInspectOutputMode(options: { plain?: boolean; json?: boolean }): KloDemoOutputMode {
function demoInspectOutputMode(options: { plain?: boolean; json?: boolean }): KtxDemoOutputMode {
if (options.json === true) {
return 'json';
}
return 'plain';
}
function demoInputMode(options: { input?: boolean }): { inputMode?: KloDemoInputMode } {
function demoInputMode(options: { input?: boolean }): { inputMode?: KtxDemoInputMode } {
return options.input === false ? { inputMode: 'disabled' } : {};
}
@ -118,19 +118,19 @@ export function resolveDemoCommandOptions<T>(command: { opts: () => T; optsWithG
return { ...inherited, ...definedOptions(command.opts() as Record<string, unknown>, inherited, command) } as T;
}
async function runDemoArgs(context: KloCliCommandContext, args: KloDemoArgs): Promise<void> {
const runner = context.deps.demo ?? (await import('../demo.js')).runKloDemo;
async function runDemoArgs(context: KtxCliCommandContext, args: KtxDemoArgs): Promise<void> {
const runner = context.deps.demo ?? (await import('../demo.js')).runKtxDemo;
context.setExitCode(await runner(args, context.io));
}
export function registerDemoCommands(
program: Command,
context: KloCliCommandContext,
context: KtxCliCommandContext,
options: { description?: string } = {},
): void {
const demo = program
.command('demo')
.description(options.description ?? 'Run the pre-seeded KLO demo or a full LLM-backed demo')
.description(options.description ?? 'Run the pre-seeded KTX demo or a full LLM-backed demo')
.addOption(
new Option('--mode <mode>', 'Demo mode: seeded (default), replay, or full')
.choices(['seeded', 'replay', 'full'])
@ -260,7 +260,7 @@ export function registerDemoCommands(
.addOption(new Option('--plain', 'Print plain text output instead of the visual demo').conflicts('json'))
.addOption(new Option('--json', 'Print JSON output').conflicts('plain'))
.option('--no-input', 'Disable interactive terminal input')
.action(async (_options, command: { opts: () => { mode: KloDemoMode } & DemoOptions }) => {
.action(async (_options, command: { opts: () => { mode: KtxDemoMode } & DemoOptions }) => {
const options = resolveDemoCommandOptions(command);
await runDemoArgs(context, {
command: 'ingest',

View file

@ -1,6 +1,6 @@
import type { Command } from '@commander-js/extra-typings';
import { type CommandWithGlobalOptions, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KloDoctorArgs } from '../doctor.js';
import { type CommandWithGlobalOptions, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxDoctorArgs } from '../doctor.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/doctor-commands');
@ -13,15 +13,15 @@ function inputMode(options: { input?: boolean }): { inputMode?: 'disabled' } {
return options.input === false ? { inputMode: 'disabled' } : {};
}
async function runDoctorArgs(context: KloCliCommandContext, args: KloDoctorArgs): Promise<void> {
const runner = context.deps.doctor ?? (await import('../doctor.js')).runKloDoctor;
async function runDoctorArgs(context: KtxCliCommandContext, args: KtxDoctorArgs): Promise<void> {
const runner = context.deps.doctor ?? (await import('../doctor.js')).runKtxDoctor;
context.setExitCode(await runner(args, context.io));
}
export function registerDoctorCommands(program: Command, context: KloCliCommandContext): void {
export function registerDoctorCommands(program: Command, context: KtxCliCommandContext): void {
const doctor = program
.command('doctor')
.description('Check KLO setup, project, and demo readiness')
.description('Check KTX setup, project, and demo readiness')
.option('--json', 'Print JSON output', false)
.option('--no-input', 'Disable interactive terminal input')
.action(async (options: { json?: boolean; input?: boolean }, command) => {
@ -35,7 +35,7 @@ export function registerDoctorCommands(program: Command, context: KloCliCommandC
doctor
.command('setup')
.description('Check KLO install, build, and local runtime readiness')
.description('Check KTX install, build, and local runtime readiness')
.option('--json', 'Print JSON output', false)
.option('--no-input', 'Disable interactive terminal input')
.action(

View file

@ -1,22 +1,22 @@
import { resolve } from 'node:path';
import { type Command, Option } from '@commander-js/extra-typings';
import { type KloCliCommandContext, type OutputModeOptions, resolveCommandProjectDir } from '../cli-program.js';
import type { KloCliDeps, KloCliIo } from '../index.js';
import type { KloIngestArgs, KloIngestOutputMode } from '../ingest.js';
import { type KtxCliCommandContext, type OutputModeOptions, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxCliDeps, KtxCliIo } from '../index.js';
import type { KtxIngestArgs, KtxIngestOutputMode } from '../ingest.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/ingest-commands');
interface IngestCommandOptions {
runIngestWithProgress: (
args: KloIngestArgs,
io: KloCliIo,
deps: KloCliDeps,
defaultRunIngest: (args: KloIngestArgs, io: KloCliIo) => Promise<number>,
args: KtxIngestArgs,
io: KtxCliIo,
deps: KtxCliDeps,
defaultRunIngest: (args: KtxIngestArgs, io: KtxCliIo) => Promise<number>,
) => Promise<number>;
}
function outputMode(options: OutputModeOptions): KloIngestOutputMode {
function outputMode(options: OutputModeOptions): KtxIngestOutputMode {
if (options.json === true) {
return 'json';
}
@ -26,7 +26,7 @@ function outputMode(options: OutputModeOptions): KloIngestOutputMode {
return 'plain';
}
function watchOutputMode(options: OutputModeOptions): KloIngestOutputMode {
function watchOutputMode(options: OutputModeOptions): KtxIngestOutputMode {
if (options.json === true) {
return 'json';
}
@ -36,22 +36,22 @@ function watchOutputMode(options: OutputModeOptions): KloIngestOutputMode {
return 'viz';
}
function inputMode(options: OutputModeOptions): Pick<KloIngestArgs, 'inputMode'> {
function inputMode(options: OutputModeOptions): Pick<KtxIngestArgs, 'inputMode'> {
return options.input === false ? { inputMode: 'disabled' } : {};
}
async function runIngestArgs(
context: KloCliCommandContext,
args: KloIngestArgs,
context: KtxCliCommandContext,
args: KtxIngestArgs,
options: IngestCommandOptions,
): Promise<void> {
const { runKloIngest } = await import('../ingest.js');
context.setExitCode(await options.runIngestWithProgress(args, context.io, context.deps, runKloIngest));
const { runKtxIngest } = await import('../ingest.js');
context.setExitCode(await options.runIngestWithProgress(args, context.io, context.deps, runKtxIngest));
}
export function registerIngestCommands(
program: Command,
context: KloCliCommandContext,
context: KtxCliCommandContext,
commandOptions: IngestCommandOptions,
): void {
const ingest = program
@ -66,7 +66,7 @@ export function registerIngestCommands(
ingest
.command('run')
.description('Run local ingest for one configured connection and source adapter')
.requiredOption('--connection-id <connectionId>', 'KLO connection id')
.requiredOption('--connection-id <connectionId>', 'KTX connection id')
.requiredOption('--adapter <adapter>', 'Ingest source adapter name')
.option('--source-dir <path>', 'Directory containing source files')
.option('--database-introspection-url <url>', 'Daemon URL for live-database introspection')

View file

@ -1,24 +1,24 @@
import { type Command, Option } from '@commander-js/extra-typings';
import { collectOption, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { collectOption, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { wikiWriteCommandSchema } from '../command-schemas.js';
import type { KloKnowledgeArgs } from '../knowledge.js';
import type { KtxKnowledgeArgs } from '../knowledge.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/knowledge-commands');
async function runKnowledgeArgs(context: KloCliCommandContext, args: KloKnowledgeArgs): Promise<void> {
const runner = context.deps.knowledge ?? (await import('../knowledge.js')).runKloKnowledge;
async function runKnowledgeArgs(context: KtxCliCommandContext, args: KtxKnowledgeArgs): Promise<void> {
const runner = context.deps.knowledge ?? (await import('../knowledge.js')).runKtxKnowledge;
context.setExitCode(await runner(args, context.io));
}
export function registerWikiCommands(program: Command, context: KloCliCommandContext): void {
export function registerWikiCommands(program: Command, context: KtxCliCommandContext): void {
const wiki = program
.command('wiki')
.description('List, read, search, or write local wiki pages')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
wiki

View file

@ -1,7 +1,7 @@
import { InvalidArgumentError, type Command } from '@commander-js/extra-typings';
import { type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import { publicIngestReadCommandSchema, publicIngestRunCommandSchema } from '../command-schemas.js';
import type { KloPublicIngestArgs, KloPublicIngestInputMode } from '../public-ingest.js';
import type { KtxPublicIngestArgs, KtxPublicIngestInputMode } from '../public-ingest.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/public-ingest-commands');
@ -12,26 +12,26 @@ interface PublicIngestOptions {
input?: boolean;
}
function inputMode(options: { input?: boolean }): KloPublicIngestInputMode {
function inputMode(options: { input?: boolean }): KtxPublicIngestInputMode {
return options.input === false ? 'disabled' : 'auto';
}
async function runPublicIngestArgs(context: KloCliCommandContext, args: KloPublicIngestArgs): Promise<void> {
const runner = context.deps.publicIngest ?? (await import('../public-ingest.js')).runKloPublicIngest;
async function runPublicIngestArgs(context: KtxCliCommandContext, args: KtxPublicIngestArgs): Promise<void> {
const runner = context.deps.publicIngest ?? (await import('../public-ingest.js')).runKtxPublicIngest;
context.setExitCode(await runner(args, context.io));
}
function parsePublicIngestConnectionId(value: string): string {
if (value === 'run') {
throw new InvalidArgumentError('run is reserved; use klo dev ingest run for low-level adapter syntax');
throw new InvalidArgumentError('run is reserved; use ktx dev ingest run for low-level adapter syntax');
}
return value;
}
export function registerPublicIngestCommands(program: Command, context: KloCliCommandContext): void {
export function registerPublicIngestCommands(program: Command, context: KtxCliCommandContext): void {
const ingest = program
.command('ingest')
.description('Build and refresh KLO context from configured sources')
.description('Build and refresh KTX context from configured sources')
.usage('[options] [connectionId]')
.argument('[connectionId]', 'Connection id to ingest', parsePublicIngestConnectionId)
.option('--all', 'Ingest every eligible configured source', false)
@ -42,12 +42,12 @@ export function registerPublicIngestCommands(program: Command, context: KloCliCo
[
'',
'Examples:',
' klo ingest <connectionId> [options]',
' klo ingest --all [options]',
' klo ingest status [runId] [options]',
' klo ingest watch [runId] [options]',
' ktx ingest <connectionId> [options]',
' ktx ingest --all [options]',
' ktx ingest status [runId] [options]',
' ktx ingest watch [runId] [options]',
'',
'Project directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.',
'Project directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.',
'',
].join('\n'),
)
@ -58,7 +58,7 @@ export function registerPublicIngestCommands(program: Command, context: KloCliCo
.action(async (connectionId: string | undefined, _options: PublicIngestOptions, command) => {
const options = command.opts();
if (options.all === true && connectionId) {
throw new Error('klo ingest accepts either --all or <connectionId>, not both');
throw new Error('ktx ingest accepts either --all or <connectionId>, not both');
}
const args = publicIngestRunCommandSchema.parse({
command: 'run',

View file

@ -1,35 +1,35 @@
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
import { type KloCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
import type { KloScanArgs } from '../scan.js';
import { type KtxCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxScanArgs } from '../scan.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/scan-commands');
async function runScanArgs(context: KloCliCommandContext, args: KloScanArgs): Promise<void> {
const runner = context.deps.scan ?? (await import('../scan.js')).runKloScan;
async function runScanArgs(context: KtxCliCommandContext, args: KtxScanArgs): Promise<void> {
const runner = context.deps.scan ?? (await import('../scan.js')).runKtxScan;
context.setExitCode(await runner(args, context.io));
}
type KloScanModeOption = Extract<KloScanArgs, { command: 'run' }>['mode'];
type KtxScanModeOption = Extract<KtxScanArgs, { command: 'run' }>['mode'];
function parseScanModeOption(value: string): KloScanModeOption {
function parseScanModeOption(value: string): KtxScanModeOption {
if (value === 'structural' || value === 'enriched' || value === 'relationships') {
return value;
}
throw new InvalidArgumentError('Allowed choices are structural, enriched, relationships');
}
type KloRelationshipStatusOption = Extract<KloScanArgs, { command: 'relationships' }>['status'];
type KloRelationshipFeedbackDecisionOption = Extract<KloScanArgs, { command: 'relationshipFeedback' }>['decision'];
type KtxRelationshipStatusOption = Extract<KtxScanArgs, { command: 'relationships' }>['status'];
type KtxRelationshipFeedbackDecisionOption = Extract<KtxScanArgs, { command: 'relationshipFeedback' }>['decision'];
function parseRelationshipStatusOption(value: string): KloRelationshipStatusOption {
function parseRelationshipStatusOption(value: string): KtxRelationshipStatusOption {
if (value === 'accepted' || value === 'review' || value === 'rejected' || value === 'skipped' || value === 'all') {
return value;
}
throw new InvalidArgumentError('Allowed choices are accepted, review, rejected, skipped, all');
}
function parseRelationshipFeedbackDecisionOption(value: string): KloRelationshipFeedbackDecisionOption {
function parseRelationshipFeedbackDecisionOption(value: string): KtxRelationshipFeedbackDecisionOption {
if (value === 'accepted' || value === 'rejected' || value === 'all') {
return value;
}
@ -58,7 +58,7 @@ function relationshipDecisionArgs(options: {
note?: string;
json?: boolean;
}): Pick<
Extract<KloScanArgs, { command: 'relationshipDecision' }>,
Extract<KtxScanArgs, { command: 'relationshipDecision' }>,
'candidateId' | 'decision' | 'reviewer' | 'note' | 'json'
> | null {
const decisionCount = [options.accept !== undefined, options.reject !== undefined].filter(Boolean).length;
@ -69,7 +69,7 @@ function relationshipDecisionArgs(options: {
return {
candidateId: options.accept,
decision: 'accepted',
reviewer: options.reviewer ?? 'klo',
reviewer: options.reviewer ?? 'ktx',
note: options.note ?? null,
json: options.json === true,
};
@ -78,7 +78,7 @@ function relationshipDecisionArgs(options: {
return {
candidateId: options.reject,
decision: 'rejected',
reviewer: options.reviewer ?? 'klo',
reviewer: options.reviewer ?? 'ktx',
note: options.note ?? null,
json: options.json === true,
};
@ -90,11 +90,11 @@ function collectRelationshipCandidateOption(value: string, previous: string[]):
return [...previous, parseNonEmptyOption(value)];
}
export function registerScanCommands(program: Command, context: KloCliCommandContext): void {
export function registerScanCommands(program: Command, context: KtxCliCommandContext): void {
const scan = program
.command('scan')
.description('Run or inspect standalone connection scans')
.argument('[connectionId]', 'KLO connection id to scan')
.argument('[connectionId]', 'KTX connection id to scan')
.option(
'--mode <mode>',
'Scan mode: structural, enriched, relationships (default: structural)',
@ -105,7 +105,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
)
.hook('preAction', (_thisCommand, actionCommand) => {
context.writeDebug?.('scan', actionCommand);
@ -113,7 +113,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.action(async (connectionId: string | undefined, options, command) => {
if (!connectionId) {
scan.outputHelp();
context.io.stderr.write('klo dev scan requires <connectionId> or a subcommand\n');
context.io.stderr.write('ktx dev scan requires <connectionId> or a subcommand\n');
context.setExitCode(1);
return;
}
@ -135,7 +135,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.argument('<runId>', 'Local scan run id')
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (runId: string, _options: unknown, command) => {
await runScanArgs(context, {
@ -152,7 +152,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print the raw scan report JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (runId: string, options, command) => {
await runScanArgs(context, {
@ -189,7 +189,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print relationship artifacts as JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (runId: string, options, command) => {
const decision = relationshipDecisionArgs(options);
@ -231,7 +231,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print the apply result as JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (runId: string, options, command) => {
const parentOptions = command.parent?.opts() as { dryRun?: boolean } | undefined;
@ -249,7 +249,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
scan
.command('relationship-feedback')
.description('Export persisted relationship review decisions as calibration labels')
.option('--connection <connectionId>', 'Only export labels for one KLO connection')
.option('--connection <connectionId>', 'Only export labels for one KTX connection')
.option(
'--decision <decision>',
'Relationship feedback decision: accepted, rejected, all',
@ -260,7 +260,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.addOption(new Option('--jsonl', 'Print labels as newline-delimited JSON').default(false).conflicts('json'))
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (options, command) => {
await runScanArgs(context, {
@ -276,7 +276,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
scan
.command('relationship-calibration')
.description('Summarize relationship feedback labels against current score thresholds')
.option('--connection <connectionId>', 'Only calibrate labels for one KLO connection')
.option('--connection <connectionId>', 'Only calibrate labels for one KTX connection')
.option(
'--decision <decision>',
'Relationship feedback decision: accepted, rejected, all',
@ -298,7 +298,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print the calibration report as JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (options, command) => {
await runScanArgs(context, {
@ -315,7 +315,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
scan
.command('relationship-thresholds')
.description('Evaluate relationship feedback labels for offline threshold advice')
.option('--connection <connectionId>', 'Only evaluate labels for one KLO connection')
.option('--connection <connectionId>', 'Only evaluate labels for one KTX connection')
.option(
'--min-total-labels <count>',
'Minimum scored labels before advice can be ready',
@ -337,7 +337,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
.option('--json', 'Print the threshold advice report as JSON', false)
.addHelpText(
'after',
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
)
.action(async (options, command) => {
await runScanArgs(context, {

View file

@ -1,6 +1,6 @@
import { type Command, InvalidArgumentError } from '@commander-js/extra-typings';
import { type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KloServeArgs } from '../serve.js';
import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
import type { KtxServeArgs } from '../serve.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/serve-commands');
@ -12,10 +12,10 @@ function parseMcp(value: string): 'stdio' {
throw new InvalidArgumentError('Only stdio is supported in this phase');
}
export function registerServeCommands(program: Command, context: KloCliCommandContext): void {
export function registerServeCommands(program: Command, context: KtxCliCommandContext): void {
program
.command('serve')
.description('Run standalone KLO services such as MCP stdio')
.description('Run standalone KTX services such as MCP stdio')
.requiredOption('--mcp <mode>', 'MCP transport mode', parseMcp)
.option('--user-id <id>', 'Local user id', 'local')
.option('--semantic-compute', 'Enable semantic-layer compute', false)
@ -30,7 +30,7 @@ export function registerServeCommands(program: Command, context: KloCliCommandCo
if (options.executeQueries === true && !semanticCompute) {
throw new Error('--execute-queries requires --semantic-compute');
}
const args: KloServeArgs = {
const args: KtxServeArgs = {
mcp: options.mcp,
projectDir: resolveCommandProjectDir(command),
userId: options.userId,
@ -41,7 +41,7 @@ export function registerServeCommands(program: Command, context: KloCliCommandCo
memoryCapture: options.memoryCapture === true,
memoryModel: options.memoryModel,
};
const runner = context.deps.serveStdio ?? (await import('../serve.js')).runKloServeStdio;
const runner = context.deps.serveStdio ?? (await import('../serve.js')).runKtxServeStdio;
context.setExitCode(await runner(args));
});
}

View file

@ -1,15 +1,15 @@
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
import type { KloCliCommandContext } from '../cli-program.js';
import type { KtxCliCommandContext } from '../cli-program.js';
import { resolveCommandProjectDir } from '../cli-program.js';
import type { KloSetupDatabaseDriver } from '../setup-databases.js';
import type { KloSetupSourceType } from '../setup-sources.js';
import type { KtxSetupDatabaseDriver } from '../setup-databases.js';
import type { KtxSetupSourceType } from '../setup-sources.js';
import { registerDemoCommands } from './demo-commands.js';
async function runSetupArgs(
context: KloCliCommandContext,
context: KtxCliCommandContext,
args: Parameters<NonNullable<typeof context.deps.setup>>[0],
) {
const runner = context.deps.setup ?? (await import('../setup.js')).runKloSetup;
const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
context.setExitCode(await runner(args, context.io));
}
@ -28,7 +28,7 @@ function embeddingBackend(value: string): 'openai' | 'sentence-transformers' {
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function databaseDriver(value: string): KloSetupDatabaseDriver {
function databaseDriver(value: string): KtxSetupDatabaseDriver {
if (
value === 'sqlite' ||
value === 'postgres' ||
@ -43,7 +43,7 @@ function databaseDriver(value: string): KloSetupDatabaseDriver {
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function sourceType(value: string): KloSetupSourceType {
function sourceType(value: string): KtxSetupSourceType {
if (
value === 'dbt' ||
value === 'metricflow' ||
@ -109,7 +109,7 @@ function shouldShowSetupEntryMenu(
embeddingApiKeyEnv?: string;
embeddingApiKeyFile?: string;
skipEmbeddings?: boolean;
database?: KloSetupDatabaseDriver[];
database?: KtxSetupDatabaseDriver[];
databaseConnectionId?: string[];
newDatabaseConnectionId?: string;
databaseUrl?: string;
@ -121,7 +121,7 @@ function shouldShowSetupEntryMenu(
historicSqlServiceAccountPattern?: string[];
historicSqlRedactionPattern?: string[];
skipDatabases?: boolean;
source?: KloSetupSourceType;
source?: KtxSetupSourceType;
sourceConnectionId?: string;
sourcePath?: string;
sourceGitUrl?: string;
@ -210,13 +210,13 @@ function shouldShowSetupEntryMenu(
].some((optionName) => optionWasSpecified(command, optionName));
}
export function registerSetupCommands(program: Command, context: KloCliCommandContext): void {
export function registerSetupCommands(program: Command, context: KtxCliCommandContext): void {
const setup = program
.command('setup')
.description('Set up or resume a local KLO project')
.option('--project-dir <path>', 'KLO project directory')
.option('--new', 'Create a new KLO project before setup', false)
.option('--existing', 'Use an existing KLO project', false)
.description('Set up or resume a local KTX project')
.option('--project-dir <path>', 'KTX project directory')
.option('--new', 'Create a new KTX project before setup', false)
.option('--existing', 'Use an existing KTX project', false)
.option('--agents', 'Install agent integration only', false)
.addOption(
new Option('--target <target>', 'Agent target').choices([
@ -247,10 +247,10 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
.option(
'--database <driver>',
'Database driver to configure; repeatable',
(value, previous: KloSetupDatabaseDriver[]) => {
(value, previous: KtxSetupDatabaseDriver[]) => {
return [...previous, databaseDriver(value)];
},
[] as KloSetupDatabaseDriver[],
[] as KtxSetupDatabaseDriver[],
)
.option(
'--database-connection-id <id>',
@ -291,7 +291,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
(value, previous: string[]) => [...previous, value],
[],
)
.option('--skip-databases', 'Leave database setup incomplete; KLO cannot work until a primary source is added', false)
.option('--skip-databases', 'Leave database setup incomplete; KTX cannot work until a primary source is added', false)
.addOption(new Option('--source <type>', 'Source connector type').argParser(sourceType))
.option('--source-connection-id <id>', 'Connection id for source setup')
.option('--source-path <path>', 'Local source path for dbt, MetricFlow, or LookML')
@ -421,9 +421,9 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
});
});
registerDemoCommands(setup, context, { description: 'Run the packaged KLO demo from setup' });
registerDemoCommands(setup, context, { description: 'Run the packaged KTX demo from setup' });
const setupContext = setup.command('context').description('Build, inspect, and recover setup-managed KLO context');
const setupContext = setup.command('context').description('Build, inspect, and recover setup-managed KTX context');
function setupContextInputMode(command: {
optsWithGlobals?: () => unknown;
@ -435,7 +435,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
setupContext
.command('build')
.description('Build agent-ready KLO context for setup')
.description('Build agent-ready KTX context for setup')
.option('--no-input', 'Disable interactive terminal input')
.action(async (options: { input?: boolean }, command) => {
await runSetupArgs(context, {
@ -505,7 +505,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
setup
.command('status')
.description('Show setup readiness for the resolved KLO project')
.description('Show setup readiness for the resolved KTX project')
.option('--json', 'Print JSON output', false)
.action(async (options: { json?: boolean }, command) => {
await runSetupArgs(context, {

View file

@ -1,12 +1,12 @@
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
import {
collectOption,
type KloCliCommandContext,
type KtxCliCommandContext,
parsePositiveIntegerOption,
resolveCommandProjectDir,
} from '../cli-program.js';
import { slQueryCommandSchema } from '../command-schemas.js';
import type { KloSlArgs } from '../sl.js';
import type { KtxSlArgs } from '../sl.js';
import { profileMark } from '../startup-profile.js';
profileMark('module:commands/sl-commands');
@ -32,24 +32,24 @@ function collectOrderBy(
return [...previous, parseOrderBy(value)];
}
async function runSlArgs(context: KloCliCommandContext, args: KloSlArgs): Promise<void> {
const runner = context.deps.sl ?? (await import('../sl.js')).runKloSl;
async function runSlArgs(context: KtxCliCommandContext, args: KtxSlArgs): Promise<void> {
const runner = context.deps.sl ?? (await import('../sl.js')).runKtxSl;
context.setExitCode(await runner(args, context.io));
}
export function registerSlCommands(program: Command, context: KloCliCommandContext, commandName = 'sl'): void {
export function registerSlCommands(program: Command, context: KtxCliCommandContext, commandName = 'sl'): void {
const sl = program
.command(commandName)
.description('List, read, validate, query, or write local semantic-layer sources')
.showHelpAfterError()
.addHelpText(
'after',
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
);
sl.command('list')
.description('List semantic-layer sources')
.option('--connection-id <id>', 'KLO connection id')
.option('--connection-id <id>', 'KTX connection id')
.addOption(
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
'pretty',
@ -71,7 +71,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
sl.command('read')
.description('Read a semantic-layer source')
.argument('<sourceName>', 'Semantic-layer source name')
.requiredOption('--connection-id <id>', 'KLO connection id')
.requiredOption('--connection-id <id>', 'KTX connection id')
.action(async (sourceName: string, options: { connectionId: string }, command) => {
await runSlArgs(context, {
command: 'read',
@ -84,7 +84,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
sl.command('validate')
.description('Validate a semantic-layer source')
.argument('<sourceName>', 'Semantic-layer source name')
.requiredOption('--connection-id <id>', 'KLO connection id')
.requiredOption('--connection-id <id>', 'KTX connection id')
.action(async (sourceName: string, options: { connectionId: string }, command) => {
await runSlArgs(context, {
command: 'validate',
@ -97,7 +97,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
sl.command('write')
.description('Write a semantic-layer source')
.argument('<sourceName>', 'Semantic-layer source name')
.requiredOption('--connection-id <id>', 'KLO connection id')
.requiredOption('--connection-id <id>', 'KTX connection id')
.requiredOption('--yaml <yaml>', 'Semantic-layer source YAML')
.action(async (sourceName: string, options: { connectionId: string; yaml: string }, command) => {
await runSlArgs(context, {
@ -111,7 +111,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
sl.command('query')
.description('Compile or execute a semantic-layer query')
.option('--connection-id <id>', 'KLO connection id')
.option('--connection-id <id>', 'KTX connection id')
.option('--measure <measure>', 'Measure to query; repeatable', collectOption, [])
.option('--dimension <dimension>', 'Dimension to include; repeatable', collectOption, [])
.option('--filter <filter>', 'Filter expression; repeatable', collectOption, [])

View file

@ -1,14 +1,14 @@
import type { Command } from '@commander-js/extra-typings';
import type { KloCliCommandContext } from '../cli-program.js';
import type { KtxCliCommandContext } from '../cli-program.js';
import { resolveCommandProjectDir } from '../cli-program.js';
export function registerStatusCommands(program: Command, context: KloCliCommandContext): void {
export function registerStatusCommands(program: Command, context: KtxCliCommandContext): void {
program
.command('status')
.description('Show current KLO project setup status')
.description('Show current KTX project setup status')
.option('--json', 'Print JSON output', false)
.action(async (options: { json?: boolean }, command) => {
const runner = context.deps.setup ?? (await import('../setup.js')).runKloSetup;
const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
context.setExitCode(
await runner(
{

View file

@ -28,10 +28,10 @@ export interface ZshCompletionInstallResult {
zshrcPath: string;
}
const KLO_COMPLETION_BLOCK_START = '# >>> klo completion >>>';
const KLO_COMPLETION_BLOCK_END = '# <<< klo completion <<<';
const KLO_COMPLETION_BLOCK_PATTERN = new RegExp(
`\\n?${escapeRegExp(KLO_COMPLETION_BLOCK_START)}[\\s\\S]*?${escapeRegExp(KLO_COMPLETION_BLOCK_END)}\\n?`,
const KTX_COMPLETION_BLOCK_START = '# >>> ktx completion >>>';
const KTX_COMPLETION_BLOCK_END = '# <<< ktx completion <<<';
const KTX_COMPLETION_BLOCK_PATTERN = new RegExp(
`\\n?${escapeRegExp(KTX_COMPLETION_BLOCK_START)}[\\s\\S]*?${escapeRegExp(KTX_COMPLETION_BLOCK_END)}\\n?`,
'g',
);
@ -39,27 +39,27 @@ export function zshCompletionScript(): string {
const zshWords = '$' + '{words[@]}';
const zshCompletionCapture = [
'$',
`{(@f)$("${'$'}{klo_completion_command[@]}" dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}" 2>/dev/null)}`,
`{(@f)$("${'$'}{ktx_completion_command[@]}" dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}" 2>/dev/null)}`,
].join('');
const zshCompletionsCount = '$' + '{#completions[@]}';
const zshCompletionCommand = '$' + '(eval "print -r -- $' + '{KLO_COMPLETION_COMMAND:-klo}")';
const zshCompletionCommand = '$' + '(eval "print -r -- $' + '{KTX_COMPLETION_COMMAND:-ktx}")';
return [
'#compdef klo',
'#compdef ktx',
'',
'_klo() {',
'_ktx() {',
' local -a completions',
' local -a klo_completion_command',
` klo_completion_command=("\${(@z)${zshCompletionCommand}}")`,
' local -a ktx_completion_command',
` ktx_completion_command=("\${(@z)${zshCompletionCommand}}")`,
` completions=("${zshCompletionCapture}")`,
` if (( ${zshCompletionsCount} )); then`,
" _describe 'klo completions' completions",
" _describe 'ktx completions' completions",
' else',
' _files',
' fi',
'}',
'',
'compdef _klo klo',
'compdef _ktx ktx',
'',
].join('\n');
}
@ -68,7 +68,7 @@ export async function installZshCompletion(): Promise<ZshCompletionInstallResult
const homeDir = process.env.HOME || homedir();
const zshConfigDir = process.env.ZDOTDIR || homeDir;
const completionDir = join(homeDir, '.zfunc');
const completionPath = join(completionDir, '_klo');
const completionPath = join(completionDir, '_ktx');
const zshrcPath = join(zshConfigDir, '.zshrc');
await mkdir(completionDir, { recursive: true });
@ -290,7 +290,7 @@ async function readOptionalTextFile(path: string): Promise<string> {
}
function updateZshrcCompletionBlock(contents: string): string {
const withoutManagedBlock = contents.replace(KLO_COMPLETION_BLOCK_PATTERN, normalizeTrailingNewline);
const withoutManagedBlock = contents.replace(KTX_COMPLETION_BLOCK_PATTERN, normalizeTrailingNewline);
const hasCompinit = /^.*\bcompinit\b.*$/m.test(withoutManagedBlock);
const block = zshrcCompletionBlock({ includeCompinit: !hasCompinit });
@ -313,23 +313,23 @@ function updateZshrcCompletionBlock(contents: string): string {
function zshrcCompletionBlock(options: { includeCompinit: boolean }): string {
return [
KLO_COMPLETION_BLOCK_START,
'_klo_completion_command() {',
KTX_COMPLETION_BLOCK_START,
'_ktx_completion_command() {',
' local dir="$PWD"',
' while [[ "$dir" != "/" ]]; do',
` if [[ -f "$dir/package.json" ]] && command grep -q '"name": "klo-workspace"' "$dir/package.json" 2>/dev/null; then`,
' print -r -- "node $dir/scripts/run-klo.mjs --"',
` if [[ -f "$dir/package.json" ]] && command grep -q '"name": "ktx-workspace"' "$dir/package.json" 2>/dev/null; then`,
' print -r -- "node $dir/scripts/run-ktx.mjs --"',
' return',
' fi',
' dir="' + '$' + '{dir:h}"',
' done',
' print -r -- "klo"',
' print -r -- "ktx"',
'}',
"export KLO_COMPLETION_COMMAND='$(_klo_completion_command)'",
"export KTX_COMPLETION_COMMAND='$(_ktx_completion_command)'",
'setopt complete_aliases',
'fpath=("$HOME/.zfunc" $fpath)',
...(options.includeCompinit ? ['autoload -Uz compinit', 'compinit'] : []),
KLO_COMPLETION_BLOCK_END,
KTX_COMPLETION_BLOCK_END,
].join('\n');
}

View file

@ -1,11 +1,11 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initKloProject, parseKloProjectConfig } from '@klo/context/project';
import type { KloConnectionDriver, KloScanConnector, KloSchemaSnapshot } from '@klo/context/scan';
import { initKtxProject, parseKtxProjectConfig } from '@ktx/context/project';
import type { KtxConnectionDriver, KtxScanConnector, KtxSchemaSnapshot } from '@ktx/context/scan';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloConnection } from './connection.js';
import { runKloCli, type KloCliIo } from './index.js';
import { runKtxConnection } from './connection.js';
import { runKtxCli, type KtxCliIo } from './index.js';
function makeIo(options: { stdoutIsTty?: boolean; stdinIsTty?: boolean } = {}) {
let stdout = '';
@ -32,7 +32,7 @@ function makeIo(options: { stdoutIsTty?: boolean; stdinIsTty?: boolean } = {}) {
};
}
function snapshotFor(driver: KloConnectionDriver, tableNames: string[]): KloSchemaSnapshot {
function snapshotFor(driver: KtxConnectionDriver, tableNames: string[]): KtxSchemaSnapshot {
return {
connectionId: 'warehouse',
driver,
@ -52,10 +52,10 @@ function snapshotFor(driver: KloConnectionDriver, tableNames: string[]): KloSche
};
}
function nativeConnector(driver: KloConnectionDriver, tableNames: string[]) {
function nativeConnector(driver: KtxConnectionDriver, tableNames: string[]) {
const introspect = vi.fn(async () => snapshotFor(driver, tableNames));
const cleanup = vi.fn(async () => undefined);
const connector: KloScanConnector = {
const connector: KtxScanConnector = {
id: `${driver}:warehouse`,
driver,
capabilities: {
@ -75,11 +75,11 @@ function nativeConnector(driver: KloConnectionDriver, tableNames: string[]) {
return { connector, introspect, cleanup };
}
describe('runKloConnection', () => {
describe('runKtxConnection', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-connection-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-connection-'));
});
afterEach(async () => {
@ -88,11 +88,11 @@ describe('runKloConnection', () => {
it('adds and lists env-referenced connections without resolving secrets', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'add',
projectDir,
@ -109,18 +109,18 @@ describe('runKloConnection', () => {
).resolves.toBe(0);
expect(io.stdout()).toContain('Connection: warehouse');
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain('url: env:DATABASE_URL');
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain('url: env:DATABASE_URL');
const listIo = makeIo();
await expect(runKloConnection({ command: 'list', projectDir }, listIo.io)).resolves.toBe(0);
await expect(runKtxConnection({ command: 'list', projectDir }, listIo.io)).resolves.toBe(0);
expect(listIo.stdout()).toContain('warehouse');
expect(listIo.stdout()).toContain('postgres');
});
it('removes a configured connection from klo.yaml without deleting local artifacts when forced', async () => {
it('removes a configured connection from ktx.yaml without deleting local artifacts when forced', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -134,14 +134,14 @@ describe('runKloConnection', () => {
},
makeIo().io,
);
const artifactPath = join(projectDir, '.klo', 'artifacts', 'warehouse.txt');
await mkdir(join(projectDir, '.klo', 'artifacts'), { recursive: true });
const artifactPath = join(projectDir, '.ktx', 'artifacts', 'warehouse.txt');
await mkdir(join(projectDir, '.ktx', 'artifacts'), { recursive: true });
await writeFile(artifactPath, 'keep me', 'utf-8');
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'remove',
projectDir,
@ -153,20 +153,20 @@ describe('runKloConnection', () => {
),
).resolves.toBe(0);
const parsed = parseKloProjectConfig(await readFile(join(projectDir, 'klo.yaml'), 'utf-8'));
const parsed = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
expect(parsed.connections.warehouse).toBeUndefined();
await expect(readFile(artifactPath, 'utf-8')).resolves.toBe('keep me');
expect(io.stdout()).toContain('Connection removed from klo.yaml.');
expect(io.stdout()).toContain('Connection removed from ktx.yaml.');
expect(io.stdout()).toContain(
'Ingested artifacts from this connection remain in .klo/. Run klo dev artifacts to inspect.',
'Ingested artifacts from this connection remain in .ktx/. Run ktx dev artifacts to inspect.',
);
expect(io.stderr()).toBe('');
});
it('requires --force when removing in non-interactive mode', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -183,7 +183,7 @@ describe('runKloConnection', () => {
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'remove',
projectDir,
@ -200,11 +200,11 @@ describe('runKloConnection', () => {
it('returns a clear error when removing an unknown connection', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'remove',
projectDir,
@ -216,13 +216,13 @@ describe('runKloConnection', () => {
),
).resolves.toBe(1);
expect(io.stderr()).toContain('Connection "missing" is not configured in klo.yaml');
expect(io.stderr()).toContain('Connection "missing" is not configured in ktx.yaml');
});
it('asks for confirmation before removing in an interactive terminal', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -243,7 +243,7 @@ describe('runKloConnection', () => {
};
await expect(
runKloConnection(
runKtxConnection(
{
command: 'remove',
projectDir,
@ -256,14 +256,14 @@ describe('runKloConnection', () => {
).resolves.toBe(0);
expect(prompts.confirm).toHaveBeenCalledWith({
message: 'Remove connection "warehouse" from klo.yaml? Ingested artifacts will remain in .klo/.',
message: 'Remove connection "warehouse" from ktx.yaml? Ingested artifacts will remain in .ktx/.',
initialValue: false,
});
});
it('runs public connect map as refresh, validate, and list over the low-level mapping runner', async () => {
const io = makeIo();
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
if (argv[0] === 'refresh') {
mappingIo.stdout.write('Discovery: 1 database\n');
mappingIo.stdout.write('Unmapped discovered: 1\n');
@ -282,7 +282,7 @@ describe('runKloConnection', () => {
});
await expect(
runKloConnection(
runKtxConnection(
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-metabase', json: false },
io.io,
{ runMapping },
@ -309,14 +309,14 @@ describe('runKloConnection', () => {
expect(io.stdout()).toContain('Mappings:');
expect(io.stdout()).toContain('1 -> [unmapped]');
expect(io.stdout()).toContain('Next:');
expect(io.stdout()).toContain('klo ingest prod-metabase');
expect(io.stdout()).toContain('klo dev mapping');
expect(io.stdout()).toContain('ktx ingest prod-metabase');
expect(io.stdout()).toContain('ktx dev mapping');
expect(io.stderr()).toBe('');
});
it('prints stable JSON for public connect map without leaking low-level stdout', async () => {
const io = makeIo();
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
if (argv[0] === 'refresh') {
mappingIo.stdout.write('Discovery: 1 connection\nUnmapped discovered: 0\nStale mappings: 0\n');
return 0;
@ -332,8 +332,8 @@ describe('runKloConnection', () => {
[
{
lookerConnectionName: 'analytics',
kloConnectionId: 'prod-warehouse',
source: 'klo.yaml',
ktxConnectionId: 'prod-warehouse',
source: 'ktx.yaml',
},
],
null,
@ -346,7 +346,7 @@ describe('runKloConnection', () => {
});
await expect(
runKloConnection(
runKtxConnection(
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-looker', json: true },
io.io,
{ runMapping },
@ -357,7 +357,7 @@ describe('runKloConnection', () => {
connectionId: string;
refresh: { ok: boolean; output: string[] };
validation: { ok: boolean; output: string[] };
mappings: Array<{ lookerConnectionName: string; kloConnectionId: string; source: string }>;
mappings: Array<{ lookerConnectionName: string; ktxConnectionId: string; source: string }>;
};
expect(parsed).toEqual({
connectionId: 'prod-looker',
@ -372,8 +372,8 @@ describe('runKloConnection', () => {
mappings: [
{
lookerConnectionName: 'analytics',
kloConnectionId: 'prod-warehouse',
source: 'klo.yaml',
ktxConnectionId: 'prod-warehouse',
source: 'ktx.yaml',
},
],
});
@ -382,7 +382,7 @@ describe('runKloConnection', () => {
it('returns the refresh failure when public connect map cannot discover source metadata', async () => {
const io = makeIo();
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
if (argv[0] === 'refresh') {
mappingIo.stderr.write('Metabase API key is not configured\n');
return 1;
@ -391,7 +391,7 @@ describe('runKloConnection', () => {
});
await expect(
runKloConnection(
runKtxConnection(
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-metabase', json: false },
io.io,
{ runMapping },
@ -405,11 +405,11 @@ describe('runKloConnection', () => {
it('rejects literal credential URLs unless explicitly allowed', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'add',
projectDir,
@ -430,12 +430,12 @@ describe('runKloConnection', () => {
it('warns before writing explicitly allowed literal credential URLs without echoing the URL', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
const literalUrl = 'postgres://localhost:5432/warehouse';
await expect(
runKloConnection(
runKtxConnection(
{
command: 'add',
projectDir,
@ -452,19 +452,19 @@ describe('runKloConnection', () => {
).resolves.toBe(0);
expect(io.stderr()).toContain(
'Warning: writing a literal credential URL to klo.yaml for connection "warehouse". Prefer env:NAME or file:/path references.',
'Warning: writing a literal credential URL to ktx.yaml for connection "warehouse". Prefer env:NAME or file:/path references.',
);
expect(io.stderr()).not.toContain(literalUrl);
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain(literalUrl);
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain(literalUrl);
});
it('adds a Notion connection without writing token values', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloConnection(
runKtxConnection(
{
command: 'add',
projectDir,
@ -490,7 +490,7 @@ describe('runKloConnection', () => {
),
).resolves.toBe(0);
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(yaml).toContain('driver: notion');
expect(yaml).toContain('auth_token_ref: env:NOTION_AUTH_TOKEN');
expect(yaml).toContain('crawl_mode: all_accessible');
@ -502,8 +502,8 @@ describe('runKloConnection', () => {
it('runs connection notion pick --no-input through the public connection entrypoint', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -530,7 +530,7 @@ describe('runKloConnection', () => {
const io = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'connection',
'notion',
@ -546,7 +546,7 @@ describe('runKloConnection', () => {
),
).resolves.toBe(0);
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(yaml).toContain('crawl_mode: selected_roots');
expect(yaml).toContain('11111111-2222-3333-4444-555555555555');
expect(yaml).toContain('database-1');
@ -556,8 +556,8 @@ describe('runKloConnection', () => {
it('tests a configured connection through the native scan connector', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -576,7 +576,7 @@ describe('runKloConnection', () => {
const io = makeIo();
await expect(
runKloConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
runKtxConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
createScanConnector,
}),
).resolves.toBe(0);
@ -600,8 +600,8 @@ describe('runKloConnection', () => {
it('cleans up the native scan connector when connection testing fails', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await runKloConnection(
await initKtxProject({ projectDir, projectName: 'warehouse' });
await runKtxConnection(
{
command: 'add',
projectDir,
@ -616,7 +616,7 @@ describe('runKloConnection', () => {
makeIo().io,
);
const cleanup = vi.fn(async () => undefined);
const connector: KloScanConnector = {
const connector: KtxScanConnector = {
id: 'sqlite:warehouse',
driver: 'sqlite',
capabilities: {
@ -638,7 +638,7 @@ describe('runKloConnection', () => {
const io = makeIo();
await expect(
runKloConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
runKtxConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
createScanConnector: vi.fn(async () => connector),
}),
).resolves.toBe(1);

View file

@ -1,14 +1,14 @@
import { cancel, confirm, isCancel } from '@clack/prompts';
import { type KloLocalProject, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
import type { KloScanConnector } from '@klo/context/scan';
import type { KloConnectionMappingArgs } from './commands/connection-mapping.js';
import type { KloCliIo } from './index.js';
import { createKloCliScanConnector } from './local-scan-connectors.js';
import { type KtxLocalProject, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
import type { KtxScanConnector } from '@ktx/context/scan';
import type { KtxConnectionMappingArgs } from './commands/connection-mapping.js';
import type { KtxCliIo } from './index.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js';
import { profileMark } from './startup-profile.js';
profileMark('module:connection');
interface KloNotionConnectionCliConfig {
interface KtxNotionConnectionCliConfig {
authTokenRef: string;
crawlMode: 'all_accessible' | 'selected_roots';
rootPageIds: string[];
@ -19,9 +19,9 @@ interface KloNotionConnectionCliConfig {
maxKnowledgeUpdatesPerRun?: number;
}
type KloConnectionInputMode = 'disabled';
type KtxConnectionInputMode = 'disabled';
export type KloConnectionArgs =
export type KtxConnectionArgs =
| { command: 'list'; projectDir: string }
| {
command: 'add';
@ -33,7 +33,7 @@ export type KloConnectionArgs =
readonly: boolean;
force: boolean;
allowLiteralCredentials: boolean;
notion?: KloNotionConnectionCliConfig;
notion?: KtxNotionConnectionCliConfig;
}
| { command: 'test'; projectDir: string; connectionId: string }
| {
@ -41,7 +41,7 @@ export type KloConnectionArgs =
projectDir: string;
connectionId: string;
force: boolean;
inputMode?: KloConnectionInputMode;
inputMode?: KtxConnectionInputMode;
}
| {
command: 'map';
@ -50,19 +50,19 @@ export type KloConnectionArgs =
json: boolean;
};
interface KloConnectionPromptAdapter {
interface KtxConnectionPromptAdapter {
confirm(options: { message: string; initialValue?: boolean }): Promise<boolean>;
cancel(message: string): void;
}
interface KloConnectionIo extends KloCliIo {
interface KtxConnectionIo extends KtxCliIo {
stdin?: { isTTY?: boolean };
}
interface KloConnectionDeps {
createScanConnector?: typeof createKloCliScanConnector;
runMapping?: (argv: string[], io: KloCliIo) => Promise<number>;
prompts?: KloConnectionPromptAdapter;
interface KtxConnectionDeps {
createScanConnector?: typeof createKtxCliScanConnector;
runMapping?: (argv: string[], io: KtxCliIo) => Promise<number>;
prompts?: KtxConnectionPromptAdapter;
}
function assertSafeConnectionId(connectionId: string): void {
@ -76,10 +76,10 @@ function isCredentialReference(value: string): boolean {
}
function literalCredentialWarning(connectionId: string): string {
return `Warning: writing a literal credential URL to klo.yaml for connection "${connectionId}". Prefer env:NAME or file:/path references.`;
return `Warning: writing a literal credential URL to ktx.yaml for connection "${connectionId}". Prefer env:NAME or file:/path references.`;
}
function createClackConnectionPromptAdapter(): KloConnectionPromptAdapter {
function createClackConnectionPromptAdapter(): KtxConnectionPromptAdapter {
return {
async confirm(options: { message: string; initialValue?: boolean }): Promise<boolean> {
const value = await confirm(options);
@ -92,24 +92,24 @@ function createClackConnectionPromptAdapter(): KloConnectionPromptAdapter {
}
function isInteractiveConnectionIo(
args: Extract<KloConnectionArgs, { command: 'remove' }>,
io: KloConnectionIo,
args: Extract<KtxConnectionArgs, { command: 'remove' }>,
io: KtxConnectionIo,
): boolean {
return args.inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
}
async function cleanupConnector(connector: KloScanConnector | null): Promise<void> {
async function cleanupConnector(connector: KtxScanConnector | null): Promise<void> {
if (connector?.cleanup) {
await connector.cleanup();
}
}
async function testNativeConnection(
project: KloLocalProject,
project: KtxLocalProject,
connectionId: string,
createScanConnector: typeof createKloCliScanConnector,
createScanConnector: typeof createKtxCliScanConnector,
): Promise<{ driver: string; tableCount: number }> {
let connector: KloScanConnector | null = null;
let connector: KtxScanConnector | null = null;
try {
connector = await createScanConnector(project, connectionId);
const snapshot = await connector.introspect(
@ -131,7 +131,7 @@ async function testNativeConnection(
}
}
interface BufferedIo extends KloCliIo {
interface BufferedIo extends KtxCliIo {
stdoutText(): string;
stderrText(): string;
}
@ -167,17 +167,17 @@ function splitOutputLines(output: string): string[] {
}
async function runLowLevelMapping(
args: KloConnectionMappingArgs,
args: KtxConnectionMappingArgs,
argv: string[],
io: KloCliIo,
deps: KloConnectionDeps,
io: KtxCliIo,
deps: KtxConnectionDeps,
): Promise<number> {
if (deps.runMapping) {
return await deps.runMapping(argv, io);
}
const { runKloConnectionMapping } = await import('./commands/connection-mapping.js');
return await runKloConnectionMapping(args, io);
const { runKtxConnectionMapping } = await import('./commands/connection-mapping.js');
return await runKtxConnectionMapping(args, io);
}
function parseMappingListJson(output: string): unknown[] {
@ -190,12 +190,12 @@ function parseMappingListJson(output: string): unknown[] {
}
async function runPublicConnectionMap(
args: Extract<KloConnectionArgs, { command: 'map' }>,
io: KloCliIo,
deps: KloConnectionDeps,
args: Extract<KtxConnectionArgs, { command: 'map' }>,
io: KtxCliIo,
deps: KtxConnectionDeps,
): Promise<number> {
const refreshIo = createBufferedIo();
const refreshArgs: KloConnectionMappingArgs = {
const refreshArgs: KtxConnectionMappingArgs = {
command: 'refresh',
projectDir: args.projectDir,
connectionId: args.sourceConnectionId,
@ -217,7 +217,7 @@ async function runPublicConnectionMap(
}
const validationIo = createBufferedIo();
const validationArgs: KloConnectionMappingArgs = {
const validationArgs: KtxConnectionMappingArgs = {
command: 'validate',
projectDir: args.projectDir,
connectionId: args.sourceConnectionId,
@ -237,7 +237,7 @@ async function runPublicConnectionMap(
const listIo = createBufferedIo();
const listArgv = ['list', args.sourceConnectionId, '--project-dir', args.projectDir];
const listArgs: KloConnectionMappingArgs = {
const listArgs: KtxConnectionMappingArgs = {
command: 'list',
projectDir: args.projectDir,
connectionId: args.sourceConnectionId,
@ -271,26 +271,26 @@ async function runPublicConnectionMap(
io.stdout.write('\nMappings:\n');
io.stdout.write(listIo.stdoutText().trim() ? listIo.stdoutText() : 'No mappings found.\n');
io.stdout.write('\nNext:\n');
io.stdout.write(` klo ingest ${args.sourceConnectionId}\n`);
io.stdout.write(` klo dev mapping list ${args.sourceConnectionId}\n`);
io.stdout.write(` ktx ingest ${args.sourceConnectionId}\n`);
io.stdout.write(` ktx dev mapping list ${args.sourceConnectionId}\n`);
return 0;
}
export async function runKloConnection(
args: KloConnectionArgs,
io: KloConnectionIo = process,
deps: KloConnectionDeps = {},
export async function runKtxConnection(
args: KtxConnectionArgs,
io: KtxConnectionIo = process,
deps: KtxConnectionDeps = {},
): Promise<number> {
try {
if (args.command === 'map') {
return await runPublicConnectionMap(args, io, deps);
}
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
if (args.command === 'list') {
const entries = Object.entries(project.config.connections).sort(([a], [b]) => a.localeCompare(b));
if (entries.length === 0) {
io.stdout.write('No connections configured. Run `klo connection add <id> --driver <driver>` to add one.\n');
io.stdout.write('No connections configured. Run `ktx connection add <id> --driver <driver>` to add one.\n');
return 0;
}
const idWidth = Math.max('ID'.length, ...entries.map(([id]) => id.length));
@ -348,11 +348,11 @@ export async function runKloConnection(
},
};
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(nextConfig),
'klo',
'klo@example.com',
`Update KLO connection: ${args.connectionId}`,
'ktx.yaml',
serializeKtxProjectConfig(nextConfig),
'ktx',
'ktx@example.com',
`Update KTX connection: ${args.connectionId}`,
);
io.stdout.write(`Connection: ${args.connectionId}\n`);
io.stdout.write(`Driver: ${args.driver}\n`);
@ -361,7 +361,7 @@ export async function runKloConnection(
if (args.command === 'remove') {
if (!project.config.connections[args.connectionId]) {
throw new Error(`Connection "${args.connectionId}" is not configured in klo.yaml`);
throw new Error(`Connection "${args.connectionId}" is not configured in ktx.yaml`);
}
if (!args.force) {
@ -373,7 +373,7 @@ export async function runKloConnection(
const prompts = deps.prompts ?? createClackConnectionPromptAdapter();
const confirmed = await prompts.confirm({
message: `Remove connection "${args.connectionId}" from klo.yaml? Ingested artifacts will remain in .klo/.`,
message: `Remove connection "${args.connectionId}" from ktx.yaml? Ingested artifacts will remain in .ktx/.`,
initialValue: false,
});
if (!confirmed) {
@ -388,21 +388,21 @@ export async function runKloConnection(
connections,
};
await project.fileStore.writeFile(
'klo.yaml',
serializeKloProjectConfig(nextConfig),
'klo',
'klo@example.com',
`Remove KLO connection: ${args.connectionId}`,
'ktx.yaml',
serializeKtxProjectConfig(nextConfig),
'ktx',
'ktx@example.com',
`Remove KTX connection: ${args.connectionId}`,
);
io.stdout.write('Connection removed from klo.yaml.\n');
io.stdout.write('Ingested artifacts from this connection remain in .klo/. Run klo dev artifacts to inspect.\n');
io.stdout.write('Connection removed from ktx.yaml.\n');
io.stdout.write('Ingested artifacts from this connection remain in .ktx/. Run ktx dev artifacts to inspect.\n');
return 0;
}
const result = await testNativeConnection(
project,
args.connectionId,
deps.createScanConnector ?? createKloCliScanConnector,
deps.createScanConnector ?? createKtxCliScanConnector,
);
io.stdout.write(`Connection test passed: ${args.connectionId}\n`);
io.stdout.write(`Driver: ${result.driver}\n`);

View file

@ -1,6 +1,6 @@
import { buildDefaultKloProjectConfig, type KloProjectConfig } from '@klo/context/project';
import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '@ktx/context/project';
import { describe, expect, it, vi } from 'vitest';
import type { KloPublicIngestProject, KloPublicIngestTargetResult } from './public-ingest.js';
import type { KtxPublicIngestProject, KtxPublicIngestTargetResult } from './public-ingest.js';
import {
extractProgressMessage,
initViewState,
@ -32,17 +32,17 @@ function makeIo(options: { isTTY?: boolean } = {}) {
};
}
function projectWithConnections(connections: KloProjectConfig['connections']): KloPublicIngestProject {
function projectWithConnections(connections: KtxProjectConfig['connections']): KtxPublicIngestProject {
return {
projectDir: '/tmp/project',
config: {
...buildDefaultKloProjectConfig('warehouse'),
...buildDefaultKtxProjectConfig('warehouse'),
connections,
},
};
}
function successResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KloPublicIngestTargetResult {
function successResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KtxPublicIngestTargetResult {
return {
connectionId,
driver,
@ -55,7 +55,7 @@ function successResult(connectionId: string, driver: string, operation: 'scan' |
};
}
function failedResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KloPublicIngestTargetResult {
function failedResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KtxPublicIngestTargetResult {
return {
connectionId,
driver,
@ -78,7 +78,7 @@ describe('extractProgressMessage', () => {
});
it('returns null for non-progress output', () => {
expect(extractProgressMessage('KLO scan completed\n')).toBeNull();
expect(extractProgressMessage('KTX scan completed\n')).toBeNull();
});
});
@ -137,7 +137,7 @@ describe('renderContextBuildView', () => {
]);
const output = renderContextBuildView(state, { styled: false });
expect(output).toContain('Building KLO context');
expect(output).toContain('Building KTX context');
expect(output).toContain('Primary sources:');
expect(output).toContain('warehouse');
expect(output).toContain('queued');
@ -237,7 +237,7 @@ describe('runContextBuild', () => {
);
const output = io.stdout();
expect(output).toContain('Building KLO context');
expect(output).toContain('Building KTX context');
expect(output).toContain('Primary sources:');
expect(output).toContain('warehouse');
expect(output).toContain('Context sources:');
@ -297,7 +297,7 @@ describe('runContextBuild', () => {
expect(mockExit).toHaveBeenCalledWith(0);
expect(io.stdout()).toContain('Context build continuing in the background.');
expect(io.stdout()).toContain('Resume: klo setup --project-dir /tmp/project');
expect(io.stdout()).toContain('Resume: ktx setup --project-dir /tmp/project');
mockExit.mockRestore();
});
});

View file

@ -1,12 +1,12 @@
import { spawn } from 'node:child_process';
import { mkdirSync, openSync } from 'node:fs';
import { join, resolve } from 'node:path';
import type { KloCliIo } from './index.js';
import type { KtxCliIo } from './index.js';
import type {
KloPublicIngestArgs,
KloPublicIngestPlanTarget,
KloPublicIngestProject,
KloPublicIngestTargetResult,
KtxPublicIngestArgs,
KtxPublicIngestPlanTarget,
KtxPublicIngestProject,
KtxPublicIngestTargetResult,
} from './public-ingest.js';
import { buildPublicIngestPlan, executePublicIngestTarget } from './public-ingest.js';
import { formatDuration } from './demo-metrics.js';
@ -18,7 +18,7 @@ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧',
const ESC = String.fromCharCode(0x1b);
export interface ContextBuildTargetState {
target: KloPublicIngestPlanTarget;
target: KtxPublicIngestPlanTarget;
status: 'queued' | 'running' | 'done' | 'failed';
detailLine: string | null;
summaryText: string | null;
@ -131,7 +131,7 @@ function renderTargetGroup(
}
function resumeCommand(projectDir?: string): string {
return projectDir ? `klo setup --project-dir ${projectDir}` : 'klo setup';
return projectDir ? `ktx setup --project-dir ${projectDir}` : 'ktx setup';
}
export function renderContextBuildView(
@ -142,7 +142,7 @@ export function renderContextBuildView(
const width = columnWidth(state);
const lines: string[] = [
'',
'Building KLO context',
'Building KTX context',
'─────────────────────',
...renderTargetGroup('Primary sources', state.primarySources, state.frame, styled, width),
...renderTargetGroup('Context sources', state.contextSources, state.frame, styled, width),
@ -184,7 +184,7 @@ export function parseIngestSummary(output: string): string | null {
}
interface CapturedIo {
io: KloCliIo;
io: KtxCliIo;
captured(): string;
}
@ -212,7 +212,7 @@ function createCaptureIo(onProgress: (message: string) => void, isTTY: boolean):
// --- Repaint ---
function createRepainter(io: KloCliIo) {
function createRepainter(io: KtxCliIo) {
let lastLineCount = 0;
return {
@ -229,7 +229,7 @@ function createRepainter(io: KloCliIo) {
// --- Background build ---
function resolveKloEntryScript(): string | null {
function resolveKtxEntryScript(): string | null {
const argv1 = process.argv[1];
if (argv1 && (argv1.endsWith('.js') || argv1.endsWith('.ts') || argv1.endsWith('.mjs'))) {
return argv1;
@ -238,11 +238,11 @@ function resolveKloEntryScript(): string | null {
}
function spawnBackgroundBuild(projectDir: string): { logPath: string } | null {
const entryScript = resolveKloEntryScript();
const entryScript = resolveKtxEntryScript();
if (!entryScript) return null;
const resolvedDir = resolve(projectDir);
const logDir = join(resolvedDir, '.klo', 'setup');
const logDir = join(resolvedDir, '.ktx', 'setup');
mkdirSync(logDir, { recursive: true });
const logPath = join(logDir, 'context-build.log');
const logFd = openSync(logPath, 'w');
@ -280,11 +280,11 @@ function defaultSetupKeystroke(onDetach: () => void, onCtrlC: () => void): (() =
// --- Orchestration ---
function makeTargetState(target: KloPublicIngestPlanTarget): ContextBuildTargetState {
function makeTargetState(target: KtxPublicIngestPlanTarget): ContextBuildTargetState {
return { target, status: 'queued', detailLine: null, summaryText: null, startedAt: null, elapsedMs: 0 };
}
export function initViewState(targets: KloPublicIngestPlanTarget[]): ContextBuildViewState {
export function initViewState(targets: KtxPublicIngestPlanTarget[]): ContextBuildViewState {
return {
primarySources: targets.filter((t) => t.operation === 'scan').map(makeTargetState),
contextSources: targets.filter((t) => t.operation === 'source-ingest').map(makeTargetState),
@ -293,9 +293,9 @@ export function initViewState(targets: KloPublicIngestPlanTarget[]): ContextBuil
}
export async function runContextBuild(
project: KloPublicIngestProject,
project: KtxPublicIngestProject,
args: ContextBuildArgs,
io: KloCliIo,
io: KtxCliIo,
deps: ContextBuildDeps = {},
): Promise<ContextBuildResult> {
const plan = buildPublicIngestPlan(project, { projectDir: args.projectDir, all: true });
@ -339,7 +339,7 @@ export async function runContextBuild(
const bg = spawnBackgroundBuild(args.projectDir);
io.stdout.write('\n\nContext build continuing in the background.\n');
if (bg) io.stdout.write(`Log: ${bg.logPath}\n`);
io.stdout.write(`Status: klo setup context status --project-dir ${resolve(args.projectDir)}\n`);
io.stdout.write(`Status: ktx setup context status --project-dir ${resolve(args.projectDir)}\n`);
io.stdout.write(`Resume: ${resumeCommand(args.projectDir)}\n`);
process.exit(0);
},
@ -351,7 +351,7 @@ export async function runContextBuild(
},
);
}
const runArgs: Extract<KloPublicIngestArgs, { command: 'run' }> = {
const runArgs: Extract<KtxPublicIngestArgs, { command: 'run' }> = {
command: 'run',
projectDir: args.projectDir,
all: true,

View file

@ -28,7 +28,7 @@ async function readPackagedJson<T>(relativePath: string): Promise<T> {
}
describe('demo assets', () => {
const projectDir = join(tmpdir(), `klo-demo-assets-${process.pid}`);
const projectDir = join(tmpdir(), `ktx-demo-assets-${process.pid}`);
afterEach(async () => {
await rm(projectDir, { recursive: true, force: true });
@ -36,8 +36,8 @@ describe('demo assets', () => {
it('resolves the default demo root under the OS temp directory', () => {
const dir = defaultDemoProjectDir();
expect(dir.startsWith(join(tmpdir(), 'klo-demo-'))).toBe(true);
expect(dir).toMatch(/klo-demo-[a-f0-9]{8}$/);
expect(dir.startsWith(join(tmpdir(), 'ktx-demo-'))).toBe(true);
expect(dir).toMatch(/ktx-demo-[a-f0-9]{8}$/);
});
it('exports the packaged Orbit demo identity', () => {
@ -124,7 +124,7 @@ describe('demo assets', () => {
await expect(access(join(projectDir, 'raw-sources'))).resolves.toBeUndefined();
await expect(access(join(projectDir, '_schema'))).rejects.toMatchObject({ code: 'ENOENT' });
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('backend: anthropic');
expect(config).toContain('api_key: env:ANTHROPIC_API_KEY');
expect(config).not.toContain('sk-ant-');
@ -187,7 +187,7 @@ describe('demo assets', () => {
await expect(inspectDemoProjectState(projectDir)).resolves.toEqual({
status: 'missing',
projectDir,
missing: ['klo.yaml', 'demo.db', 'state.sqlite', 'replays/replay.memory-flow.v1.json'],
missing: ['ktx.yaml', 'demo.db', 'state.sqlite', 'replays/replay.memory-flow.v1.json'],
});
await ensureDemoProject({ projectDir, force: false });
@ -210,7 +210,7 @@ describe('demo assets', () => {
await rm(join(projectDir, 'demo.db'), { force: true });
await expect(resetDemoProject({ projectDir, force: false })).rejects.toThrow(
`klo setup demo reset is destructive; pass --force to recreate ${projectDir}`,
`ktx setup demo reset is destructive; pass --force to recreate ${projectDir}`,
);
await expect(resetDemoProject({ projectDir, force: true })).resolves.toMatchObject({ projectDir });
@ -218,10 +218,10 @@ describe('demo assets', () => {
await expect(inspectDemoProjectState(projectDir)).resolves.toMatchObject({ status: 'ready' });
});
it('preserves a user-edited klo.yaml across reset --force', async () => {
it('preserves a user-edited ktx.yaml across reset --force', async () => {
await ensureDemoProject({ projectDir, force: false });
const customConfig = [
'project: klo-demo-orbit',
'project: ktx-demo-orbit',
'connections:',
` ${DEMO_CONNECTION_ID}:`,
' driver: sqlite',
@ -232,7 +232,7 @@ describe('demo assets', () => {
' search: sqlite-fts5',
' git:',
' auto_commit: true',
' author: klo <klo@example.com>',
' author: ktx <ktx@example.com>',
'llm:',
' provider:',
' backend: vertex',
@ -253,20 +253,20 @@ describe('demo assets', () => {
' failureMode: continue',
'',
].join('\n');
await writeFile(join(projectDir, 'klo.yaml'), customConfig, 'utf-8');
await writeFile(join(projectDir, 'ktx.yaml'), customConfig, 'utf-8');
await resetDemoProject({ projectDir, force: true });
const preserved = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const preserved = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(preserved).toBe(customConfig);
expect(preserved).toContain('backend: vertex');
expect(preserved).not.toContain('backend: anthropic');
await expect(inspectDemoProjectState(projectDir)).resolves.toMatchObject({ status: 'ready' });
});
it('still writes the default klo.yaml on reset when none exists', async () => {
it('still writes the default ktx.yaml on reset when none exists', async () => {
await resetDemoProject({ projectDir, force: true });
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('backend: anthropic');
});
});

View file

@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { randomBytes } from 'node:crypto';
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { loadDemoReplayFile, loadLatestDemoReplay } from './demo-replay-store.js';
interface DemoProjectResult {
@ -33,7 +33,7 @@ export const DEMO_REPLAY_FILE = 'replay.memory-flow.v1.json';
export const DEMO_FULL_JOB_ID = 'demo-full-ingest';
const REQUIRED_BASE_PROJECT_PATHS = [
'klo.yaml',
'ktx.yaml',
'demo.db',
'state.sqlite',
join('replays', DEMO_REPLAY_FILE),
@ -70,7 +70,7 @@ async function exists(path: string): Promise<boolean> {
export function defaultDemoProjectDir(): string {
const suffix = randomBytes(4).toString('hex');
return join(tmpdir(), `klo-demo-${suffix}`);
return join(tmpdir(), `ktx-demo-${suffix}`);
}
export async function inspectDemoProjectState(projectDir: string): Promise<DemoProjectState> {
@ -97,10 +97,10 @@ export async function inspectDemoProjectState(projectDir: string): Promise<DemoP
export async function resetDemoProject(options: EnsureDemoProjectOptions): Promise<DemoProjectResult> {
const projectDir = resolve(options.projectDir);
if (!options.force) {
throw new Error(`klo setup demo reset is destructive; pass --force to recreate ${projectDir}`);
throw new Error(`ktx setup demo reset is destructive; pass --force to recreate ${projectDir}`);
}
const preservedConfig = await readExistingConfig(join(projectDir, 'klo.yaml'));
const preservedConfig = await readExistingConfig(join(projectDir, 'ktx.yaml'));
const result = await ensureDemoProject({ projectDir, force: true });
if (preservedConfig !== null) {
await writeFile(result.configPath, preservedConfig, 'utf-8');
@ -118,7 +118,7 @@ async function readExistingConfig(configPath: string): Promise<string | null> {
function demoConfig(databasePath: string): string {
return [
'project: klo-demo-orbit',
'project: ktx-demo-orbit',
'connections:',
` ${DEMO_CONNECTION_ID}:`,
' driver: sqlite',
@ -129,7 +129,7 @@ function demoConfig(databasePath: string): string {
' search: sqlite-fts5',
' git:',
' auto_commit: true',
' author: klo <klo@example.com>',
' author: ktx <ktx@example.com>',
'llm:',
' provider:',
' backend: anthropic',
@ -185,7 +185,7 @@ async function assertPackagedSeededAssetsPresent(): Promise<void> {
export async function ensureDemoProject(options: EnsureDemoProjectOptions): Promise<DemoProjectResult> {
const projectDir = resolve(options.projectDir);
const configPath = join(projectDir, 'klo.yaml');
const configPath = join(projectDir, 'ktx.yaml');
if (!options.force && (await exists(configPath))) {
throw new Error(`Demo project already exists at ${projectDir}; pass --force to recreate it`);
}
@ -237,7 +237,7 @@ export async function ensureSeededDemoProject(options: EnsureDemoProjectOptions)
if (!options.force && error instanceof Error && error.message.includes('Demo project already exists')) {
return {
projectDir,
configPath: join(projectDir, 'klo.yaml'),
configPath: join(projectDir, 'ktx.yaml'),
databasePath: join(projectDir, 'demo.db'),
replayPath: join(projectDir, 'replays', DEMO_REPLAY_FILE),
};

View file

@ -1,7 +1,7 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { IngestReportSnapshot, LocalIngestResult, RunLocalIngestOptions } from '@klo/context/ingest';
import type { IngestReportSnapshot, LocalIngestResult, RunLocalIngestOptions } from '@ktx/context/ingest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
import {
@ -69,7 +69,7 @@ describe('full demo helpers', () => {
let projectDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-full-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-full-'));
projectDir = join(tempDir, 'demo');
await ensureDemoProject({ projectDir, force: false });
});
@ -79,18 +79,18 @@ describe('full demo helpers', () => {
});
it('fails full mode with exact Anthropic env guidance when the key is missing', async () => {
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
expect(() => assertFullDemoCredentials(project, {})).toThrow(
'klo setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `klo setup demo --mode full --no-input`, or run `klo setup demo --mode seeded --no-input` without credentials.',
'ktx setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `ktx setup demo --mode full --no-input`, or run `ktx setup demo --mode seeded --no-input` without credentials.',
);
});
it('respects an existing gateway provider project for full mode', async () => {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: klo-demo-orbit',
'project: ktx-demo-orbit',
'connections:',
' orbit_demo:',
' driver: sqlite',
@ -109,22 +109,22 @@ describe('full demo helpers', () => {
].join('\n'),
'utf-8',
);
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
expect(() => assertFullDemoCredentials(project, {})).not.toThrow();
expect(fullDemoCredentialStatus(project, {})).toEqual({ status: 'ready' });
});
it('reports full-demo credential status without throwing', async () => {
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
expect(fullDemoCredentialStatus(project, {})).toEqual({ status: 'missing-anthropic-key' });
expect(fullDemoCredentialStatus(project, { ANTHROPIC_API_KEY: 'sk-ant-test' })).toEqual({ status: 'ready' }); // pragma: allowlist secret
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: klo-demo-orbit',
'project: ktx-demo-orbit',
'connections:',
' orbit_demo:',
' driver: sqlite',
@ -136,7 +136,7 @@ describe('full demo helpers', () => {
].join('\n'),
'utf-8',
);
const disabledProject = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
const disabledProject = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
expect(fullDemoCredentialStatus(disabledProject, {})).toEqual({ status: 'unsupported-provider', provider: 'none' });
});
@ -192,9 +192,9 @@ describe('full demo helpers', () => {
expect(summary).toContain('Full demo ingest: done');
expect(summary).toContain('Saved memory: 1 wiki, 1 semantic layer');
expect(summary).toContain('Provenance rows: 2');
expect(summary).toContain('Next: klo setup demo inspect');
expect(summary).toContain('Shows the files, semantic-layer sources, and memory KLO just produced.');
expect(summary).toContain('Next: klo setup demo replay');
expect(summary).toContain('Next: ktx setup demo inspect');
expect(summary).toContain('Shows the files, semantic-layer sources, and memory KTX just produced.');
expect(summary).toContain('Next: ktx setup demo replay');
expect(summary).toContain('Replays the same visual story without calling the LLM again.');
expect(summary).not.toContain('--viz');
});

View file

@ -1,4 +1,4 @@
import { resolveKloConfigReference } from '@klo/context/core';
import { resolveKtxConfigReference } from '@ktx/context/core';
import {
createMemoryFlowLiveBuffer,
ingestReportToMemoryFlowReplay,
@ -7,12 +7,12 @@ import {
type LocalIngestResult,
type MemoryFlowReplayInput,
type RunLocalIngestOptions,
} from '@klo/context/ingest';
import { loadKloProject, type KloLocalProject } from '@klo/context/project';
import { runLocalScan, type LocalScanRunResult } from '@klo/context/scan';
} from '@ktx/context/ingest';
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import { runLocalScan, type LocalScanRunResult } from '@ktx/context/scan';
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
import { runDemoScan } from './demo-scan.js';
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
import { formatNextStepLines } from './next-steps.js';
interface DemoFullOptions {
@ -24,7 +24,7 @@ interface DemoFullOptions {
}
export interface DemoFullResult {
project: KloLocalProject;
project: KtxLocalProject;
scan: LocalScanRunResult;
ingest: LocalIngestResult;
report: IngestReportSnapshot;
@ -54,7 +54,7 @@ function savedCounts(report: IngestReportSnapshot): { wikiCount: number; slCount
}
export function fullDemoCredentialStatus(
project: KloLocalProject,
project: KtxLocalProject,
env: NodeJS.ProcessEnv = process.env,
): FullDemoCredentialStatus {
const llm = project.config.llm;
@ -62,14 +62,14 @@ export function fullDemoCredentialStatus(
return { status: 'unsupported-provider', provider: llm.provider.backend };
}
if (llm.provider.backend === 'anthropic' && !resolveKloConfigReference(llm.provider.anthropic?.api_key, env)) {
if (llm.provider.backend === 'anthropic' && !resolveKtxConfigReference(llm.provider.anthropic?.api_key, env)) {
return { status: 'missing-anthropic-key' };
}
return { status: 'ready' };
}
export function assertFullDemoCredentials(project: KloLocalProject, env: NodeJS.ProcessEnv = process.env): void {
export function assertFullDemoCredentials(project: KtxLocalProject, env: NodeJS.ProcessEnv = process.env): void {
const llm = project.config.llm;
const status = fullDemoCredentialStatus(project, env);
if (status.status === 'ready') {
@ -78,13 +78,13 @@ export function assertFullDemoCredentials(project: KloLocalProject, env: NodeJS.
if (status.status === 'unsupported-provider') {
throw new Error(
'klo setup demo --mode full requires llm.provider.backend: anthropic, vertex, or gateway. Run `klo setup demo init --force --no-input` to recreate the demo config, or run `klo setup demo --mode seeded --no-input` without credentials.',
'ktx setup demo --mode full requires llm.provider.backend: anthropic, vertex, or gateway. Run `ktx setup demo init --force --no-input` to recreate the demo config, or run `ktx setup demo --mode seeded --no-input` without credentials.',
);
}
if (llm.provider.backend === 'anthropic') {
throw new Error(
'klo setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `klo setup demo --mode full --no-input`, or run `klo setup demo --mode seeded --no-input` without credentials.',
'ktx setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `ktx setup demo --mode full --no-input`, or run `ktx setup demo --mode seeded --no-input` without credentials.',
);
}
}
@ -110,7 +110,7 @@ function initialFullReplay(projectDir: string): MemoryFlowReplayInput {
export async function runDemoFull(options: DemoFullOptions): Promise<DemoFullResult> {
await ensureDemoProjectForReuse(options.projectDir);
const project = await loadKloProject({ projectDir: options.projectDir });
const project = await loadKtxProject({ projectDir: options.projectDir });
assertFullDemoCredentials(project, options.env);
const { result: scan } = await runDemoScan({
@ -125,7 +125,7 @@ export async function runDemoFull(options: DemoFullOptions): Promise<DemoFullRes
const executeLocalIngest = options.runLocalIngest ?? runLocalIngest;
const ingest = await executeLocalIngest({
project,
adapters: createKloCliLocalIngestAdapters(project),
adapters: createKtxCliLocalIngestAdapters(project),
adapter: DEMO_ADAPTER,
connectionId: DEMO_CONNECTION_ID,
trigger: 'manual_resync',
@ -152,9 +152,9 @@ export function formatFullDemoSummary(report: IngestReportSnapshot): string {
`Sync: ${report.body.syncId}`,
`Saved memory: ${counts.wikiCount} wiki, ${counts.slCount} semantic layer`,
`Provenance rows: ${report.body.provenanceRows.length}`,
'Next: klo setup demo inspect',
' Shows the files, semantic-layer sources, and memory KLO just produced.',
'Next: klo setup demo replay',
'Next: ktx setup demo inspect',
' Shows the files, semantic-layer sources, and memory KTX just produced.',
'Next: ktx setup demo replay',
' Replays the same visual story without calling the LLM again.',
'',
].join('\n');
@ -176,7 +176,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
const conflictCount = report.body.conflictsResolved.length;
const areasAnalyzed = workUnits.filter((wu) => wu.actions.length > 0).length;
const lines: string[] = ['', '★ KLO finished ingesting your data', ''];
const lines: string[] = ['', '★ KTX finished ingesting your data', ''];
if (areasAnalyzed > 0) {
lines.push(` ✓ Analyzed ${areasAnalyzed} business area${areasAnalyzed === 1 ? '' : 's'}`);
@ -187,7 +187,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
lines.push('');
if (counts.slCount > 0 || counts.wikiCount > 0) {
lines.push(' KLO created:');
lines.push(' KTX created:');
if (counts.slCount > 0) lines.push(` 📊 ${counts.slCount} query definition${counts.slCount === 1 ? '' : 's'} — so agents can write accurate SQL for your data`);
if (counts.wikiCount > 0) lines.push(` 📝 ${counts.wikiCount} knowledge page${counts.wikiCount === 1 ? '' : 's'} — so agents understand your business context`);
lines.push('');
@ -206,7 +206,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
lines.push(' What to do next:');
lines.push(...formatNextStepLines());
lines.push('');
lines.push(` Your KLO project files are at: ${projectDir}`);
lines.push(` Your KTX project files are at: ${projectDir}`);
lines.push('');
return lines.join('\n');

View file

@ -21,7 +21,7 @@ describe('demo interaction decisions', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-interaction-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-interaction-'));
});
afterEach(async () => {
@ -53,7 +53,7 @@ describe('demo interaction decisions', () => {
prompts: createTestDemoPromptAdapter({ choices: [] }),
}),
).rejects.toThrow(
`Demo project is not ready at ${tempDir}: missing demo.db. Run klo setup demo reset --project-dir ${tempDir} --force --no-input`,
`Demo project is not ready at ${tempDir}: missing demo.db. Run ktx setup demo reset --project-dir ${tempDir} --force --no-input`,
);
});

View file

@ -2,7 +2,7 @@ import { cancel, confirm, isCancel, password, select, text } from '@clack/prompt
import type { Option as ClackOption } from '@clack/prompts';
import { resolve } from 'node:path';
import { inspectDemoProjectState } from './demo-assets.js';
import type { KloDemoInputMode } from './demo.js';
import type { KtxDemoInputMode } from './demo.js';
import { withMenuOptionsSpacing } from './prompt-navigation.js';
type DemoPromptOption<T extends string> = ClackOption<T>;
@ -29,7 +29,7 @@ type FullCredentialDecision =
| { action: 'run-mode'; mode: 'seeded' | 'replay' }
| { action: 'cancel' };
function isInteractive(inputMode: KloDemoInputMode | undefined, io: DemoInteractiveIo): boolean {
function isInteractive(inputMode: KtxDemoInputMode | undefined, io: DemoInteractiveIo): boolean {
return inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
}
@ -97,7 +97,7 @@ export function createTestDemoPromptAdapter(options: {
export async function chooseDemoProjectForInteractiveRun(options: {
projectDir: string;
inputMode?: KloDemoInputMode;
inputMode?: KtxDemoInputMode;
io: DemoInteractiveIo;
prompts?: DemoPromptAdapter;
}): Promise<DemoProjectDecision> {
@ -108,7 +108,7 @@ export async function chooseDemoProjectForInteractiveRun(options: {
if (!isInteractive(options.inputMode, options.io)) {
if (state.status === 'corrupt') {
throw new Error(
`Demo project is not ready at ${projectDir}: missing ${state.missing.join(', ')}. Run klo setup demo reset --project-dir ${projectDir} --force --no-input`,
`Demo project is not ready at ${projectDir}: missing ${state.missing.join(', ')}. Run ktx setup demo reset --project-dir ${projectDir} --force --no-input`,
);
}
return { action: 'use', projectDir, reset: false };
@ -163,7 +163,7 @@ export async function chooseDemoProjectForInteractiveRun(options: {
export async function resolveFullCredentialDecision(options: {
needsAnthropicKey: boolean;
inputMode?: KloDemoInputMode;
inputMode?: KtxDemoInputMode;
io: DemoInteractiveIo;
env: NodeJS.ProcessEnv;
prompts?: DemoPromptAdapter;

View file

@ -1,4 +1,4 @@
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { describe, expect, it } from 'vitest';
import {
buildDemoMetrics,

View file

@ -1,4 +1,4 @@
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
const DEFAULT_INPUT_TOKENS_PER_STEP = 4500;
const DEFAULT_OUTPUT_TOKENS_PER_STEP = 700;

View file

@ -1,4 +1,4 @@
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { describe, expect, it } from 'vitest';
import { createPlainProgressEmitter, formatMemoryFlowEventLine } from './demo-progress.js';

View file

@ -1,5 +1,5 @@
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { KloDemoIo } from './demo.js';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import type { KtxDemoIo } from './demo.js';
function plural(n: number, one: string, many = `${one}s`): string {
return `${n} ${n === 1 ? one : many}`;
@ -62,7 +62,7 @@ export function formatMemoryFlowEventLine(event: MemoryFlowEvent): string | null
}
}
export function createPlainProgressEmitter(io: KloDemoIo): (snapshot: MemoryFlowReplayInput) => void {
export function createPlainProgressEmitter(io: KtxDemoIo): (snapshot: MemoryFlowReplayInput) => void {
let printed = 0;
return (snapshot) => {
while (printed < snapshot.events.length) {

View file

@ -1,7 +1,7 @@
import { mkdtemp, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { type MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import { type MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { describe, expect, it } from 'vitest';
import { DEMO_LATEST_REPLAY_FILE, loadLatestDemoReplay, writeDemoReplay } from './demo-replay-store.js';
@ -35,7 +35,7 @@ function replay(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowRepla
describe('demo replay store', () => {
it('writes a versioned replay file and updates latest', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'klo-demo-replay-store-'));
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-demo-replay-store-'));
const saved = await writeDemoReplay(projectDir, replay(), { label: 'full' });
@ -53,7 +53,7 @@ describe('demo replay store', () => {
});
it('returns null when no latest local replay exists', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'klo-demo-replay-store-empty-'));
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-demo-replay-store-empty-'));
await expect(loadLatestDemoReplay(projectDir)).resolves.toBeNull();
});

View file

@ -1,7 +1,7 @@
import { constants as fsConstants } from 'node:fs';
import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { parseMemoryFlowReplayInput, type MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import { parseMemoryFlowReplayInput, type MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
interface StoredMemoryFlowReplayFile {
memoryFlowReplaySchemaVersion: 1;

View file

@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest';
import { findLatestDemoScanReport, runDemoScan } from './demo-scan.js';
describe('demo scan helpers', () => {
const projectDir = join(tmpdir(), `klo-demo-scan-${process.pid}`);
const projectDir = join(tmpdir(), `ktx-demo-scan-${process.pid}`);
afterEach(async () => {
await rm(projectDir, { recursive: true, force: true });

View file

@ -1,9 +1,9 @@
import { getLocalIngestStatus, type IngestReportSnapshot, type MemoryFlowReplayInput } from '@klo/context/ingest';
import { loadKloProject, type KloLocalProject } from '@klo/context/project';
import { runLocalScan, type KloScanReport, type LocalScanRunResult } from '@klo/context/scan';
import { getLocalIngestStatus, type IngestReportSnapshot, type MemoryFlowReplayInput } from '@ktx/context/ingest';
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import { runLocalScan, type KtxScanReport, type LocalScanRunResult } from '@ktx/context/scan';
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
import { loadLatestDemoReplay } from './demo-replay-store.js';
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
interface DemoScanOptions {
projectDir: string;
@ -13,13 +13,13 @@ interface DemoScanOptions {
}
interface DemoScanResult {
project: KloLocalProject;
project: KtxLocalProject;
result: LocalScanRunResult;
}
interface DemoInspectSummary {
projectDir: string;
scanReport: KloScanReport | null;
scanReport: KtxScanReport | null;
fullReport: IngestReportSnapshot | null;
semanticLayerFileCount: number;
knowledgeFileCount: number;
@ -28,7 +28,7 @@ interface DemoInspectSummary {
}
interface DemoInspectDeps {
findFullReport?: (project: KloLocalProject) => Promise<IngestReportSnapshot | null>;
findFullReport?: (project: KtxLocalProject) => Promise<IngestReportSnapshot | null>;
}
async function ensureDemoProjectForReuse(projectDir: string): Promise<void> {
@ -40,36 +40,36 @@ async function ensureDemoProjectForReuse(projectDir: string): Promise<void> {
});
}
async function loadReadyDemoProject(projectDir: string): Promise<KloLocalProject> {
async function loadReadyDemoProject(projectDir: string): Promise<KtxLocalProject> {
try {
return await loadKloProject({ projectDir });
return await loadKtxProject({ projectDir });
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(
`Demo project is not ready at ${projectDir}: ${reason}. Run klo setup demo init --project-dir ${projectDir} --force --no-input to recreate it.`,
`Demo project is not ready at ${projectDir}: ${reason}. Run ktx setup demo init --project-dir ${projectDir} --force --no-input to recreate it.`,
);
}
}
function reportDiff(report: KloScanReport): string {
function reportDiff(report: KtxScanReport): string {
return `+${report.diffSummary.tablesAdded}/~${report.diffSummary.tablesModified}/-${report.diffSummary.tablesDeleted}/=${report.diffSummary.tablesUnchanged}`;
}
function jsonReport(raw: string, path: string): KloScanReport {
function jsonReport(raw: string, path: string): KtxScanReport {
try {
return JSON.parse(raw) as KloScanReport;
return JSON.parse(raw) as KtxScanReport;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(`Invalid demo scan report at ${path}: ${reason}`);
}
}
async function countFiles(project: KloLocalProject, root: string, predicate: (path: string) => boolean): Promise<number> {
async function countFiles(project: KtxLocalProject, root: string, predicate: (path: string) => boolean): Promise<number> {
const { files } = await project.fileStore.listFiles(root, true);
return files.filter(predicate).length;
}
async function findFullDemoReport(project: KloLocalProject): Promise<IngestReportSnapshot | null> {
async function findFullDemoReport(project: KtxLocalProject): Promise<IngestReportSnapshot | null> {
return getLocalIngestStatus(project, DEMO_FULL_JOB_ID);
}
@ -92,13 +92,13 @@ export async function runDemoScan(options: DemoScanOptions): Promise<DemoScanRes
trigger: 'cli',
jobId: options.jobId ?? 'demo-scan',
now: options.now,
adapters: createKloCliLocalIngestAdapters(project),
adapters: createKtxCliLocalIngestAdapters(project),
});
return { project, result };
}
export async function findLatestDemoScanReport(projectDir: string): Promise<KloScanReport | null> {
export async function findLatestDemoScanReport(projectDir: string): Promise<KtxScanReport | null> {
const project = await loadReadyDemoProject(projectDir);
const root = `raw-sources/${DEMO_CONNECTION_ID}/${DEMO_ADAPTER}`;
const { files } = await project.fileStore.listFiles(root, true);
@ -117,7 +117,7 @@ export async function findLatestDemoScanReport(projectDir: string): Promise<KloS
export async function inspectDemoProject(
projectDir: string,
projectOverride?: KloLocalProject,
projectOverride?: KtxLocalProject,
deps: DemoInspectDeps = {},
): Promise<DemoInspectSummary> {
const project = projectOverride ?? (await loadReadyDemoProject(projectDir));
@ -143,7 +143,7 @@ export async function inspectDemoProject(
};
}
export function formatDemoScanSummary(report: KloScanReport): string {
export function formatDemoScanSummary(report: KtxScanReport): string {
return [
'Demo scan: done',
`Connection: ${report.connectionId}`,
@ -152,7 +152,7 @@ export function formatDemoScanSummary(report: KloScanReport): string {
`Tables: ${reportDiff(report)}`,
`Semantic-layer artifacts: ${report.artifactPaths.manifestShards.length}`,
`Report: ${report.artifactPaths.reportPath ?? 'none'}`,
'Next: klo setup demo inspect',
'Next: ktx setup demo inspect',
' Shows the files and semantic-layer draft created from the database scan.',
'',
].join('\n');
@ -190,22 +190,22 @@ export function formatDemoInspect(summary: DemoInspectSummary): string {
: [report ? 'Memory synthesis: full mode not run' : 'Memory synthesis: not run'];
const next = fullReport
? [
`Next: klo ingest watch ${fullReport.runId} --project-dir ${summary.projectDir}`,
`Next: ktx ingest watch ${fullReport.runId} --project-dir ${summary.projectDir}`,
' Opens the captured run timeline and lets you inspect what happened.',
'Next: klo setup demo replay',
'Next: ktx setup demo replay',
' Replays the same visual story without calling the LLM again.',
]
: report
? [
'Next: klo setup demo --mode full',
'Next: ktx setup demo --mode full',
' Runs the full AI-backed pass with your LLM provider.',
'Next: klo setup demo replay',
'Next: ktx setup demo replay',
' Replays the packaged visual story without calling the LLM.',
]
: [
'Next: klo setup demo --no-input',
'Next: ktx setup demo --no-input',
' Runs the pre-seeded demo without calling the LLM.',
'Next: klo setup demo --mode full',
'Next: ktx setup demo --mode full',
' Runs the full AI-backed pass with your LLM provider.',
];

View file

@ -4,10 +4,10 @@ import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { runDemoSeeded } from './demo-seeded.js';
import { formatSeededInspect, inspectSeededProject } from './demo-seeded-inspect.js';
import { KLO_NEXT_STEP_COMMANDS } from './next-steps.js';
import { KTX_NEXT_STEP_COMMANDS } from './next-steps.js';
describe('seeded demo inspect contract', () => {
const projectDir = join(tmpdir(), `klo-demo-seeded-inspect-${process.pid}`);
const projectDir = join(tmpdir(), `ktx-demo-seeded-inspect-${process.pid}`);
afterEach(async () => {
await rm(projectDir, { recursive: true, force: true });
@ -59,7 +59,7 @@ describe('seeded demo inspect contract', () => {
reports: { primaryPath: 'reports/seeded-demo-report.json', fileCount: 1 },
replays: { primaryPath: 'replays/replay.memory-flow.v1.json', latestPath: 'replays/latest.memory-flow.v1.json' },
},
nextCommands: KLO_NEXT_STEP_COMMANDS,
nextCommands: KTX_NEXT_STEP_COMMANDS,
});
expect(inspect.generatedOutputs.replays.fileCount).toBeGreaterThanOrEqual(3);
@ -89,13 +89,13 @@ describe('seeded demo inspect contract', () => {
expect(output).toContain('Report: reports/seeded-demo-report.json');
expect(output).toContain('Replay: replays/replay.memory-flow.v1.json');
expect(output).toContain('Latest replay: seeded (packaged, prebuilt)');
expect(output).toContain(' $ klo agent tools --json');
expect(output).toContain(' $ klo agent context --json');
expect(output).toContain(' $ klo serve --mcp stdio --user-id local');
expect(output.indexOf('klo agent tools --json')).toBeLessThan(
output.indexOf('klo serve --mcp stdio --user-id local'),
expect(output).toContain(' $ ktx agent tools --json');
expect(output).toContain(' $ ktx agent context --json');
expect(output).toContain(' $ ktx serve --mcp stdio --user-id local');
expect(output.indexOf('ktx agent tools --json')).toBeLessThan(
output.indexOf('ktx serve --mcp stdio --user-id local'),
);
expect(output).not.toContain('klo ask');
expect(output).not.toContain('ktx ask');
expect(output).not.toContain('deterministic mode');
});

View file

@ -1,10 +1,10 @@
import { constants as fsConstants } from 'node:fs';
import { access, readFile, readdir } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { loadPackagedDemoReplay } from './demo-assets.js';
import { DEMO_LATEST_REPLAY_FILE, loadLatestDemoReplay } from './demo-replay-store.js';
import { KLO_NEXT_STEP_COMMANDS, KLO_NEXT_STEP_COMMAND_WIDTH } from './next-steps.js';
import { KTX_NEXT_STEP_COMMANDS, KTX_NEXT_STEP_COMMAND_WIDTH } from './next-steps.js';
type SeededInspectReadiness = 'missing' | 'ready' | 'corrupt';
@ -66,7 +66,7 @@ export interface SeededInspectSummary {
}
const REQUIRED_SEEDED_PROJECT_PATHS = [
'klo.yaml',
'ktx.yaml',
'demo.db',
'state.sqlite',
'manifest.json',
@ -181,7 +181,7 @@ function sourceBundleFromManifest(manifest: DemoSeededManifest): SeededInspectSu
}
function nextCommands(): SeededInspectSummary['nextCommands'] {
return [...KLO_NEXT_STEP_COMMANDS];
return [...KTX_NEXT_STEP_COMMANDS];
}
function modeMetadataFromReplay(replay: MemoryFlowReplayInput | null): SeededInspectSummary['modeMetadata'] {
@ -291,9 +291,9 @@ export function formatSeededInspect(summary: SeededInspectSummary): string {
);
for (const command of summary.nextCommands) {
lines.push(` $ ${command.command.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} ${command.description}`);
lines.push(` $ ${command.command.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} ${command.description}`);
}
lines.push('', `Your KLO project files are at: ${summary.projectDir}`, '');
lines.push('', `Your KTX project files are at: ${summary.projectDir}`, '');
return lines.join('\n');
}

View file

@ -6,7 +6,7 @@ import { ensureSeededDemoProject } from './demo-assets.js';
import { runDemoSeeded } from './demo-seeded.js';
describe('demo seeded mode', () => {
const projectDir = join(tmpdir(), `klo-demo-seeded-${process.pid}`);
const projectDir = join(tmpdir(), `ktx-demo-seeded-${process.pid}`);
afterEach(async () => {
await rm(projectDir, { recursive: true, force: true });
@ -17,7 +17,7 @@ describe('demo seeded mode', () => {
expect(result.projectDir).toBe(projectDir);
await expect(access(join(projectDir, 'demo.db'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'klo.yaml'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'ktx.yaml'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'manifest.json'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'semantic-layer/orbit_demo/accounts.yaml'))).resolves.toBeUndefined();
await expect(access(join(projectDir, 'knowledge/global/arr-contract-first.md'))).resolves.toBeUndefined();
@ -35,7 +35,7 @@ describe('demo seeded mode', () => {
expect(result.replay.metadata?.timing).toBe('prebuilt');
expect(result.inspect.mode).toBe('seeded');
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
expect(config).toContain('api_key: env:ANTHROPIC_API_KEY');
expect(config).not.toContain('sk-ant-');
});

View file

@ -1,4 +1,4 @@
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import {
ensureSeededDemoProject,
loadPackagedDemoReplay,

View file

@ -1,14 +1,14 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { IngestReportSnapshot, MemoryFlowReplayInput } from '@klo/context/ingest';
import type { IngestReportSnapshot, MemoryFlowReplayInput } from '@ktx/context/ingest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { runKloDemo } from './demo.js';
import { runKtxDemo } from './demo.js';
import { DEMO_FULL_JOB_ID, defaultDemoProjectDir, ensureDemoProject } from './demo-assets.js';
import type { DemoFullResult } from './demo-full.js';
import { createTestDemoPromptAdapter } from './demo-interaction.js';
import type { renderMemoryFlowTui } from './memory-flow-tui.js';
import { KLO_NEXT_STEP_COMMANDS } from './next-steps.js';
import { KTX_NEXT_STEP_COMMANDS } from './next-steps.js';
import { resetVizFallbackWarningsForTest } from './viz-fallback.js';
function makeIo(options: { isTTY?: boolean; columns?: number; rawMode?: boolean } = {}) {
@ -108,12 +108,12 @@ function fakeFullResult(projectDir: string): DemoFullResult {
};
}
describe('runKloDemo', () => {
describe('runKtxDemo', () => {
let tempDir: string;
beforeEach(async () => {
resetVizFallbackWarningsForTest();
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-command-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-command-'));
});
afterEach(async () => {
@ -123,7 +123,7 @@ describe('runKloDemo', () => {
it('initializes the demo project', async () => {
const io = makeIo();
await expect(
runKloDemo({ command: 'init', projectDir: tempDir, force: false, inputMode: 'disabled' }, io.io),
runKtxDemo({ command: 'init', projectDir: tempDir, force: false, inputMode: 'disabled' }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain(`Demo project: ${tempDir}`);
@ -135,14 +135,14 @@ describe('runKloDemo', () => {
it('renders the packaged replay in no-input viz mode', async () => {
const io = makeIo({ isTTY: true });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' } },
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow Warehouse + dbt + BI + Docs done');
expect(io.stdout()).toContain('KTX memory flow Warehouse + dbt + BI + Docs done');
expect(io.stdout()).toContain('Saved 16 memories');
expect(io.stderr()).toBe('');
});
@ -152,7 +152,7 @@ describe('runKloDemo', () => {
const renderStoredMemoryFlow = vi.fn<typeof renderMemoryFlowTui>(async () => true);
await expect(
runKloDemo(
runKtxDemo(
{ command: 'replay', projectDir: tempDir, outputMode: 'viz' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
@ -166,7 +166,7 @@ describe('runKloDemo', () => {
adapter: 'live-database',
});
expect(renderStoredMemoryFlow.mock.calls[0]?.[2]).toEqual({ speedMultiplier: 0.125 });
expect(io.stdout()).toContain('KLO finished ingesting your data');
expect(io.stdout()).toContain('KTX finished ingesting your data');
expect(io.stderr()).toBe('');
});
@ -175,7 +175,7 @@ describe('runKloDemo', () => {
const renderStoredMemoryFlow = vi.fn<typeof renderMemoryFlowTui>(async () => true);
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: tempDir, outputMode: 'viz' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
@ -184,7 +184,7 @@ describe('runKloDemo', () => {
expect(renderStoredMemoryFlow).toHaveBeenCalledTimes(1);
expect(renderStoredMemoryFlow.mock.calls[0]?.[2]).toEqual({ speedMultiplier: 0.125 });
expect(io.stdout()).toContain('KLO finished ingesting your data');
expect(io.stdout()).toContain('KTX finished ingesting your data');
expect(io.stderr()).toBe('');
});
@ -193,7 +193,7 @@ describe('runKloDemo', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloDemo(
runKtxDemo(
{ command: 'replay', projectDir: tempDir, outputMode: 'viz' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
@ -203,10 +203,10 @@ describe('runKloDemo', () => {
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
expect(io.stdout()).toContain('Memory-flow summary: done');
expect(io.stdout()).toContain('Connection: orbit_demo');
expect(io.stdout()).toContain('klo sl list');
expect(io.stdout()).toContain('klo wiki list');
expect(io.stdout()).toContain('klo serve --mcp stdio --user-id local');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).toContain('ktx sl list');
expect(io.stdout()).toContain('ktx wiki list');
expect(io.stdout()).toContain('ktx serve --mcp stdio --user-id local');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
);
@ -216,15 +216,15 @@ describe('runKloDemo', () => {
const testIo = makeIo({ isTTY: false });
await expect(
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io),
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io),
).resolves.toBe(0);
expect(testIo.stdout()).toContain('Memory-flow summary: done');
expect(testIo.stdout()).toContain('Connection: orbit_demo');
expect(testIo.stdout()).toContain('klo sl list');
expect(testIo.stdout()).toContain('klo wiki list');
expect(testIo.stdout()).toContain('klo serve --mcp stdio --user-id local');
expect(testIo.stdout()).not.toContain('KLO memory flow');
expect(testIo.stdout()).toContain('ktx sl list');
expect(testIo.stdout()).toContain('ktx wiki list');
expect(testIo.stdout()).toContain('ktx serve --mcp stdio --user-id local');
expect(testIo.stdout()).not.toContain('KTX memory flow');
expect(testIo.stderr()).toContain(
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
);
@ -233,7 +233,7 @@ describe('runKloDemo', () => {
it('prints JSON replay output when requested', async () => {
const io = makeIo();
await expect(
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, io.io),
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, io.io),
).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toMatchObject({ runId: 'demo-seeded-orbit', connectionId: 'orbit_demo' });
@ -242,7 +242,7 @@ describe('runKloDemo', () => {
it('runs the packaged SQLite demo scan', async () => {
const io = makeIo();
await expect(runKloDemo({ command: 'scan', projectDir: tempDir, inputMode: 'disabled' }, io.io)).resolves.toBe(0);
await expect(runKtxDemo({ command: 'scan', projectDir: tempDir, inputMode: 'disabled' }, io.io)).resolves.toBe(0);
expect(io.stdout()).toContain('Demo scan: done');
expect(io.stdout()).toContain('Connection: orbit_demo');
@ -254,7 +254,7 @@ describe('runKloDemo', () => {
it('runs seeded mode with pre-seeded assets and inspect summary', async () => {
const io = makeIo({ isTTY: true });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
io.io,
{ env: { ...process.env, TERM: 'xterm-256color' } },
@ -272,7 +272,7 @@ describe('runKloDemo', () => {
const io = makeIo();
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: defaultDemoProjectDir(), outputMode: 'plain', inputMode: 'disabled' },
io.io,
),
@ -282,10 +282,10 @@ describe('runKloDemo', () => {
expect(io.stdout()).toContain('Source: packaged demo project');
expect(io.stdout()).toContain('Generated context: prebuilt from bundled assets');
expect(io.stdout()).toContain('LLM calls: none');
expect(io.stdout()).toContain('Your KLO project files are at:');
expect(io.stdout()).toContain(join(tmpdir(), 'klo-demo-'));
expect(io.stdout()).toContain('klo serve --mcp stdio');
expect(io.stdout()).not.toContain(['klo', 'mcp'].join(' '));
expect(io.stdout()).toContain('Your KTX project files are at:');
expect(io.stdout()).toContain(join(tmpdir(), 'ktx-demo-'));
expect(io.stdout()).toContain('ktx serve --mcp stdio');
expect(io.stdout()).not.toContain(['ktx', 'mcp'].join(' '));
expect(io.stdout()).not.toContain('deterministic');
});
@ -293,7 +293,7 @@ describe('runKloDemo', () => {
const testIo = makeIo({ isTTY: true, columns: 120 });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' },
testIo.io,
{ env: { ...process.env, TERM: 'dumb' } },
@ -310,19 +310,19 @@ describe('runKloDemo', () => {
it('prints demo inspect as plain text and JSON', async () => {
const seededIo = makeIo();
await expect(
runKloDemo({ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, seededIo.io),
runKtxDemo({ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, seededIo.io),
).resolves.toBe(0);
const plainIo = makeIo();
await expect(
runKloDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, plainIo.io),
runKtxDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, plainIo.io),
).resolves.toBe(0);
expect(plainIo.stdout()).toContain('Mode: seeded');
expect(plainIo.stdout()).toContain('Semantic-layer sources:');
const jsonIo = makeIo();
await expect(
runKloDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, jsonIo.io),
runKtxDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, jsonIo.io),
).resolves.toBe(0);
const parsed = JSON.parse(jsonIo.stdout());
expect(parsed).toMatchObject({
@ -347,7 +347,7 @@ describe('runKloDemo', () => {
generatedContext: 'prebuilt from bundled assets',
llmCalls: 'none',
},
nextCommands: KLO_NEXT_STEP_COMMANDS,
nextCommands: KTX_NEXT_STEP_COMMANDS,
});
expect(parsed.generatedOutputs.replays.fileCount).toBeGreaterThanOrEqual(3);
expect(jsonIo.stderr()).toBe('');
@ -359,7 +359,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io, {
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io, {
env: {},
runFullDemo,
}),
@ -372,10 +372,10 @@ describe('runKloDemo', () => {
onMemoryFlowChange: expect.any(Function),
}),
);
expect(testIo.stdout()).toContain('KLO memory flow orbit_demo/live-database done');
expect(testIo.stdout()).toContain('KTX memory flow orbit_demo/live-database done');
expect(testIo.stdout()).toContain('Full demo ingest: done');
expect(testIo.stdout()).toContain('Next: klo setup demo inspect');
expect(testIo.stdout()).toContain('Shows the files, semantic-layer sources, and memory KLO just produced.');
expect(testIo.stdout()).toContain('Next: ktx setup demo inspect');
expect(testIo.stdout()).toContain('Shows the files, semantic-layer sources, and memory KTX just produced.');
});
it('streams live memory-flow snapshots for full demo viz and then prints final summary', async () => {
@ -399,7 +399,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
prompts: createTestDemoPromptAdapter({ choices: ['reuse'] }),
runFullDemo,
@ -411,12 +411,12 @@ describe('runKloDemo', () => {
expect(liveSession.update).toHaveBeenCalledTimes(1);
expect(liveSession.close).toHaveBeenCalledTimes(1);
expect(testIo.stdout()).not.toContain('Memory-flow summary: done');
expect(testIo.stdout()).toContain('KLO finished ingesting your data');
expect(testIo.stdout()).toContain('klo sl list');
expect(testIo.stdout()).toContain('klo wiki list');
expect(testIo.stdout()).toContain('klo serve --mcp stdio --user-id local');
expect(testIo.stdout()).not.toContain(['klo', 'ask'].join(' '));
expect(testIo.stdout()).not.toContain(['klo', 'mcp'].join(' '));
expect(testIo.stdout()).toContain('KTX finished ingesting your data');
expect(testIo.stdout()).toContain('ktx sl list');
expect(testIo.stdout()).toContain('ktx wiki list');
expect(testIo.stdout()).toContain('ktx serve --mcp stdio --user-id local');
expect(testIo.stdout()).not.toContain(['ktx', 'ask'].join(' '));
expect(testIo.stdout()).not.toContain(['ktx', 'mcp'].join(' '));
});
it('uses plain progress for full demo viz when stdin raw mode is unavailable', async () => {
@ -440,7 +440,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
prompts: createTestDemoPromptAdapter({ choices: ['reuse'] }),
runFullDemo,
@ -456,7 +456,7 @@ describe('runKloDemo', () => {
);
expect(testIo.stdout()).toContain('[connect] Connected live-database - 7 database files (demo_full)');
expect(testIo.stdout()).toContain('Full demo ingest: done');
expect(testIo.stdout()).not.toContain('KLO memory flow');
expect(testIo.stdout()).not.toContain('KTX memory flow');
expect(testIo.stderr()).toContain(
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
);
@ -486,7 +486,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{ env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, runFullDemo }, // pragma: allowlist secret
@ -510,7 +510,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
testIo.io,
{ env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, runFullDemo }, // pragma: allowlist secret
@ -526,7 +526,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'ingest', mode: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{ env: {}, runFullDemo },
@ -537,12 +537,12 @@ describe('runKloDemo', () => {
});
it('saves full-demo replay output for the next demo replay command', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-full-replay-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-full-replay-'));
await ensureDemoProject({ projectDir: tempDir, force: false });
const io = makeIo();
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
io.io,
{
@ -554,7 +554,7 @@ describe('runKloDemo', () => {
const replayIo = makeIo();
await expect(
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, replayIo.io),
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, replayIo.io),
).resolves.toBe(0);
expect(JSON.parse(replayIo.stdout())).toMatchObject({
runId: 'run-full',
@ -566,7 +566,7 @@ describe('runKloDemo', () => {
const testIo = makeIo();
await expect(
runKloDemo(
runKtxDemo(
{ command: 'ingest', mode: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
),
@ -581,7 +581,7 @@ describe('runKloDemo', () => {
const runDoctor = vi.fn().mockResolvedValue(0);
await expect(
runKloDemo(
runKtxDemo(
{
command: 'doctor',
projectDir: tempDir,
@ -610,13 +610,13 @@ describe('runKloDemo', () => {
const rejected = makeIo();
await expect(
runKloDemo({ command: 'reset', projectDir: tempDir, force: false, inputMode: 'disabled' }, rejected.io),
runKtxDemo({ command: 'reset', projectDir: tempDir, force: false, inputMode: 'disabled' }, rejected.io),
).resolves.toBe(1);
expect(rejected.stderr()).toContain(`klo setup demo reset is destructive; pass --force to recreate ${tempDir}`);
expect(rejected.stderr()).toContain(`ktx setup demo reset is destructive; pass --force to recreate ${tempDir}`);
const accepted = makeIo();
await expect(
runKloDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, accepted.io),
runKtxDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, accepted.io),
).resolves.toBe(0);
expect(accepted.stdout()).toContain(`Demo project reset: ${tempDir}`);
});
@ -624,12 +624,12 @@ describe('runKloDemo', () => {
it('rehydrates seeded assets after reset --force', async () => {
const resetIo = makeIo();
await expect(
runKloDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, resetIo.io),
runKtxDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, resetIo.io),
).resolves.toBe(0);
const seededIo = makeIo();
await expect(
runKloDemo(
runKtxDemo(
{ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
seededIo.io,
),
@ -648,11 +648,11 @@ describe('runKloDemo', () => {
const testIo = makeIo();
await expect(
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, testIo.io),
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, testIo.io),
).resolves.toBe(1);
expect(testIo.stderr()).toContain(`Demo project is not ready at ${tempDir}: missing demo.db`);
expect(testIo.stderr()).toContain(`klo setup demo reset --project-dir ${tempDir} --force --no-input`);
expect(testIo.stderr()).toContain(`ktx setup demo reset --project-dir ${tempDir} --force --no-input`);
});
it('uses a process-local Anthropic key from the interactive prompt', async () => {
@ -661,7 +661,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
testIo.io,
{
@ -682,7 +682,7 @@ describe('runKloDemo', () => {
onMemoryFlowChange: expect.any(Function),
}),
);
expect(await readFile(join(tempDir, 'klo.yaml'), 'utf-8')).toContain('api_key: env:ANTHROPIC_API_KEY');
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain('api_key: env:ANTHROPIC_API_KEY');
});
it('routes an interactive missing-key choice to seeded mode', async () => {
@ -691,7 +691,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
testIo.io,
{
@ -712,7 +712,7 @@ describe('runKloDemo', () => {
const testIo = makeIo({ isTTY: true });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
testIo.io,
{
@ -733,7 +733,7 @@ describe('runKloDemo', () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
runKloDemo(
runKtxDemo(
{ command: 'full', projectDir: tempDir, outputMode: 'viz' },
testIo.io,
{
@ -745,7 +745,7 @@ describe('runKloDemo', () => {
).resolves.toBe(0);
expect(runFullDemo).not.toHaveBeenCalled();
expect(testIo.stdout()).toContain('KLO memory flow');
expect(testIo.stdout()).toContain('KTX memory flow');
expect(testIo.stdout()).toContain('done');
});
});

View file

@ -3,9 +3,9 @@ import {
formatMemoryFlowFinalSummary,
renderMemoryFlowReplay,
type MemoryFlowReplayInput,
} from '@klo/context/ingest/memory-flow';
import { resolveKloConfigReference } from '@klo/context/core';
import { loadKloProject } from '@klo/context/project';
} from '@ktx/context/ingest/memory-flow';
import { resolveKtxConfigReference } from '@ktx/context/core';
import { loadKtxProject } from '@ktx/context/project';
import {
DEMO_ADAPTER,
DEMO_CONNECTION_ID,
@ -34,11 +34,11 @@ import {
resolveFullCredentialDecision,
type DemoPromptAdapter,
} from './demo-interaction.js';
import type { KloDoctorArgs } from './doctor.js';
import type { KtxDoctorArgs } from './doctor.js';
import {
renderMemoryFlowTui,
startLiveMemoryFlowTui,
type KloMemoryFlowTuiIo,
type KtxMemoryFlowTuiIo,
type MemoryFlowTuiLiveSession,
} from './memory-flow-tui.js';
import {
@ -51,36 +51,36 @@ import { formatNextStepLines } from './next-steps.js';
profileMark('module:demo');
export type KloDemoOutputMode = 'plain' | 'json' | 'viz';
export type KloDemoInputMode = 'auto' | 'disabled';
export type KloDemoMode = 'full' | 'seeded';
export type KtxDemoOutputMode = 'plain' | 'json' | 'viz';
export type KtxDemoInputMode = 'auto' | 'disabled';
export type KtxDemoMode = 'full' | 'seeded';
export type KloDemoArgs =
| { command: 'init'; projectDir: string; force: boolean; inputMode?: KloDemoInputMode }
| { command: 'reset'; projectDir: string; force: boolean; inputMode?: KloDemoInputMode }
| { command: 'replay'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
| { command: 'scan'; projectDir: string; inputMode?: KloDemoInputMode }
| { command: 'inspect'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
| { command: 'doctor'; projectDir: string; outputMode: Exclude<KloDemoOutputMode, 'viz'>; inputMode?: KloDemoInputMode }
| { command: 'seeded'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
| { command: 'full'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
export type KtxDemoArgs =
| { command: 'init'; projectDir: string; force: boolean; inputMode?: KtxDemoInputMode }
| { command: 'reset'; projectDir: string; force: boolean; inputMode?: KtxDemoInputMode }
| { command: 'replay'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
| { command: 'scan'; projectDir: string; inputMode?: KtxDemoInputMode }
| { command: 'inspect'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
| { command: 'doctor'; projectDir: string; outputMode: Exclude<KtxDemoOutputMode, 'viz'>; inputMode?: KtxDemoInputMode }
| { command: 'seeded'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
| { command: 'full'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
| {
command: 'ingest';
mode: KloDemoMode;
mode: KtxDemoMode;
projectDir: string;
outputMode: KloDemoOutputMode;
inputMode?: KloDemoInputMode;
outputMode: KtxDemoOutputMode;
inputMode?: KtxDemoInputMode;
};
export interface KloDemoIo {
stdin?: KloMemoryFlowTuiIo['stdin'];
export interface KtxDemoIo {
stdin?: KtxMemoryFlowTuiIo['stdin'];
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
stderr: { write(chunk: string): void };
}
interface KloDemoDeps {
interface KtxDemoDeps {
runFullDemo?: typeof runDemoFull;
runDoctor?: (args: KloDoctorArgs, io: KloDemoIo) => Promise<number>;
runDoctor?: (args: KtxDoctorArgs, io: KtxDemoIo) => Promise<number>;
renderStoredMemoryFlow?: typeof renderMemoryFlowTui;
startLiveMemoryFlow?: typeof startLiveMemoryFlowTui;
env?: NodeJS.ProcessEnv;
@ -127,7 +127,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
}
}
const lines: string[] = ['', '★ KLO finished ingesting your data', ''];
const lines: string[] = ['', '★ KTX finished ingesting your data', ''];
if (chunkCount > 0) {
lines.push(` ✓ Analyzed ${chunkCount} business area${chunkCount === 1 ? '' : 's'}`);
@ -137,7 +137,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
lines.push('');
if (slCount > 0 || wikiCount > 0) {
lines.push(' KLO created:');
lines.push(' KTX created:');
if (slCount > 0) lines.push(` 📊 ${slCount} query definition${slCount === 1 ? '' : 's'} — so agents can write accurate SQL for your data`);
if (wikiCount > 0) lines.push(` 📝 ${wikiCount} knowledge page${wikiCount === 1 ? '' : 's'} — so agents understand your business context`);
lines.push('');
@ -153,7 +153,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
lines.push(...formatNextStepLines());
if (input.sourceDir) {
lines.push('');
lines.push(` Your KLO project files are at: ${input.sourceDir}`);
lines.push(` Your KTX project files are at: ${input.sourceDir}`);
}
lines.push('');
@ -164,7 +164,7 @@ function formatPlainReplaySummary(input: MemoryFlowReplayInput): string {
return [formatMemoryFlowFinalSummary(input).trimEnd(), '', 'What to do next:', ...formatNextStepLines(), ''].join('\n');
}
function writeReplay(input: MemoryFlowReplayInput, outputMode: KloDemoOutputMode, io: KloDemoIo): void {
function writeReplay(input: MemoryFlowReplayInput, outputMode: KtxDemoOutputMode, io: KtxDemoIo): void {
if (outputMode === 'json') {
io.stdout.write(`${JSON.stringify(input, null, 2)}\n`);
return;
@ -181,10 +181,10 @@ function writeReplay(input: MemoryFlowReplayInput, outputMode: KloDemoOutputMode
async function writeStoredReplay(
input: MemoryFlowReplayInput,
outputMode: KloDemoOutputMode,
inputMode: KloDemoArgs['inputMode'],
io: KloDemoIo,
deps: KloDemoDeps,
outputMode: KtxDemoOutputMode,
inputMode: KtxDemoArgs['inputMode'],
io: KtxDemoIo,
deps: KtxDemoDeps,
env: NodeJS.ProcessEnv,
): Promise<void> {
const resolvedOutputMode = effectiveDemoOutputMode(outputMode, io, env, {
@ -211,8 +211,8 @@ async function writeStoredReplay(
function writeInspect(
summary: Awaited<ReturnType<typeof inspectDemoProject>>,
outputMode: KloDemoOutputMode,
io: KloDemoIo,
outputMode: KtxDemoOutputMode,
io: KtxDemoIo,
): void {
if (outputMode === 'json') {
io.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
@ -224,8 +224,8 @@ function writeInspect(
function writeFullDemo(
result: Awaited<ReturnType<typeof runDemoFull>>,
outputMode: KloDemoOutputMode,
io: KloDemoIo,
outputMode: KtxDemoOutputMode,
io: KtxDemoIo,
options: { liveWasRendered?: boolean; projectDir?: string } = {},
): void {
if (outputMode === 'json') {
@ -274,8 +274,8 @@ function replayWithFullMetadata(result: Awaited<ReturnType<typeof runDemoFull>>)
function pickMemoryFlowProgress(
liveSession: MemoryFlowTuiLiveSession | null,
outputMode: KloDemoOutputMode,
io: KloDemoIo,
outputMode: KtxDemoOutputMode,
io: KtxDemoIo,
): ((snapshot: MemoryFlowReplayInput) => void) | undefined {
if (liveSession) {
return (snapshot: MemoryFlowReplayInput) => {
@ -290,7 +290,7 @@ function pickMemoryFlowProgress(
return createPlainProgressEmitter(io);
}
function isTuiCapableDemoIo(io: KloDemoIo): io is KloDemoIo & KloMemoryFlowTuiIo {
function isTuiCapableDemoIo(io: KtxDemoIo): io is KtxDemoIo & KtxMemoryFlowTuiIo {
return (
io.stdin?.isTTY === true &&
io.stdout.isTTY === true &&
@ -304,11 +304,11 @@ interface EffectiveDemoOutputModeOptions {
}
function effectiveDemoOutputMode(
outputMode: KloDemoOutputMode,
io: KloDemoIo,
outputMode: KtxDemoOutputMode,
io: KtxDemoIo,
env: NodeJS.ProcessEnv,
options: EffectiveDemoOutputModeOptions = {},
): KloDemoOutputMode {
): KtxDemoOutputMode {
if (outputMode !== 'viz') {
return outputMode;
}
@ -346,7 +346,7 @@ async function ensureDemoProjectForCommand(projectDir: string): Promise<void> {
});
}
async function prepareProjectForDemoCommand(args: KloDemoArgs, io: KloDemoIo, deps: KloDemoDeps): Promise<string | null> {
async function prepareProjectForDemoCommand(args: KtxDemoArgs, io: KtxDemoIo, deps: KtxDemoDeps): Promise<string | null> {
if (args.command === 'init' || args.command === 'reset' || args.command === 'doctor') {
return args.projectDir;
}
@ -372,10 +372,10 @@ async function prepareProjectForDemoCommand(args: KloDemoArgs, io: KloDemoIo, de
async function runReplayDemo(
projectDir: string,
outputMode: KloDemoOutputMode,
inputMode: KloDemoArgs['inputMode'],
io: KloDemoIo,
deps: KloDemoDeps,
outputMode: KtxDemoOutputMode,
inputMode: KtxDemoArgs['inputMode'],
io: KtxDemoIo,
deps: KtxDemoDeps,
env: NodeJS.ProcessEnv = process.env,
): Promise<number> {
await ensureDemoProjectForCommand(projectDir);
@ -385,10 +385,10 @@ async function runReplayDemo(
async function runSeededDemo(
projectDir: string,
outputMode: KloDemoOutputMode,
inputMode: KloDemoArgs['inputMode'],
io: KloDemoIo,
deps: KloDemoDeps,
outputMode: KtxDemoOutputMode,
inputMode: KtxDemoArgs['inputMode'],
io: KtxDemoIo,
deps: KtxDemoDeps,
env: NodeJS.ProcessEnv = process.env,
): Promise<number> {
const result = await runDemoSeeded({ projectDir });
@ -411,7 +411,7 @@ async function runSeededDemo(
return 0;
}
export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, deps: KloDemoDeps = {}): Promise<number> {
export async function runKtxDemo(args: KtxDemoArgs, io: KtxDemoIo = process, deps: KtxDemoDeps = {}): Promise<number> {
try {
if (args.command === 'init') {
const result = await ensureDemoProject({ projectDir: args.projectDir, force: args.force });
@ -419,7 +419,7 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
io.stdout.write(`Config: ${result.configPath}\n`);
io.stdout.write(`Database: ${result.databasePath}\n`);
io.stdout.write(`Replay: ${result.replayPath}\n`);
io.stdout.write('Next: klo setup demo --no-input\n');
io.stdout.write('Next: ktx setup demo --no-input\n');
io.stdout.write(' Runs the pre-seeded demo without calling the LLM.\n');
return 0;
}
@ -430,7 +430,7 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
io.stdout.write(`Config: ${result.configPath}\n`);
io.stdout.write(`Database: ${result.databasePath}\n`);
io.stdout.write(`Replay: ${result.replayPath}\n`);
io.stdout.write('Next: klo setup demo --mode full\n');
io.stdout.write('Next: ktx setup demo --mode full\n');
io.stdout.write(' Runs the full AI-backed pass with your LLM provider.\n');
return 0;
}
@ -454,13 +454,13 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
if (args.command === 'full' || (args.command === 'ingest' && args.mode === 'full')) {
const executeFullDemo = deps.runFullDemo ?? runDemoFull;
await ensureDemoProjectForCommand(preparedProjectDir);
const project = await loadKloProject({ projectDir: preparedProjectDir });
const project = await loadKtxProject({ projectDir: preparedProjectDir });
const credentialStatus = fullDemoCredentialStatus(project, env);
const credentialDecision = await resolveFullCredentialDecision({
needsAnthropicKey:
credentialStatus.status === 'missing-anthropic-key' &&
project.config.llm.provider.backend === 'anthropic' &&
!resolveKloConfigReference(project.config.llm.provider.anthropic?.api_key, env),
!resolveKtxConfigReference(project.config.llm.provider.anthropic?.api_key, env),
inputMode: args.inputMode,
io,
env,
@ -523,8 +523,8 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
}
if (args.command === 'doctor') {
const { runKloDoctor } = await import('./doctor.js');
const executeDoctor = deps.runDoctor ?? runKloDoctor;
const { runKtxDoctor } = await import('./doctor.js');
const executeDoctor = deps.runDoctor ?? runKtxDoctor;
return await executeDoctor(
{
command: 'demo',

View file

@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { runKloCli } from './index.js';
import { runKtxCli } from './index.js';
function makeIo() {
let stdout = '';
@ -26,9 +26,9 @@ describe('dev Commander tree', () => {
it('prints visible dev help with only supported low-level command groups', async () => {
const testIo = makeIo();
await expect(runKloCli(['dev', '--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', '--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo dev [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
for (const command of ['init', 'doctor', 'scan', 'ingest', 'mapping']) {
expect(testIo.stdout()).toContain(command);
}
@ -51,10 +51,10 @@ describe('dev Commander tree', () => {
it('keeps dev callable while hiding it from root command rows', async () => {
const testIo = makeIo();
await expect(runKloCli(['--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Advanced:');
expect(testIo.stdout()).toContain('klo dev');
expect(testIo.stdout()).toContain('ktx dev');
expect(testIo.stdout()).not.toContain('dev Low-level diagnostics');
expect(testIo.stderr()).toBe('');
});
@ -63,15 +63,15 @@ describe('dev Commander tree', () => {
const { mkdtemp, readFile, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const tempDir = await mkdtemp(join(tmpdir(), 'klo-dev-init-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-init-'));
const projectDir = join(tempDir, 'warehouse');
const testIo = makeIo();
try {
await expect(runKloCli(['dev', 'init', projectDir, '--name', 'warehouse'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'init', projectDir, '--name', 'warehouse'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain(`Initialized KLO project at ${projectDir}`);
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
expect(testIo.stderr()).toBe('');
} finally {
await rm(tempDir, { recursive: true, force: true });
@ -82,16 +82,16 @@ describe('dev Commander tree', () => {
const { mkdtemp, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const tempDir = await mkdtemp(join(tmpdir(), 'klo-dev-init-global-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-init-global-'));
const projectDir = join(tempDir, 'global-init');
const testIo = makeIo();
try {
await expect(
runKloCli(['--project-dir', projectDir, 'dev', 'init', '--name', 'global-init'], testIo.io),
runKtxCli(['--project-dir', projectDir, 'dev', 'init', '--name', 'global-init'], testIo.io),
).resolves.toBe(0);
expect(testIo.stdout()).toContain(`Initialized KLO project at ${projectDir}`);
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
expect(testIo.stderr()).toBe('');
} finally {
await rm(tempDir, { recursive: true, force: true });
@ -106,7 +106,7 @@ describe('dev Commander tree', () => {
]) {
const testIo = makeIo();
await expect(runKloCli(argv, testIo.io)).resolves.toBe(1);
await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown command|error:/);
}
@ -115,12 +115,12 @@ describe('dev Commander tree', () => {
it.each([
{
argv: ['dev', 'doctor', '--help'],
expected: ['Usage: klo dev doctor', '--json', '--no-input'],
expected: ['Usage: ktx dev doctor', '--json', '--no-input'],
},
{
argv: ['dev', 'scan', '--help'],
expected: [
'Usage: klo dev scan',
'Usage: ktx dev scan',
'--mode <mode>',
'structural',
'relationships',
@ -136,12 +136,12 @@ describe('dev Commander tree', () => {
},
{
argv: ['dev', 'scan', 'report', '--help'],
expected: ['Usage: klo dev scan report [options] <runId>', '<runId>', '--json'],
expected: ['Usage: ktx dev scan report [options] <runId>', '<runId>', '--json'],
},
{
argv: ['dev', 'scan', 'relationships', '--help'],
expected: [
'Usage: klo dev scan relationships [options] <runId>',
'Usage: ktx dev scan relationships [options] <runId>',
'--status <status>',
'--limit <count>',
'--accept <candidateId>',
@ -154,7 +154,7 @@ describe('dev Commander tree', () => {
{
argv: ['dev', 'scan', 'relationship-apply', '--help'],
expected: [
'Usage: klo dev scan relationship-apply [options] <runId>',
'Usage: ktx dev scan relationship-apply [options] <runId>',
'--all-accepted',
'--candidate <candidateId>',
'--dry-run',
@ -163,7 +163,7 @@ describe('dev Commander tree', () => {
{
argv: ['dev', 'scan', 'relationship-thresholds', '--help'],
expected: [
'Usage: klo dev scan relationship-thresholds [options]',
'Usage: ktx dev scan relationship-thresholds [options]',
'--connection <connectionId>',
'--min-total-labels <count>',
'--min-accepted-labels <count>',
@ -174,7 +174,7 @@ describe('dev Commander tree', () => {
{
argv: ['dev', 'scan', 'relationship-feedback', '--help'],
expected: [
'Usage: klo dev scan relationship-feedback [options]',
'Usage: ktx dev scan relationship-feedback [options]',
'--connection <connectionId>',
'--decision <decision>',
'--json',
@ -184,7 +184,7 @@ describe('dev Commander tree', () => {
{
argv: ['dev', 'scan', 'relationship-calibration', '--help'],
expected: [
'Usage: klo dev scan relationship-calibration [options]',
'Usage: ktx dev scan relationship-calibration [options]',
'--connection <connectionId>',
'--decision <decision>',
'--accept-threshold <value>',
@ -194,11 +194,11 @@ describe('dev Commander tree', () => {
},
{
argv: ['dev', 'ingest', 'run', '--help'],
expected: ['Usage: klo dev ingest run [options]', '--connection-id <connectionId>', '--adapter <adapter>'],
expected: ['Usage: ktx dev ingest run [options]', '--connection-id <connectionId>', '--adapter <adapter>'],
},
{
argv: ['dev', 'mapping', 'sync-state', 'set', '--help'],
expected: ['Usage: klo dev mapping sync-state set [options] <connectionId>', '--mode <mode>'],
expected: ['Usage: ktx dev mapping sync-state set [options] <connectionId>', '--mode <mode>'],
},
])('prints generated nested help for $argv', async ({ argv, expected }) => {
const io = makeIo();
@ -206,7 +206,7 @@ describe('dev Commander tree', () => {
const ingest = vi.fn(async () => 0);
const scan = vi.fn(async () => 0);
await expect(runKloCli(argv, io.io, { doctor, ingest, scan })).resolves.toBe(0);
await expect(runKtxCli(argv, io.io, { doctor, ingest, scan })).resolves.toBe(0);
for (const text of expected) {
expect(io.stdout()).toContain(text);
@ -222,7 +222,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--dry-run'], scanIo.io, { scan }),
runKtxCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--dry-run'], scanIo.io, { scan }),
).resolves.toBe(0);
expect(scan).toHaveBeenCalledWith(
@ -245,7 +245,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'], io.io, {
runKtxCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'], io.io, {
scan,
}),
).resolves.toBe(0);
@ -269,7 +269,7 @@ describe('dev Commander tree', () => {
const io = makeIo();
const scan = vi.fn(async () => 0);
await expect(runKloCli(['dev', 'scan', 'warehouse', option], io.io, { scan })).resolves.toBe(1);
await expect(runKtxCli(['dev', 'scan', 'warehouse', option], io.io, { scan })).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
expect(io.stderr()).toContain(`unknown option '${option}'`);
@ -279,18 +279,18 @@ describe('dev Commander tree', () => {
const io = makeIo();
const scan = vi.fn(async () => 0);
await expect(runKloCli(['dev', 'scan', '--dry-run'], io.io, { scan })).resolves.toBe(1);
await expect(runKtxCli(['dev', 'scan', '--dry-run'], io.io, { scan })).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
expect(io.stdout()).toContain('Usage: klo dev scan');
expect(io.stderr()).toContain('klo dev scan requires <connectionId> or a subcommand');
expect(io.stdout()).toContain('Usage: ktx dev scan');
expect(io.stderr()).toContain('ktx dev scan requires <connectionId> or a subcommand');
});
it('rejects invalid scan modes before dispatch', async () => {
const io = makeIo();
const scan = vi.fn(async () => 0);
await expect(runKloCli(['dev', 'scan', 'warehouse', '--mode', 'deep'], io.io, { scan })).resolves.toBe(1);
await expect(runKtxCli(['dev', 'scan', 'warehouse', '--mode', 'deep'], io.io, { scan })).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
expect(io.stderr()).toContain("argument 'deep' is invalid");
@ -301,10 +301,10 @@ describe('dev Commander tree', () => {
const io = makeIo();
const scan = vi.fn(async () => 0);
await expect(runKloCli(['dev', 'scan', 'report', '--help'], io.io, { scan })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'scan', 'report', '--help'], io.io, { scan })).resolves.toBe(0);
expect(io.stdout()).toContain('--project-dir is inherited from `klo dev scan`');
expect(io.stdout()).not.toContain('--project-dir is inherited from `klo scan`');
expect(io.stdout()).toContain('--project-dir is inherited from `ktx dev scan`');
expect(io.stdout()).not.toContain('--project-dir is inherited from `ktx scan`');
expect(scan).not.toHaveBeenCalled();
});
@ -314,10 +314,10 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'report', 'scan-run-1', '--project-dir', '/tmp/project'], humanIo.io, { scan }),
runKtxCli(['dev', 'scan', 'report', 'scan-run-1', '--project-dir', '/tmp/project'], humanIo.io, { scan }),
).resolves.toBe(0);
await expect(
runKloCli(['dev', 'scan', 'report', 'scan-run-2', '--project-dir', '/tmp/project', '--json'], jsonIo.io, {
runKtxCli(['dev', 'scan', 'report', 'scan-run-2', '--project-dir', '/tmp/project', '--json'], jsonIo.io, {
scan,
}),
).resolves.toBe(0);
@ -339,7 +339,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -377,7 +377,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -419,7 +419,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'relationships', 'scan-run-review', option, ''], io.io, { scan }),
runKtxCli(['dev', 'scan', 'relationships', 'scan-run-review', option, ''], io.io, { scan }),
).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
@ -431,7 +431,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'relationship-feedback', '--json', '--jsonl'], io.io, { scan }),
runKtxCli(['dev', 'scan', 'relationship-feedback', '--json', '--jsonl'], io.io, { scan }),
).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
@ -443,7 +443,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -480,7 +480,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -516,7 +516,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -557,7 +557,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -598,7 +598,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(['dev', 'scan', 'relationship-calibration', '--accept-threshold', '1.5'], io.io, { scan }),
runKtxCli(['dev', 'scan', 'relationship-calibration', '--accept-threshold', '1.5'], io.io, { scan }),
).resolves.toBe(1);
expect(scan).not.toHaveBeenCalled();
@ -610,7 +610,7 @@ describe('dev Commander tree', () => {
const scan = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'scan',
@ -635,7 +635,7 @@ describe('dev Commander tree', () => {
const ingest = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'ingest',

View file

@ -1,6 +1,6 @@
import { resolve } from 'node:path';
import type { Command } from '@commander-js/extra-typings';
import { type CommandWithGlobalOptions, type KloCliCommandContext, resolveCommandProjectDir } from './cli-program.js';
import { type CommandWithGlobalOptions, type KtxCliCommandContext, resolveCommandProjectDir } from './cli-program.js';
import { registerCompletionCommands } from './commands/completion-commands.js';
import { registerConnectionMappingCommands } from './commands/connection-commands.js';
import { registerDoctorCommands } from './commands/doctor-commands.js';
@ -10,7 +10,7 @@ import { profileMark } from './startup-profile.js';
profileMark('module:dev');
export function registerDevCommands(program: Command, context: KloCliCommandContext): void {
export function registerDevCommands(program: Command, context: KtxCliCommandContext): void {
const dev = program
.command('dev', { hidden: true })
.description('Low-level diagnostics, scans, adapter commands, and mapping tools')
@ -27,10 +27,10 @@ export function registerDevCommands(program: Command, context: KloCliCommandCont
dev
.command('init')
.description('Initialize a Git-backed KLO project directory for maintenance scripts')
.description('Initialize a Git-backed KTX project directory for maintenance scripts')
.argument('[directory]', 'Project directory')
.option('--name <name>', 'Project name written to klo.yaml')
.option('--force', 'Rewrite klo.yaml and scaffold files in an existing project', false)
.option('--name <name>', 'Project name written to ktx.yaml')
.option('--force', 'Rewrite ktx.yaml and scaffold files in an existing project', false)
.action(
async (
projectDir: string | undefined,

View file

@ -2,10 +2,10 @@ import { mkdtemp, rm, writeFile } 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 { KloEmbeddingConfig, KloEmbeddingHealthCheckOptions, KloEmbeddingHealthCheckResult } from '@klo/llm';
import type { KtxEmbeddingConfig, KtxEmbeddingHealthCheckOptions, KtxEmbeddingHealthCheckResult } from '@ktx/llm';
import {
formatDoctorReport,
runKloDoctor,
runKtxDoctor,
runSetupDoctorChecks,
type DoctorCheck,
} from './doctor.js';
@ -32,13 +32,13 @@ function makeIo() {
}
type EmbeddingHealthCheck = (
config: KloEmbeddingConfig,
options?: KloEmbeddingHealthCheckOptions,
) => Promise<KloEmbeddingHealthCheckResult>;
config: KtxEmbeddingConfig,
options?: KtxEmbeddingHealthCheckOptions,
) => Promise<KtxEmbeddingHealthCheckResult>;
async function writeProjectConfig(projectDir: string, embeddingLines: string[]): Promise<void> {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -69,9 +69,9 @@ describe('formatDoctorReport', () => {
},
];
expect(formatDoctorReport({ title: 'KLO setup doctor', checks })).toBe(
expect(formatDoctorReport({ title: 'KTX setup doctor', checks })).toBe(
[
'KLO setup doctor',
'KTX setup doctor',
'PASS Node 22+: v22.16.0 ABI 127',
'FAIL Native SQLite: Cannot load better-sqlite3',
' Fix: Run: pnpm run native:rebuild',
@ -85,12 +85,12 @@ describe('runSetupDoctorChecks', () => {
it('returns pass checks when injected commands and file checks succeed', async () => {
const checks = await runSetupDoctorChecks({
env: { PATH: '/bin' },
workspaceRoot: '/workspace/klo',
workspaceRoot: '/workspace/ktx',
execText: async (command, args) => {
if (command === 'pnpm' && args[0] === '--version') return '10.28.0';
if (command === 'corepack' && args[0] === '--version') return '0.32.0';
if (command === 'uv' && args[0] === '--version') return 'uv 0.9.5';
if (command === process.execPath && args.includes('--version')) return '@klo/cli 0.0.0-private';
if (command === process.execPath && args.includes('--version')) return '@ktx/cli 0.0.0-private';
throw new Error(`${command} ${args.join(' ')}`);
},
pathExists: async () => true,
@ -111,7 +111,7 @@ describe('runSetupDoctorChecks', () => {
it('returns exact fixes when setup checks fail', async () => {
const checks = await runSetupDoctorChecks({
env: {},
workspaceRoot: '/workspace/klo',
workspaceRoot: '/workspace/ktx',
execText: async (command) => {
throw new Error(`${command} not found`);
},
@ -140,12 +140,12 @@ describe('runSetupDoctorChecks', () => {
it('treats missing corepack as a warning so setup doctor can still pass', async () => {
const checks = await runSetupDoctorChecks({
env: { PATH: '/bin' },
workspaceRoot: '/workspace/klo',
workspaceRoot: '/workspace/ktx',
execText: async (command, args) => {
if (command === 'pnpm' && args[0] === '--version') return '10.28.0';
if (command === 'corepack' && args[0] === '--version') throw new Error('spawn corepack ENOENT');
if (command === 'uv' && args[0] === '--version') return 'uv 0.9.5';
if (command === process.execPath && args.includes('--version')) return '@klo/cli 0.0.0-private';
if (command === process.execPath && args.includes('--version')) return '@ktx/cli 0.0.0-private';
throw new Error(`${command} ${args.join(' ')}`);
},
pathExists: async () => true,
@ -154,7 +154,7 @@ describe('runSetupDoctorChecks', () => {
const testIo = makeIo();
await expect(
runKloDoctor({ command: 'setup', outputMode: 'plain', inputMode: 'disabled' }, testIo.io, {
runKtxDoctor({ command: 'setup', outputMode: 'plain', inputMode: 'disabled' }, testIo.io, {
runSetupChecks: async () => checks,
}),
).resolves.toBe(0);
@ -171,11 +171,11 @@ describe('runSetupDoctorChecks', () => {
});
});
describe('runKloDoctor', () => {
describe('runKtxDoctor', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-doctor-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-doctor-'));
});
afterEach(async () => {
@ -186,7 +186,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'setup', outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -204,7 +204,7 @@ describe('runKloDoctor', () => {
),
).resolves.toBe(1);
expect(testIo.stdout()).toContain('KLO setup doctor');
expect(testIo.stdout()).toContain('KTX setup doctor');
expect(testIo.stdout()).toContain('FAIL TypeScript package build: Missing packages/cli/dist/bin.js');
expect(testIo.stdout()).toContain('Fix: Run: pnpm run build');
expect(testIo.stderr()).toBe('');
@ -214,7 +214,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'setup', outputMode: 'json', inputMode: 'disabled' },
testIo.io,
{
@ -226,14 +226,14 @@ describe('runKloDoctor', () => {
).resolves.toBe(0);
expect(JSON.parse(testIo.stdout())).toEqual({
title: 'KLO setup doctor',
title: 'KTX setup doctor',
checks: [{ id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0 ABI 127' }],
});
});
it('runs project checks against a valid klo.yaml', async () => {
it('runs project checks against a valid ktx.yaml', async () => {
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -250,7 +250,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -261,14 +261,14 @@ describe('runKloDoctor', () => {
),
).resolves.toBe(0);
expect(testIo.stdout()).toContain('KLO project doctor');
expect(testIo.stdout()).toContain('KTX project doctor');
expect(testIo.stdout()).toContain('PASS Project config: warehouse');
expect(testIo.stdout()).toContain('PASS Connections: 1 configured');
});
it('includes Postgres historic-SQL readiness in project doctor output', async () => {
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -295,12 +295,12 @@ describe('runKloDoctor', () => {
status: 'warn' as const,
detail:
'pg_stat_statements ready (PostgreSQL 16.4) with warnings: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
fix: `Update the Postgres parameter group or config, then rerun \`klo dev doctor --project-dir ${tempDir}\``,
fix: `Update the Postgres parameter group or config, then rerun \`ktx dev doctor --project-dir ${tempDir}\``,
},
]);
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -322,7 +322,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -338,7 +338,7 @@ describe('runKloDoctor', () => {
'Semantic lane will be skipped; lexical, dictionary, and token lanes remain available.',
);
expect(testIo.stdout()).toContain(
`Fix: Run: klo setup --project-dir ${tempDir} --no-input`,
`Fix: Run: ktx setup --project-dir ${tempDir} --no-input`,
);
});
@ -355,7 +355,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -375,7 +375,7 @@ describe('runKloDoctor', () => {
dimensions: 384,
sentenceTransformers: { baseURL: 'http://127.0.0.1:8765', pathPrefix: '' },
},
{ text: 'KLO semantic search doctor probe', timeoutMs: 1234 },
{ text: 'KTX semantic search doctor probe', timeoutMs: 1234 },
);
expect(testIo.stdout()).toContain(
'PASS Semantic search embeddings: sentence-transformers/all-MiniLM-L6-v2 (384d) probe succeeded',
@ -395,7 +395,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
testIo.io,
{
@ -413,7 +413,7 @@ describe('runKloDoctor', () => {
model: 'all-MiniLM-L6-v2',
dimensions: 384,
}),
{ text: 'KLO semantic search doctor probe', timeoutMs: 120_000 },
{ text: 'KTX semantic search doctor probe', timeoutMs: 120_000 },
);
});
@ -433,7 +433,7 @@ describe('runKloDoctor', () => {
const testIo = makeIo();
await expect(
runKloDoctor(
runKtxDoctor(
{ command: 'project', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
testIo.io,
{
@ -454,7 +454,7 @@ describe('runKloDoctor', () => {
status: 'warn',
detail:
'sentence-transformers/all-MiniLM-L6-v2 (384d) probe failed: connect ECONNREFUSED 127.0.0.1:8765. Semantic lane will be skipped; lexical, dictionary, and token lanes remain available.',
fix: `Run: klo setup --project-dir ${tempDir} --no-input`,
fix: `Run: ktx setup --project-dir ${tempDir} --no-input`,
});
});
});

View file

@ -4,15 +4,15 @@ import { access } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import type { KloLocalProject, KloProjectEmbeddingConfig } from '@klo/context/project';
import type { KloEmbeddingConfig, KloEmbeddingHealthCheckOptions, KloEmbeddingHealthCheckResult } from '@klo/llm';
import type { KtxLocalProject, KtxProjectEmbeddingConfig } from '@ktx/context/project';
import type { KtxEmbeddingConfig, KtxEmbeddingHealthCheckOptions, KtxEmbeddingHealthCheckResult } from '@ktx/llm';
import type { HistoricSqlDoctorDeps } from './historic-sql-doctor.js';
const execFileAsync = promisify(execFile);
type DoctorStatus = 'pass' | 'warn' | 'fail';
type KloDoctorOutputMode = 'plain' | 'json';
type KloDoctorInputMode = 'auto' | 'disabled';
type KtxDoctorOutputMode = 'plain' | 'json';
type KtxDoctorInputMode = 'auto' | 'disabled';
export interface DoctorCheck {
id: string;
@ -27,12 +27,12 @@ interface DoctorReport {
checks: DoctorCheck[];
}
export type KloDoctorArgs =
| { command: 'setup'; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode }
| { command: 'project'; projectDir: string; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode }
| { command: 'demo'; projectDir: string; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode };
export type KtxDoctorArgs =
| { command: 'setup'; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode }
| { command: 'project'; projectDir: string; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode }
| { command: 'demo'; projectDir: string; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode };
interface KloDoctorIo {
interface KtxDoctorIo {
stdout: { write(chunk: string): void };
stderr: { write(chunk: string): void };
}
@ -46,9 +46,9 @@ interface SetupDoctorDeps {
}
type EmbeddingHealthCheck = (
config: KloEmbeddingConfig,
options?: KloEmbeddingHealthCheckOptions,
) => Promise<KloEmbeddingHealthCheckResult>;
config: KtxEmbeddingConfig,
options?: KtxEmbeddingHealthCheckOptions,
) => Promise<KtxEmbeddingHealthCheckResult>;
interface SemanticSearchDoctorDeps {
env?: NodeJS.ProcessEnv;
@ -56,9 +56,9 @@ interface SemanticSearchDoctorDeps {
embeddingProbeTimeoutMs?: number;
}
interface KloDoctorDeps extends SemanticSearchDoctorDeps, HistoricSqlDoctorDeps {
interface KtxDoctorDeps extends SemanticSearchDoctorDeps, HistoricSqlDoctorDeps {
runSetupChecks?: () => Promise<DoctorCheck[]>;
runHistoricSqlDoctorChecks?: (project: KloLocalProject, deps: HistoricSqlDoctorDeps) => Promise<DoctorCheck[]>;
runHistoricSqlDoctorChecks?: (project: KtxLocalProject, deps: HistoricSqlDoctorDeps) => Promise<DoctorCheck[]>;
}
function workspaceRootDir(): string {
@ -119,18 +119,18 @@ function check(status: DoctorStatus, id: string, label: string, detail: string,
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
}
const SEMANTIC_SEARCH_HEALTH_TEXT = 'KLO semantic search doctor probe';
const SEMANTIC_SEARCH_HEALTH_TEXT = 'KTX semantic search doctor probe';
const SEMANTIC_SEARCH_HEALTH_TIMEOUT_MS = 5_000;
const SEMANTIC_SEARCH_LOCAL_HEALTH_TIMEOUT_MS = 120_000;
function semanticEmbeddingSetupFix(projectDir: string, backend: KloProjectEmbeddingConfig['backend']): string {
function semanticEmbeddingSetupFix(projectDir: string, backend: KtxProjectEmbeddingConfig['backend']): string {
if (backend === 'openai') {
return `Set OPENAI_API_KEY or rerun: klo setup --project-dir ${projectDir} --embedding-backend openai --no-input`;
return `Set OPENAI_API_KEY or rerun: ktx setup --project-dir ${projectDir} --embedding-backend openai --no-input`;
}
return `Run: klo setup --project-dir ${projectDir} --no-input`;
return `Run: ktx setup --project-dir ${projectDir} --no-input`;
}
function embeddingConfigLabel(config: KloProjectEmbeddingConfig | KloEmbeddingConfig): string {
function embeddingConfigLabel(config: KtxProjectEmbeddingConfig | KtxEmbeddingConfig): string {
const model = config.model?.trim() || 'model not configured';
return `${config.backend}/${model} (${config.dimensions}d)`;
}
@ -140,15 +140,15 @@ function semanticLaneFallbackDetail(reason: string): string {
}
async function defaultEmbeddingHealthCheck(
config: KloEmbeddingConfig,
options?: KloEmbeddingHealthCheckOptions,
): Promise<KloEmbeddingHealthCheckResult> {
const { runKloEmbeddingHealthCheck } = await import('@klo/llm');
return runKloEmbeddingHealthCheck(config, options);
config: KtxEmbeddingConfig,
options?: KtxEmbeddingHealthCheckOptions,
): Promise<KtxEmbeddingHealthCheckResult> {
const { runKtxEmbeddingHealthCheck } = await import('@ktx/llm');
return runKtxEmbeddingHealthCheck(config, options);
}
async function runSemanticSearchEmbeddingCheck(
config: KloProjectEmbeddingConfig,
config: KtxProjectEmbeddingConfig,
projectDir: string,
deps: SemanticSearchDoctorDeps = {},
): Promise<DoctorCheck> {
@ -163,8 +163,8 @@ async function runSemanticSearchEmbeddingCheck(
}
try {
const { resolveLocalKloEmbeddingConfig } = await import('@klo/context');
const resolved = resolveLocalKloEmbeddingConfig(config, deps.env ?? process.env);
const { resolveLocalKtxEmbeddingConfig } = await import('@ktx/context');
const resolved = resolveLocalKtxEmbeddingConfig(config, deps.env ?? process.env);
if (!resolved) {
return check(
'warn',
@ -300,7 +300,7 @@ export async function runSetupDoctorChecks(deps: SetupDoctorDeps = {}): Promise<
'workspace-cli',
'Workspace-local CLI',
failureMessage(error),
'Run: pnpm run build && pnpm run klo -- --version',
'Run: pnpm run build && pnpm run ktx -- --version',
),
);
}
@ -308,11 +308,11 @@ export async function runSetupDoctorChecks(deps: SetupDoctorDeps = {}): Promise<
return checks;
}
async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): Promise<DoctorCheck[]> {
const { loadKloProject } = await import('@klo/context/project');
async function runProjectChecks(projectDir: string, deps: KtxDoctorDeps = {}): Promise<DoctorCheck[]> {
const { loadKtxProject } = await import('@ktx/context/project');
const checks: DoctorCheck[] = [];
try {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
checks.push(check('pass', 'project-config', 'Project config', project.config.project));
const connectionCount = Object.keys(project.config.connections).length;
checks.push(
@ -323,7 +323,7 @@ async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): P
'connections',
'Connections',
'0 configured',
'Add a connection to klo.yaml or run `klo setup demo init`',
'Add a connection to ktx.yaml or run `ktx setup demo init`',
),
);
checks.push(check('pass', 'storage', 'Storage', `${project.config.storage.state}/${project.config.storage.search}`));
@ -339,20 +339,20 @@ async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): P
'project-config',
'Project config',
failureMessage(error),
`Run: klo init ${projectDir} --name <project-name>`,
`Run: ktx init ${projectDir} --name <project-name>`,
),
);
}
return checks;
}
async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): Promise<DoctorCheck[]> {
async function runDemoProjectChecks(projectDir: string, deps: KtxDoctorDeps = {}): Promise<DoctorCheck[]> {
const env = deps.env ?? process.env;
const { DEMO_CONNECTION_ID, DEMO_REPLAY_FILE } = await import('./demo-assets.js');
const { loadKloProject } = await import('@klo/context/project');
const { loadKtxProject } = await import('@ktx/context/project');
const checks: DoctorCheck[] = [];
const requiredPaths = [
['demo-config', 'Demo config', 'klo.yaml'],
['demo-config', 'Demo config', 'ktx.yaml'],
['demo-database', 'Demo dataset', 'demo.db'],
['demo-state', 'Demo state database', 'state.sqlite'],
['demo-replay', 'Demo replay', join('replays', DEMO_REPLAY_FILE)],
@ -371,13 +371,13 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
id,
label,
`Missing ${relativePath}`,
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
),
);
}
try {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
const connection = project.config.connections[DEMO_CONNECTION_ID];
checks.push(
connection?.driver === 'sqlite'
@ -387,7 +387,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
'demo-connection',
'Demo connection',
`${DEMO_CONNECTION_ID} is missing or is not sqlite`,
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
),
);
const provider = project.config.llm.provider.backend;
@ -399,7 +399,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
'demo-llm-provider',
'Demo LLM provider',
provider,
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
),
);
if (provider === 'anthropic' && !env.ANTHROPIC_API_KEY) {
@ -409,7 +409,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
'anthropic-credentials',
'Anthropic credentials',
'ANTHROPIC_API_KEY is not set',
'Export ANTHROPIC_API_KEY to run `klo setup demo --mode full --no-input`',
'Export ANTHROPIC_API_KEY to run `ktx setup demo --mode full --no-input`',
),
);
} else {
@ -426,7 +426,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
'demo-config-parse',
'Demo config parse',
failureMessage(error),
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
),
);
}
@ -450,7 +450,7 @@ function hasFailures(report: DoctorReport): boolean {
return report.checks.some((item) => item.status === 'fail');
}
function writeReport(report: DoctorReport, outputMode: KloDoctorOutputMode, io: KloDoctorIo): void {
function writeReport(report: DoctorReport, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void {
if (outputMode === 'json') {
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
return;
@ -458,24 +458,24 @@ function writeReport(report: DoctorReport, outputMode: KloDoctorOutputMode, io:
io.stdout.write(formatDoctorReport(report));
}
export async function runKloDoctor(
args: KloDoctorArgs,
io: KloDoctorIo = process,
deps: KloDoctorDeps = {},
export async function runKtxDoctor(
args: KtxDoctorArgs,
io: KtxDoctorIo = process,
deps: KtxDoctorDeps = {},
): Promise<number> {
try {
const runSetupChecks = deps.runSetupChecks ?? (() => runSetupDoctorChecks());
const setupChecks = await runSetupChecks();
const report: DoctorReport =
args.command === 'setup'
? { title: 'KLO setup doctor', checks: setupChecks }
? { title: 'KTX setup doctor', checks: setupChecks }
: args.command === 'demo'
? {
title: 'KLO demo doctor',
title: 'KTX demo doctor',
checks: [...setupChecks, ...(await runDemoProjectChecks(args.projectDir, deps))],
}
: {
title: 'KLO project doctor',
title: 'KTX project doctor',
checks: [...setupChecks, ...(await runProjectChecks(args.projectDir, deps))],
};

View file

@ -70,7 +70,7 @@ describe('standalone local warehouse example', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-example-smoke-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-example-smoke-'));
});
afterEach(async () => {
@ -128,14 +128,14 @@ describe('standalone local warehouse example', () => {
]);
expect(ingest).toMatchObject({ code: 1, stdout: '' });
expect(ingest.stderr).toContain(
'klo dev ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner',
'ktx dev ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner',
);
}, 30_000);
it('serves local wiki and semantic-layer MCP tools against the copied example project', async () => {
const projectDir = await copyExampleProject(tempDir);
const client = new Client({ name: 'klo-example-client', version: '0.0.0' });
const client = new Client({ name: 'ktx-example-client', version: '0.0.0' });
const transport = new StdioClientTransport({
command: process.execPath,
args: [CLI_BIN, 'serve', '--mcp', 'stdio', '--project-dir', projectDir, '--user-id', 'example-user'],

View file

@ -1,5 +1,5 @@
import { buildDefaultKloProjectConfig, type KloProjectConnectionConfig } from '@klo/context/project';
import { HistoricSqlExtensionMissingError } from '@klo/context/ingest';
import { buildDefaultKtxProjectConfig, type KtxProjectConnectionConfig } from '@ktx/context/project';
import { HistoricSqlExtensionMissingError } from '@ktx/context/ingest';
import { describe, expect, it, vi } from 'vitest';
import {
runPostgresHistoricSqlDoctorChecks,
@ -7,14 +7,14 @@ import {
type PostgresHistoricSqlDoctorProbe,
} from './historic-sql-doctor.js';
function projectWithConnections(connections: Record<string, KloProjectConnectionConfig>): HistoricSqlDoctorProject {
function projectWithConnections(connections: Record<string, KtxProjectConnectionConfig>): HistoricSqlDoctorProject {
return {
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
config: {
...buildDefaultKloProjectConfig('warehouse'),
...buildDefaultKtxProjectConfig('warehouse'),
connections,
ingest: {
...buildDefaultKloProjectConfig('warehouse').ingest,
...buildDefaultKtxProjectConfig('warehouse').ingest,
adapters: ['live-database', 'historic-sql'],
},
},
@ -61,7 +61,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
);
expect(probe).toHaveBeenCalledWith({
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
connectionId: 'warehouse',
connection: {
driver: 'postgres',
@ -108,7 +108,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
status: 'warn',
detail:
'pg_stat_statements ready (PostgreSQL 16.4) with warnings: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
fix: 'Update the Postgres parameter group or config, then rerun `klo dev doctor --project-dir /tmp/klo-project`',
fix: 'Update the Postgres parameter group or config, then rerun `ktx dev doctor --project-dir /tmp/ktx-project`',
},
]);
});

View file

@ -1,15 +1,15 @@
import type { KloProjectConfig, KloProjectConnectionConfig } from '@klo/context/project';
import type { KtxProjectConfig, KtxProjectConnectionConfig } from '@ktx/context/project';
import type { DoctorCheck } from './doctor.js';
export interface HistoricSqlDoctorProject {
projectDir: string;
config: Pick<KloProjectConfig, 'connections' | 'ingest'>;
config: Pick<KtxProjectConfig, 'connections' | 'ingest'>;
}
export interface PostgresHistoricSqlDoctorProbeInput {
projectDir: string;
connectionId: string;
connection: KloProjectConnectionConfig;
connection: KtxProjectConnectionConfig;
env: NodeJS.ProcessEnv;
}
@ -31,19 +31,19 @@ function check(status: DoctorCheck['status'], id: string, label: string, detail:
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
}
function historicSqlRecord(connection: KloProjectConnectionConfig): Record<string, unknown> | null {
function historicSqlRecord(connection: KtxProjectConnectionConfig): Record<string, unknown> | null {
const historicSql = connection.historicSql;
return historicSql && typeof historicSql === 'object' && !Array.isArray(historicSql)
? (historicSql as Record<string, unknown>)
: null;
}
function isEnabledPostgresHistoricSql(connection: KloProjectConnectionConfig): boolean {
function isEnabledPostgresHistoricSql(connection: KtxProjectConnectionConfig): boolean {
const historicSql = historicSqlRecord(connection);
return historicSql?.enabled === true && historicSql.dialect === 'postgres';
}
function isPostgresDriver(connection: KloProjectConnectionConfig): boolean {
function isPostgresDriver(connection: KtxProjectConnectionConfig): boolean {
const driver = String(connection.driver ?? '').toLowerCase();
return driver === 'postgres' || driver === 'postgresql';
}
@ -62,7 +62,7 @@ function capabilityFailureFix(error: unknown, connectionId: string, projectDir:
if (error instanceof Error && error.name === 'HistoricSqlVersionUnsupportedError') {
return 'Use PostgreSQL 14 or newer, or disable historicSql for this connection';
}
return `Fix connections.${connectionId} Postgres settings, then rerun \`klo dev doctor --project-dir ${projectDir}\``;
return `Fix connections.${connectionId} Postgres settings, then rerun \`ktx dev doctor --project-dir ${projectDir}\``;
}
function failureDetail(error: unknown): string {
@ -75,14 +75,14 @@ function failureDetail(error: unknown): string {
async function defaultPostgresHistoricSqlProbe(
input: PostgresHistoricSqlDoctorProbeInput,
): Promise<PostgresHistoricSqlDoctorProbeResult> {
const [{ PostgresPgssQueryHistoryReader }, { KloPostgresHistoricSqlQueryClient, isKloPostgresConnectionConfig }] =
await Promise.all([import('@klo/context/ingest'), import('@klo/connector-postgres')]);
const [{ PostgresPgssQueryHistoryReader }, { KtxPostgresHistoricSqlQueryClient, isKtxPostgresConnectionConfig }] =
await Promise.all([import('@ktx/context/ingest'), import('@ktx/connector-postgres')]);
if (!isKloPostgresConnectionConfig(input.connection)) {
if (!isKtxPostgresConnectionConfig(input.connection)) {
throw new Error(`Native PostgreSQL connector cannot run driver "${input.connection.driver ?? 'unknown'}"`);
}
const client = new KloPostgresHistoricSqlQueryClient({
const client = new KtxPostgresHistoricSqlQueryClient({
connectionId: input.connectionId,
connection: input.connection,
env: input.env,
@ -135,7 +135,7 @@ export async function runPostgresHistoricSqlDoctorChecks(
checkId(connectionId),
label,
`pg_stat_statements ready (${result.pgServerVersion}) with warnings: ${result.warnings.join('; ')}`,
`Update the Postgres parameter group or config, then rerun \`klo dev doctor --project-dir ${project.projectDir}\``,
`Update the Postgres parameter group or config, then rerun \`ktx dev doctor --project-dir ${project.projectDir}\``,
),
);
} else {

View file

@ -5,11 +5,11 @@ import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
getKloCliPackageInfo,
getKtxCliPackageInfo,
rendererUnavailableVizFallback,
renderMemoryFlowTui,
resolveVizFallback,
runKloCli,
runKtxCli,
sanitizeMemoryFlowTuiError,
startLiveMemoryFlowTui,
warnVizFallbackOnce,
@ -39,20 +39,20 @@ function makeIo(options: { stdoutIsTty?: boolean } = {}) {
};
}
describe('getKloCliPackageInfo', () => {
describe('getKtxCliPackageInfo', () => {
it('identifies the CLI package and its context dependency', () => {
expect(getKloCliPackageInfo()).toEqual({
name: '@klo/cli',
expect(getKtxCliPackageInfo()).toEqual({
name: '@ktx/cli',
version: '0.0.0-private',
contextPackageName: '@klo/context',
contextPackageName: '@ktx/context',
});
});
it('exports package metadata for package managers and runtime diagnostics', () => {
const packageJson = require('@klo/cli/package.json') as { name: string; version: string };
const packageJson = require('@ktx/cli/package.json') as { name: string; version: string };
expect(packageJson).toMatchObject({
name: '@klo/cli',
name: '@ktx/cli',
version: '0.0.0-private',
});
});
@ -82,11 +82,11 @@ describe('memory-flow renderer exports', () => {
});
});
describe('runKloCli', () => {
describe('runKtxCli', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-'));
});
afterEach(async () => {
@ -96,18 +96,18 @@ describe('runKloCli', () => {
it('prints version information', async () => {
const testIo = makeIo();
await expect(runKloCli(['--version'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['--version'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toBe('@klo/cli 0.0.0-private\n');
expect(testIo.stdout()).toBe('@ktx/cli 0.0.0-private\n');
expect(testIo.stderr()).toBe('');
});
it('prints the May 6 public command surface in root help', async () => {
const testIo = makeIo();
await expect(runKloCli(['--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'serve', 'status']) {
expect(testIo.stdout()).toContain(`${command}`);
}
@ -116,22 +116,22 @@ describe('runKloCli', () => {
expect(testIo.stdout()).not.toContain(`${removed} `);
}
expect(testIo.stdout()).toContain('--project-dir <path>');
expect(testIo.stdout()).toContain('KLO_PROJECT_DIR');
expect(testIo.stdout()).toContain('KTX_PROJECT_DIR');
expect(testIo.stdout()).toContain('--debug');
expect(testIo.stdout()).not.toContain('--' + 'verbose');
expect(testIo.stdout()).toContain('Advanced:');
expect(testIo.stdout()).toContain('klo dev');
expect(testIo.stdout()).toContain('ktx dev');
expect(testIo.stderr()).toBe('');
});
it('exposes demo under setup help instead of root help', async () => {
const testIo = makeIo();
await expect(runKloCli(['setup', '--help'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['setup', '--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo setup [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx setup [options] [command]');
expect(testIo.stdout()).toContain('demo');
expect(testIo.stdout()).toContain('Run the packaged KLO demo from setup');
expect(testIo.stdout()).toContain('Run the packaged KTX demo from setup');
expect(testIo.stdout()).not.toContain('--skip-llm');
expect(testIo.stdout()).not.toContain('--skip-embeddings');
expect(testIo.stdout()).not.toContain('--embedding-model');
@ -140,33 +140,33 @@ describe('runKloCli', () => {
expect(testIo.stderr()).toBe('');
});
it('prints help for bare klo outside a TTY', async () => {
it('prints help for bare ktx outside a TTY', async () => {
const setup = vi.fn(async () => 0);
const testIo = makeIo({ stdoutIsTty: false });
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
expect(setup).not.toHaveBeenCalled();
expect(testIo.stderr()).toBe('');
});
it('starts setup for bare klo in a TTY when no project is discoverable', async () => {
it('starts setup for bare ktx in a TTY when no project is discoverable', async () => {
const { mkdtemp, realpath, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const originalCwd = process.cwd();
const tempDir = await mkdtemp(join(tmpdir(), 'klo-bare-setup-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-bare-setup-'));
const setup = vi.fn(async () => 0);
const testIo = makeIo({ stdoutIsTty: true });
const previousProjectDir = process.env.KLO_PROJECT_DIR;
const previousProjectDir = process.env.KTX_PROJECT_DIR;
const expectedProjectDir = await realpath(tempDir);
try {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
process.chdir(tempDir);
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
expect(setup).toHaveBeenCalledWith(
{
@ -187,71 +187,71 @@ describe('runKloCli', () => {
},
testIo.io,
);
expect(testIo.stdout()).not.toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).not.toContain('Usage: ktx [options] [command]');
expect(testIo.stderr()).toBe('');
} finally {
process.chdir(originalCwd);
if (previousProjectDir === undefined) {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
} else {
process.env.KLO_PROJECT_DIR = previousProjectDir;
process.env.KTX_PROJECT_DIR = previousProjectDir;
}
await rm(tempDir, { recursive: true, force: true });
}
});
it('prints help without project status for bare klo in a TTY when a project is discoverable', async () => {
it('prints help without project status for bare ktx in a TTY when a project is discoverable', async () => {
const { mkdtemp, realpath, rm, writeFile } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const originalCwd = process.cwd();
const previousProjectDir = process.env.KLO_PROJECT_DIR;
const tempDir = await mkdtemp(join(tmpdir(), 'klo-bare-existing-'));
const previousProjectDir = process.env.KTX_PROJECT_DIR;
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-bare-existing-'));
const setup = vi.fn(async () => 0);
const testIo = makeIo({ stdoutIsTty: true });
const expectedProjectDir = await realpath(tempDir);
try {
delete process.env.KLO_PROJECT_DIR;
await writeFile(join(tempDir, 'klo.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
delete process.env.KTX_PROJECT_DIR;
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
process.chdir(tempDir);
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
expect(testIo.stdout()).not.toContain(`Project: ${expectedProjectDir}`);
expect(setup).not.toHaveBeenCalled();
} finally {
process.chdir(originalCwd);
if (previousProjectDir === undefined) {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
} else {
process.env.KLO_PROJECT_DIR = previousProjectDir;
process.env.KTX_PROJECT_DIR = previousProjectDir;
}
await rm(tempDir, { recursive: true, force: true });
}
});
it('does not invoke status for bare klo in a TTY when status would fail', async () => {
it('does not invoke status for bare ktx in a TTY when status would fail', async () => {
const setup = vi.fn(async () => {
throw new Error('Unsupported ingest.llm: use top-level llm.provider, llm.models, and ingest.workUnits');
});
const testIo = makeIo({ stdoutIsTty: true });
const previousProjectDir = process.env.KLO_PROJECT_DIR;
const previousProjectDir = process.env.KTX_PROJECT_DIR;
try {
process.env.KLO_PROJECT_DIR = tempDir;
process.env.KTX_PROJECT_DIR = tempDir;
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
expect(setup).not.toHaveBeenCalled();
expect(testIo.stderr()).toBe('');
} finally {
if (previousProjectDir === undefined) {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
} else {
process.env.KLO_PROJECT_DIR = previousProjectDir;
process.env.KTX_PROJECT_DIR = previousProjectDir;
}
}
});
@ -260,7 +260,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
const removedVerboseOption = '--' + 'verbose';
await expect(runKloCli([removedVerboseOption, 'connection', 'list'], testIo.io)).resolves.toBe(1);
await expect(runKtxCli([removedVerboseOption, 'connection', 'list'], testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toContain(`unknown option '${removedVerboseOption}'`);
expect(testIo.stdout()).toBe('');
@ -270,12 +270,12 @@ describe('runKloCli', () => {
const testIo = makeIo();
const zshWords = '$' + '{words[@]}';
await expect(runKloCli(['dev', 'completion', 'zsh'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'completion', 'zsh'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('#compdef klo');
expect(testIo.stdout()).toContain('KLO_COMPLETION_COMMAND:-klo');
expect(testIo.stdout()).toContain('#compdef ktx');
expect(testIo.stdout()).toContain('KTX_COMPLETION_COMMAND:-ktx');
expect(testIo.stdout()).toContain(`dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}"`);
expect(testIo.stdout()).toContain('compdef _klo klo');
expect(testIo.stdout()).toContain('compdef _ktx ktx');
expect(testIo.stderr()).toBe('');
});
@ -283,22 +283,22 @@ describe('runKloCli', () => {
const testIo = makeIo();
const previousHome = process.env.HOME;
const previousZdotdir = process.env.ZDOTDIR;
const tempHome = await mkdtemp(join(tmpdir(), 'klo-completion-home-'));
const tempHome = await mkdtemp(join(tmpdir(), 'ktx-completion-home-'));
try {
process.env.HOME = tempHome;
delete process.env.ZDOTDIR;
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], testIo.io)).resolves.toBe(0);
const completionFile = await readFile(join(tempHome, '.zfunc', '_klo'), 'utf-8');
const completionFile = await readFile(join(tempHome, '.zfunc', '_ktx'), 'utf-8');
const zshrc = await readFile(join(tempHome, '.zshrc'), 'utf-8');
expect(completionFile).toContain('#compdef klo');
expect(zshrc).toContain('# >>> klo completion >>>');
expect(zshrc).toContain('_klo_completion_command()');
expect(zshrc).toContain('"name": "klo-workspace"');
expect(zshrc).toContain('scripts/run-klo.mjs');
expect(zshrc).toContain("export KLO_COMPLETION_COMMAND='$(_klo_completion_command)'");
expect(completionFile).toContain('#compdef ktx');
expect(zshrc).toContain('# >>> ktx completion >>>');
expect(zshrc).toContain('_ktx_completion_command()');
expect(zshrc).toContain('"name": "ktx-workspace"');
expect(zshrc).toContain('scripts/run-ktx.mjs');
expect(zshrc).toContain("export KTX_COMPLETION_COMMAND='$(_ktx_completion_command)'");
expect(zshrc).toContain('setopt complete_aliases');
expect(zshrc).toContain('fpath=("$HOME/.zfunc" $fpath)');
expect(zshrc).toContain('autoload -Uz compinit');
@ -326,20 +326,20 @@ describe('runKloCli', () => {
const secondIo = makeIo();
const previousHome = process.env.HOME;
const previousZdotdir = process.env.ZDOTDIR;
const tempHome = await mkdtemp(join(tmpdir(), 'klo-completion-home-'));
const tempHome = await mkdtemp(join(tmpdir(), 'ktx-completion-home-'));
try {
process.env.HOME = tempHome;
delete process.env.ZDOTDIR;
await writeFile(join(tempHome, '.zshrc'), 'export EDITOR=vim\nautoload -Uz compinit\ncompinit\n', 'utf-8');
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], firstIo.io)).resolves.toBe(0);
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], secondIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], firstIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], secondIo.io)).resolves.toBe(0);
const zshrc = await readFile(join(tempHome, '.zshrc'), 'utf-8');
expect(zshrc.match(/# >>> klo completion >>>/g)).toHaveLength(1);
expect(zshrc.match(/# >>> ktx completion >>>/g)).toHaveLength(1);
expect(zshrc.indexOf('fpath=("$HOME/.zfunc" $fpath)')).toBeLessThan(zshrc.indexOf('autoload -Uz compinit'));
expect(zshrc.match(/_klo_completion_command\(\)/g)).toHaveLength(1);
expect(zshrc.match(/_ktx_completion_command\(\)/g)).toHaveLength(1);
expect(zshrc.match(/^compinit$/gm)).toHaveLength(1);
expect(secondIo.stdout()).toContain('Updated zsh config:');
expect(firstIo.stderr()).toBe('');
@ -364,11 +364,11 @@ describe('runKloCli', () => {
const connectionIo = makeIo();
await expect(
runKloCli(['dev', '__complete', '--shell', 'zsh', '--position', '2', '--', 'klo', 'co'], rootIo.io),
runKtxCli(['dev', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', 'co'], rootIo.io),
).resolves.toBe(0);
await expect(
runKloCli(
['dev', '__complete', '--shell', 'zsh', '--position', '3', '--', 'klo', 'connection', 'm'],
runKtxCli(
['dev', '__complete', '--shell', 'zsh', '--position', '3', '--', 'ktx', 'connection', 'm'],
connectionIo.io,
),
).resolves.toBe(0);
@ -386,13 +386,13 @@ describe('runKloCli', () => {
const choiceIo = makeIo();
await expect(
runKloCli(
['dev', '__complete', '--shell', 'zsh', '--position', '4', '--', 'klo', 'connection', 'add', '--cr'],
runKtxCli(
['dev', '__complete', '--shell', 'zsh', '--position', '4', '--', 'ktx', 'connection', 'add', '--cr'],
optionIo.io,
),
).resolves.toBe(0);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'__complete',
@ -401,7 +401,7 @@ describe('runKloCli', () => {
'--position',
'7',
'--',
'klo',
'ktx',
'connection',
'add',
'notion',
@ -425,7 +425,7 @@ describe('runKloCli', () => {
const serveStdio = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', tempDir, 'serve', '--mcp', 'stdio', '--user-id', 'agent'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'serve', '--mcp', 'stdio', '--user-id', 'agent'], testIo.io, {
serveStdio,
}),
).resolves.toBe(0);
@ -447,7 +447,7 @@ describe('runKloCli', () => {
const ingest = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', '/tmp/project', 'ingest', 'warehouse'], testIo.io, { publicIngest: ingest }),
runKtxCli(['--project-dir', '/tmp/project', 'ingest', 'warehouse'], testIo.io, { publicIngest: ingest }),
).resolves.toBe(0);
expect(ingest).toHaveBeenCalledWith(
@ -469,10 +469,10 @@ describe('runKloCli', () => {
const lowLevelIngest = vi.fn(async () => 0);
await expect(
runKloCli(['ingest', 'watch', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest }),
runKtxCli(['ingest', 'watch', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest }),
).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo ingest watch [options] [runId]');
expect(testIo.stdout()).toContain('Usage: ktx ingest watch [options] [runId]');
expect(testIo.stdout()).toContain('[runId]');
expect(testIo.stdout()).toContain('--project-dir <path>');
expect(testIo.stdout()).toContain('--json');
@ -488,12 +488,12 @@ describe('runKloCli', () => {
const publicIngest = vi.fn(async () => 0);
await expect(
runKloCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json', '--no-input'], statusIo.io, {
runKtxCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json', '--no-input'], statusIo.io, {
publicIngest,
}),
).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'ingest', 'watch', '--no-input'], watchIo.io, {
runKtxCli(['--project-dir', tempDir, 'ingest', 'watch', '--no-input'], watchIo.io, {
publicIngest,
}),
).resolves.toBe(0);
@ -527,7 +527,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
const demo = vi.fn().mockResolvedValue(0);
await expect(runKloCli(['demo', '--mode', 'replay', '--no-input'], testIo.io, { demo })).resolves.toBe(1);
await expect(runKtxCli(['demo', '--mode', 'replay', '--no-input'], testIo.io, { demo })).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown command|error:/i);
expect(demo).not.toHaveBeenCalled();
@ -538,7 +538,7 @@ describe('runKloCli', () => {
const demo = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'replay', '--no-input'], testIo.io, { demo }),
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'replay', '--no-input'], testIo.io, { demo }),
).resolves.toBe(0);
expect(demo).toHaveBeenCalledWith(
@ -553,7 +553,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'seeded', '--no-input'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'seeded', '--no-input'], testIo.io, {
demo,
}),
).resolves.toBe(0);
@ -569,7 +569,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', '--mode', 'seeded'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', '--mode', 'seeded'], testIo.io, {
demo,
}),
).resolves.toBe(0);
@ -585,7 +585,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'demo', 'inspect', '--no-input'], testIo.io, { demo }),
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', 'inspect', '--no-input'], testIo.io, { demo }),
).resolves.toBe(0);
expect(demo).toHaveBeenCalledWith(
{
@ -603,7 +603,7 @@ describe('runKloCli', () => {
const demo = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input'], testIo.io, {
demo,
}),
).resolves.toBe(0);
@ -621,7 +621,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', 'ingest', '--mode', 'seeded'], testIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', 'ingest', '--mode', 'seeded'], testIo.io, {
demo,
}),
).resolves.toBe(0);
@ -639,7 +639,7 @@ describe('runKloCli', () => {
demo.mockClear();
await expect(
runKloCli(
runKtxCli(
['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input', '--plain'],
testIo.io,
{
@ -665,16 +665,16 @@ describe('runKloCli', () => {
const publicIngest = vi.fn();
const lowLevelIngest = vi.fn();
await expect(runKloCli(['ingest', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(0);
await expect(runKtxCli(['ingest', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo ingest [options] [connectionId]');
expect(testIo.stdout()).toContain('Build and refresh KLO context from configured sources');
expect(testIo.stdout()).toContain('Usage: ktx ingest [options] [connectionId]');
expect(testIo.stdout()).toContain('Build and refresh KTX context from configured sources');
expect(testIo.stdout()).toContain('status');
expect(testIo.stdout()).toContain('watch');
expect(testIo.stdout()).toContain('klo ingest --all [options]');
expect(testIo.stdout()).toContain('klo ingest status [runId] [options]');
expect(testIo.stdout()).toContain('klo ingest watch [runId] [options]');
expect(testIo.stdout()).not.toContain('klo ingest replay <runId> [options]');
expect(testIo.stdout()).toContain('ktx ingest --all [options]');
expect(testIo.stdout()).toContain('ktx ingest status [runId] [options]');
expect(testIo.stdout()).toContain('ktx ingest watch [runId] [options]');
expect(testIo.stdout()).not.toContain('ktx ingest replay <runId> [options]');
expect(testIo.stdout()).toContain('--no-input');
expect(testIo.stdout()).not.toContain('--adapter');
expect(testIo.stderr()).toBe('');
@ -689,20 +689,20 @@ describe('runKloCli', () => {
const publicIngest = vi.fn(async () => 0);
const lowLevelIngest = vi.fn(async () => 0);
await expect(runKloCli(['ingest', 'run'], publicRunIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(
await expect(runKtxCli(['ingest', 'run'], publicRunIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(
1,
);
expect(publicRunIo.stderr()).toMatch(/invalid argument|reserved|run/i);
expect(publicIngest).not.toHaveBeenCalled();
await expect(
runKloCli(['ingest', 'run', '--help'], publicHelpIo.io, { publicIngest, ingest: lowLevelIngest }),
runKtxCli(['ingest', 'run', '--help'], publicHelpIo.io, { publicIngest, ingest: lowLevelIngest }),
).resolves.toBe(0);
expect(publicHelpIo.stdout()).toContain('Usage: klo ingest [options] [connectionId]');
expect(publicHelpIo.stdout()).not.toContain('Usage: klo ingest ' + 'run');
expect(publicHelpIo.stdout()).toContain('Usage: ktx ingest [options] [connectionId]');
expect(publicHelpIo.stdout()).not.toContain('Usage: ktx ingest ' + 'run');
await expect(
runKloCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
runKtxCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
publicIngest,
ingest: lowLevelIngest,
}),
@ -720,11 +720,11 @@ describe('runKloCli', () => {
const ingestRunIo = makeIo();
const ingestReplayHelpIo = makeIo();
await expect(runKloCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(
await expect(runKtxCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(
0,
);
await expect(
runKloCli(
runKtxCli(
[
'dev',
'ingest',
@ -746,7 +746,7 @@ describe('runKloCli', () => {
{ ingest },
),
).resolves.toBe(0);
await expect(runKloCli(['dev', 'ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0);
expect(doctor).toHaveBeenCalledWith({ command: 'setup', outputMode: 'json', inputMode: 'disabled' }, doctorIo.io);
expect(ingest).toHaveBeenCalledWith(
@ -763,7 +763,7 @@ describe('runKloCli', () => {
},
ingestRunIo.io,
);
expect(ingestReplayHelpIo.stdout()).toContain('Usage: klo dev ingest replay [options] <runId>');
expect(ingestReplayHelpIo.stdout()).toContain('Usage: ktx dev ingest replay [options] <runId>');
expect(ingestReplayHelpIo.stdout()).toContain('<runId>');
expect(doctorIo.stderr()).toBe('');
expect(ingestRunIo.stderr()).toBe('');
@ -774,7 +774,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
const connection = vi.fn(async () => 0);
await expect(runKloCli(['--project-dir', tempDir, 'connection', 'list'], testIo.io, { connection })).resolves.toBe(
await expect(runKtxCli(['--project-dir', tempDir, 'connection', 'list'], testIo.io, { connection })).resolves.toBe(
0,
);
@ -788,9 +788,9 @@ describe('runKloCli', () => {
const statusIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'status', '--json'], setupIo.io, { setup }),
runKtxCli(['--project-dir', tempDir, 'setup', 'status', '--json'], setupIo.io, { setup }),
).resolves.toBe(0);
await expect(runKloCli(['--project-dir', tempDir, 'status', '--json'], statusIo.io, { setup })).resolves.toBe(0);
await expect(runKtxCli(['--project-dir', tempDir, 'status', '--json'], statusIo.io, { setup })).resolves.toBe(0);
expect(setup).toHaveBeenNthCalledWith(1, { command: 'status', projectDir: tempDir, json: true }, setupIo.io);
expect(setup).toHaveBeenNthCalledWith(2, { command: 'status', projectDir: tempDir, json: true }, statusIo.io);
@ -803,21 +803,21 @@ describe('runKloCli', () => {
const statusIo = makeIo();
const stopIo = makeIo();
await expect(runKloCli(['--project-dir', tempDir, 'setup', 'context', 'build'], buildIo.io, { setup })).resolves.toBe(
await expect(runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'build'], buildIo.io, { setup })).resolves.toBe(
0,
);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'watch', 'setup-context-local-1'], watchIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'watch', 'setup-context-local-1'], watchIo.io, {
setup,
}),
).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'status', 'setup-context-local-1', '--json'], statusIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'status', 'setup-context-local-1', '--json'], statusIo.io, {
setup,
}),
).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'stop', 'setup-context-local-1'], stopIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'stop', 'setup-context-local-1'], stopIo.io, {
setup,
}),
).resolves.toBe(0);
@ -849,7 +849,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -883,7 +883,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -907,7 +907,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -943,7 +943,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'setup',
'--project-dir',
@ -997,7 +997,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1045,7 +1045,7 @@ describe('runKloCli', () => {
const removeIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1064,7 +1064,7 @@ describe('runKloCli', () => {
),
).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'setup', 'remove', '--agents'], removeIo.io, { setup }),
runKtxCli(['--project-dir', tempDir, 'setup', 'remove', '--agents'], removeIo.io, { setup }),
).resolves.toBe(0);
expect(setup).toHaveBeenNthCalledWith(
@ -1088,7 +1088,7 @@ describe('runKloCli', () => {
const testIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1115,7 +1115,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'deterministic'], setupIo.io, { setup }),
runKtxCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'deterministic'], setupIo.io, { setup }),
).resolves.toBe(1);
expect(setup).not.toHaveBeenCalled();
@ -1127,7 +1127,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'gateway'], setupIo.io, { setup }),
runKtxCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'gateway'], setupIo.io, { setup }),
).resolves.toBe(1);
expect(setup).not.toHaveBeenCalled();
@ -1139,7 +1139,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1165,7 +1165,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'setup', '--enable-historic-sql', '--disable-historic-sql'], setupIo.io, {
runKtxCli(['--project-dir', tempDir, 'setup', '--enable-historic-sql', '--disable-historic-sql'], setupIo.io, {
setup,
}),
).resolves.toBe(1);
@ -1179,12 +1179,12 @@ describe('runKloCli', () => {
const toolsIo = makeIo();
const agent = vi.fn(async () => 0);
await expect(runKloCli(['agent', '--help'], helpIo.io, { agent })).resolves.toBe(0);
await expect(runKtxCli(['agent', '--help'], helpIo.io, { agent })).resolves.toBe(0);
await expect(
runKloCli(['--project-dir', tempDir, 'agent', 'tools', '--json'], toolsIo.io, { agent }),
runKtxCli(['--project-dir', tempDir, 'agent', 'tools', '--json'], toolsIo.io, { agent }),
).resolves.toBe(0);
expect(helpIo.stdout()).toContain('Usage: klo agent');
expect(helpIo.stdout()).toContain('Usage: ktx agent');
expect(toolsIo.stderr()).toBe('');
expect(agent).toHaveBeenCalledWith({ command: 'tools', projectDir: tempDir, json: true }, toolsIo.io);
});
@ -1277,13 +1277,13 @@ describe('runKloCli', () => {
for (const entry of cases) {
const io = makeIo();
await expect(runKloCli(entry.argv, io.io, { agent })).resolves.toBe(0);
await expect(runKtxCli(entry.argv, io.io, { agent })).resolves.toBe(0);
expect(agent).toHaveBeenLastCalledWith(entry.args, io.io);
expect(io.stderr()).toBe('');
}
const helpIo = makeIo();
await expect(runKloCli(['--help'], helpIo.io, { agent })).resolves.toBe(0);
await expect(runKtxCli(['--help'], helpIo.io, { agent })).resolves.toBe(0);
expect(helpIo.stdout()).not.toContain('agent ');
});
@ -1323,7 +1323,7 @@ describe('runKloCli', () => {
const io = makeIo();
await expect(
runKloCli(
runKtxCli(
['--project-dir', tempDir, 'agent', 'sl', 'list', '--json', '--connection-id', 'warehouse', '--query', 'paid'],
io.io,
{ agent },
@ -1376,7 +1376,7 @@ describe('runKloCli', () => {
const io = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'agent', 'wiki', 'search', 'paid order', '--json', '--limit', '5'], io.io, {
runKtxCli(['--project-dir', tempDir, 'agent', 'wiki', 'search', 'paid order', '--json', '--limit', '5'], io.io, {
agent,
}),
).resolves.toBe(0);
@ -1394,23 +1394,23 @@ describe('runKloCli', () => {
});
it('dispatches public connection subcommands through the existing connection implementation', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'klo-connection-dispatch-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-connection-dispatch-'));
const connection = vi.fn(async () => 0);
await expect(
runKloCli(['--project-dir', tempDir, 'connection', 'list'], makeIo().io, { connection }),
runKtxCli(['--project-dir', tempDir, 'connection', 'list'], makeIo().io, { connection }),
).resolves.toBe(0);
const removeIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'connection', 'remove', 'warehouse', '--force', '--no-input'], removeIo.io, {
runKtxCli(['--project-dir', tempDir, 'connection', 'remove', 'warehouse', '--force', '--no-input'], removeIo.io, {
connection,
}),
).resolves.toBe(0);
const mapIo = makeIo();
await expect(
runKloCli(['--project-dir', tempDir, 'connection', 'map', 'prod-metabase', '--json'], mapIo.io, {
runKtxCli(['--project-dir', tempDir, 'connection', 'map', 'prod-metabase', '--json'], mapIo.io, {
connection,
}),
).resolves.toBe(0);
@ -1444,9 +1444,9 @@ describe('runKloCli', () => {
it('prints help for connection metabase setup', async () => {
const helpIo = makeIo();
await expect(runKloCli(['connection', 'metabase', 'setup', '--help'], helpIo.io)).resolves.toBe(0);
await expect(runKtxCli(['connection', 'metabase', 'setup', '--help'], helpIo.io)).resolves.toBe(0);
expect(helpIo.stdout()).toContain('Usage: klo connection metabase setup');
expect(helpIo.stdout()).toContain('Usage: ktx connection metabase setup');
for (const option of [
'--id <connectionId>',
'--url <url>',
@ -1465,10 +1465,10 @@ describe('runKloCli', () => {
}
expect(helpIo.stdout()).toContain('Guided equivalent of:');
for (const line of [
'klo connection mapping refresh <connectionId> --auto-accept',
'klo connection mapping set <connectionId> databaseMappings <id>=<target>',
'klo connection mapping set-sync-enabled <connectionId> <id> --enabled true',
'klo ingest <connectionId>',
'ktx connection mapping refresh <connectionId> --auto-accept',
'ktx connection mapping set <connectionId> databaseMappings <id>=<target>',
'ktx connection mapping set-sync-enabled <connectionId> <id> --enabled true',
'ktx ingest <connectionId>',
]) {
expect(helpIo.stdout()).toContain(line);
}
@ -1481,7 +1481,7 @@ describe('runKloCli', () => {
const setupIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'connection',
'metabase',
@ -1598,7 +1598,7 @@ describe('runKloCli', () => {
],
]) {
const testIo = makeIo();
await expect(runKloCli(argv, testIo.io, { connectionMetabaseSetup })).resolves.toBe(1);
await expect(runKtxCli(argv, testIo.io, { connectionMetabaseSetup })).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/map|sync|sync-mode|conflict|cannot be used|invalid|integer|choices/i);
}
@ -1615,7 +1615,7 @@ describe('runKloCli', () => {
]) {
const testIo = makeIo();
await expect(runKloCli(argv, testIo.io)).resolves.toBe(1);
await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown command|error:/);
}
@ -1626,7 +1626,7 @@ describe('runKloCli', () => {
const connection = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'connection',
'add',
@ -1688,9 +1688,9 @@ describe('runKloCli', () => {
const testIo = makeIo();
const connectionNotion = vi.fn(async () => 0);
await expect(runKloCli(argv, testIo.io, { connectionNotion })).resolves.toBe(0);
await expect(runKtxCli(argv, testIo.io, { connectionNotion })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo connection notion');
expect(testIo.stdout()).toContain('Usage: ktx connection notion');
expect(testIo.stdout()).toContain('pick');
expect(testIo.stderr()).toBe('');
expect(connectionNotion).not.toHaveBeenCalled();
@ -1702,7 +1702,7 @@ describe('runKloCli', () => {
const connectionNotion = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'--project-dir',
tempDir,
@ -1739,7 +1739,7 @@ describe('runKloCli', () => {
const connectionNotion = vi.fn(async () => 0);
await expect(
runKloCli(['connection', 'notion', 'pick', 'notion-main', '--root-page-id', 'not-a-uuid'], testIo.io, {
runKtxCli(['connection', 'notion', 'pick', 'notion-main', '--root-page-id', 'not-a-uuid'], testIo.io, {
connectionNotion,
}),
).resolves.toBe(0);
@ -1761,7 +1761,7 @@ describe('runKloCli', () => {
const connectionNotion = vi.fn(async () => 0);
await expect(
runKloCli(['connection', 'notion', 'pick', 'notion-main', '--no-input'], testIo.io, { connectionNotion }),
runKtxCli(['connection', 'notion', 'pick', 'notion-main', '--no-input'], testIo.io, { connectionNotion }),
).resolves.toBe(1);
expect(connectionNotion).not.toHaveBeenCalled();
@ -1773,18 +1773,18 @@ describe('runKloCli', () => {
const connection = vi.fn().mockResolvedValue(0);
await expect(
runKloCli(['--project-dir', tempDir, '--debug', 'connection', 'list'], testIo.io, { connection }),
runKtxCli(['--project-dir', tempDir, '--debug', 'connection', 'list'], testIo.io, { connection }),
).resolves.toBe(0);
expect(testIo.stderr()).toContain(`[debug] projectDir=${tempDir}`);
expect(testIo.stderr()).toContain('[debug] dispatch=connection');
});
it('routes low-level scan through klo dev with top-level project-dir', async () => {
it('routes low-level scan through ktx dev with top-level project-dir', async () => {
const testIo = makeIo();
const scan = vi.fn().mockResolvedValue(0);
await expect(runKloCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse'], testIo.io, { scan })).resolves.toBe(
await expect(runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse'], testIo.io, { scan })).resolves.toBe(
0,
);
@ -1807,7 +1807,7 @@ describe('runKloCli', () => {
const serveStdio = vi.fn(async () => 0);
await expect(
runKloCli(
runKtxCli(
[
'serve',
'--mcp',
@ -1843,9 +1843,9 @@ describe('runKloCli', () => {
it('prints dev help for bare dev commands', async () => {
const testIo = makeIo();
await expect(runKloCli(['dev'], testIo.io)).resolves.toBe(0);
await expect(runKtxCli(['dev'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo dev [options] [command]');
expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
expect(testIo.stdout()).toContain('Low-level diagnostics');
expect(testIo.stdout()).toContain('scan');
expect(testIo.stdout()).toContain('ingest');
@ -1857,15 +1857,15 @@ describe('runKloCli', () => {
it('prints dev command help without invoking low-level execution', async () => {
for (const [command, expected] of [
['scan', ['Usage: klo dev scan', '--dry-run', 'status', 'report']],
['ingest', ['Usage: klo dev ingest', 'run', 'replay']],
['mapping', ['Usage: klo dev mapping', 'sync-state', 'validate']],
['scan', ['Usage: ktx dev scan', '--dry-run', 'status', 'report']],
['ingest', ['Usage: ktx dev ingest', 'run', 'replay']],
['mapping', ['Usage: ktx dev mapping', 'sync-state', 'validate']],
] as const) {
const testIo = makeIo();
const scan = vi.fn().mockResolvedValue(0);
const sl = vi.fn().mockResolvedValue(0);
await expect(runKloCli(['dev', command, '--help'], testIo.io, { scan, sl })).resolves.toBe(0);
await expect(runKtxCli(['dev', command, '--help'], testIo.io, { scan, sl })).resolves.toBe(0);
for (const text of expected) {
expect(testIo.stdout()).toContain(text);
@ -1880,9 +1880,9 @@ describe('runKloCli', () => {
const testIo = makeIo();
const scan = vi.fn().mockResolvedValue(0);
await expect(runKloCli(['dev', 'scan', 'report', '--help'], testIo.io, { scan })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'scan', 'report', '--help'], testIo.io, { scan })).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: klo dev scan report [options] <runId>');
expect(testIo.stdout()).toContain('Usage: ktx dev scan report [options] <runId>');
expect(testIo.stderr()).toBe('');
expect(scan).not.toHaveBeenCalled();
});
@ -1890,7 +1890,7 @@ describe('runKloCli', () => {
it('rejects removed reserved dev subcommands', async () => {
const testIo = makeIo();
await expect(runKloCli(['dev', 'artifacts'], testIo.io)).resolves.toBe(1);
await expect(runKtxCli(['dev', 'artifacts'], testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown command|error:/);
});
@ -1906,7 +1906,7 @@ describe('runKloCli', () => {
['setup', 'demo', 'replay', '--json', '--plain'],
]) {
const testIo = makeIo();
await expect(runKloCli(argv, testIo.io, { ingest, demo })).resolves.toBe(1);
await expect(runKtxCli(argv, testIo.io, { ingest, demo })).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/conflict|cannot be used/i);
}
@ -1920,7 +1920,7 @@ describe('runKloCli', () => {
const tokenIo = makeIo();
await expect(
runKloCli(
runKtxCli(
[
'connection',
'add',
@ -1946,14 +1946,14 @@ describe('runKloCli', () => {
it('validates connection mapping set syntax before runner domain validation', async () => {
const badFieldIo = makeIo();
await expect(
runKloCli(['connection', 'mapping', 'set', 'prod-metabase', 'invalidMappings', '1=warehouse'], badFieldIo.io),
runKtxCli(['connection', 'mapping', 'set', 'prod-metabase', 'invalidMappings', '1=warehouse'], badFieldIo.io),
).resolves.toBe(1);
expect(badFieldIo.stderr()).toContain('databaseMappings or connectionMappings');
for (const assignment of ['missing-equals', '=warehouse', '1=']) {
const testIo = makeIo();
await expect(
runKloCli(['connection', 'mapping', 'set', 'prod-metabase', 'databaseMappings', assignment], testIo.io),
runKtxCli(['connection', 'mapping', 'set', 'prod-metabase', 'databaseMappings', assignment], testIo.io),
).resolves.toBe(1);
expect(testIo.stderr()).toContain('non-empty <key>=<value>');
}
@ -1962,7 +1962,7 @@ describe('runKloCli', () => {
it('does not expose root init after setup owns project creation', async () => {
const testIo = makeIo();
await expect(runKloCli(['init'], testIo.io)).resolves.toBe(1);
await expect(runKtxCli(['init'], testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toContain("error: unknown command 'init'");
});
@ -1970,7 +1970,7 @@ describe('runKloCli', () => {
it('returns an error code for unknown commands', async () => {
const testIo = makeIo();
await expect(runKloCli(['unknown'], testIo.io)).resolves.toBe(1);
await expect(runKtxCli(['unknown'], testIo.io)).resolves.toBe(1);
expect(testIo.stderr()).toContain("error: unknown command 'unknown'");
});

View file

@ -1,48 +1,48 @@
import { profileMark } from './startup-profile.js';
export {
getKloCliPackageInfo,
getKtxCliPackageInfo,
runInitForCommander,
runKloCli,
type KloCliDeps,
type KloCliIo,
type KloCliPackageInfo,
runKtxCli,
type KtxCliDeps,
type KtxCliIo,
type KtxCliPackageInfo,
} from './cli-runtime.js';
export { runKloAgent, type KloAgentArgs } from './agent.js';
export { runKtxAgent, type KtxAgentArgs } from './agent.js';
export {
KLO_AGENT_MAX_ROWS_CAP,
createKloAgentRuntime,
KTX_AGENT_MAX_ROWS_CAP,
createKtxAgentRuntime,
parseAgentMaxRows,
readAgentJsonFile,
writeAgentJson,
writeAgentJsonError,
type KloAgentRuntime,
type KloAgentRuntimeDeps,
type KtxAgentRuntime,
type KtxAgentRuntimeDeps,
} from './agent-runtime.js';
export { runKloSetup, type KloSetupArgs, type KloSetupStatus } from './setup.js';
export { runKtxSetup, type KtxSetupArgs, type KtxSetupStatus } from './setup.js';
export type {
KloSetupDatabaseDriver,
KloSetupDatabasesArgs,
KloSetupDatabasesDeps,
KloSetupDatabasesResult,
KtxSetupDatabaseDriver,
KtxSetupDatabasesArgs,
KtxSetupDatabasesDeps,
KtxSetupDatabasesResult,
} from './setup-databases.js';
export { runKloSetupDatabasesStep } from './setup-databases.js';
export { runKtxSetupDatabasesStep } from './setup-databases.js';
export type {
KloSetupEmbeddingBackend,
KloSetupEmbeddingsArgs,
KloSetupEmbeddingsDeps,
KloSetupEmbeddingsResult,
KtxSetupEmbeddingBackend,
KtxSetupEmbeddingsArgs,
KtxSetupEmbeddingsDeps,
KtxSetupEmbeddingsResult,
} from './setup-embeddings.js';
export { runKloSetupEmbeddingsStep } from './setup-embeddings.js';
export { runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
export type {
KloSetupSourcesArgs,
KloSetupSourcesDeps,
KloSetupSourcesPromptAdapter,
KloSetupSourcesResult,
KloSetupSourceType,
KtxSetupSourcesArgs,
KtxSetupSourcesDeps,
KtxSetupSourcesPromptAdapter,
KtxSetupSourcesResult,
KtxSetupSourceType,
} from './setup-sources.js';
export { runKloSetupSourcesStep } from './setup-sources.js';
export type { KloMemoryFlowTuiIo, MemoryFlowTuiLiveSession } from './memory-flow-tui.js';
export { runKtxSetupSourcesStep } from './setup-sources.js';
export type { KtxMemoryFlowTuiIo, MemoryFlowTuiLiveSession } from './memory-flow-tui.js';
export {
renderMemoryFlowTui,
sanitizeMemoryFlowTuiError,

View file

@ -36,7 +36,7 @@ describe('readIngestReportSnapshotFile', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-report-file-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-report-file-'));
});
afterEach(async () => {

View file

@ -1,5 +1,5 @@
import { readFile } from 'node:fs/promises';
import { parseIngestReportSnapshot, type IngestReportSnapshot } from '@klo/context/ingest';
import { parseIngestReportSnapshot, type IngestReportSnapshot } from '@ktx/context/ingest';
export async function readIngestReportSnapshotFile(reportFile: string): Promise<IngestReportSnapshot> {
const raw = await readFile(reportFile, 'utf-8');

View file

@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events';
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { AgentRunnerService, type RunLoopParams } from '@klo/context/agent';
import { AgentRunnerService, type RunLoopParams } from '@ktx/context/agent';
import {
LocalLookerRuntimeStore,
LocalMetabaseSourceStateReader,
@ -22,10 +22,10 @@ import {
type RunLocalIngestOptions,
type SourceAdapter,
type SqliteBundleIngestStore,
} from '@klo/context/ingest';
import { initKloProject, kloLocalStateDbPath, loadKloProject } from '@klo/context/project';
} from '@ktx/context/ingest';
import { initKtxProject, ktxLocalStateDbPath, loadKtxProject } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { type KloIngestArgs, runKloIngest } from './ingest.js';
import { type KtxIngestArgs, runKtxIngest } from './ingest.js';
import { resetVizFallbackWarningsForTest } from './viz-fallback.js';
function makeIo(
@ -104,7 +104,7 @@ function makeIo(
async function writeWarehouseConfig(projectDir: string): Promise<void> {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -123,7 +123,7 @@ async function writeWarehouseConfig(projectDir: string): Promise<void> {
async function writeMetabaseConfig(projectDir: string): Promise<void> {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -438,9 +438,9 @@ type SyncModeCase = {
async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCase): Promise<void> {
const projectDir = join(tempDir, `metabase-sync-mode-${input.name}`);
await initKloProject({ projectDir, projectName: `metabase-sync-mode-${input.name}` });
await initKtxProject({ projectDir, projectName: `metabase-sync-mode-${input.name}` });
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
`project: metabase-sync-mode-${input.name}`,
'connections:',
@ -461,8 +461,8 @@ async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCas
'utf-8',
);
const project = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
const project = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
await store.replaceSourceState({
connectionId: 'prod-metabase',
syncMode: input.syncMode,
@ -490,7 +490,7 @@ async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCas
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -640,10 +640,10 @@ function localFakeBundleReport(jobId: string, overrides: Partial<IngestReportSna
}
async function localBundleStore(projectDir: string, ids: [string, string]): Promise<SqliteBundleIngestStore> {
const { SqliteBundleIngestStore } = await import('@klo/context/ingest');
const project = await loadKloProject({ projectDir });
const { SqliteBundleIngestStore } = await import('@ktx/context/ingest');
const project = await loadKtxProject({ projectDir });
return new SqliteBundleIngestStore({
dbPath: kloLocalStateDbPath(project),
dbPath: ktxLocalStateDbPath(project),
idFactory: (() => {
let index = 0;
return () => ids[index++] ?? `generated-${index}`;
@ -696,7 +696,7 @@ function emitLiveLocalMemoryFlow(memoryFlow: MemoryFlowEventSink | undefined): v
memoryFlow?.finish('done');
}
describe('runKloIngest', () => {
describe('runKtxIngest', () => {
let tempDir: string;
let originalTerm: string | undefined;
@ -704,7 +704,7 @@ describe('runKloIngest', () => {
resetVizFallbackWarningsForTest();
originalTerm = process.env.TERM;
process.env.TERM = 'xterm-256color';
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-ingest-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-ingest-'));
});
afterEach(async () => {
@ -718,7 +718,7 @@ describe('runKloIngest', () => {
it('runs local ingest and reads status', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -731,7 +731,7 @@ describe('runKloIngest', () => {
const runIo = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -757,7 +757,7 @@ describe('runKloIngest', () => {
const statusIo = makeIo();
await expect(
runKloIngest({ command: 'status', projectDir, runId: 'cli-local-run-1', outputMode: 'plain' }, statusIo.io),
runKtxIngest({ command: 'status', projectDir, runId: 'cli-local-run-1', outputMode: 'plain' }, statusIo.io),
).resolves.toBe(0);
expect(statusIo.stdout()).toContain('Report: report-live-1');
@ -770,7 +770,7 @@ describe('runKloIngest', () => {
it('routes metabase scheduled pulls to the fan-out runner and prints child summaries', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeMetabaseConfig(projectDir);
const io = makeIo();
const report = localFakeBundleReport('metabase-child-1', {
@ -782,7 +782,7 @@ describe('runKloIngest', () => {
});
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -828,7 +828,7 @@ describe('runKloIngest', () => {
it('prints Metabase fan-out progress before the final summary', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeMetabaseConfig(projectDir);
const io = makeIo();
const report = localFakeBundleReport('metabase-child-1', {
@ -840,7 +840,7 @@ describe('runKloIngest', () => {
});
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -908,9 +908,9 @@ describe('runKloIngest', () => {
it('runs Metabase scheduled ingest through the public CLI command path with real fan-out', async () => {
const projectDir = join(tempDir, 'metabase-cli-project');
await initKloProject({ projectDir, projectName: 'metabase-cli' });
await initKtxProject({ projectDir, projectName: 'metabase-cli' });
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: metabase-cli',
'connections:',
@ -933,12 +933,12 @@ describe('runKloIngest', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
const project = await loadKtxProject({ projectDir });
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
await store.replaceSourceState({
connectionId: 'prod-metabase',
syncMode: 'ALL',
defaultTagNames: ['klo'],
defaultTagNames: ['ktx'],
selections: [],
mappings: [
{
@ -969,7 +969,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1001,7 +1001,7 @@ describe('runKloIngest', () => {
const statusIo = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{ command: 'status', projectDir, runId: 'metabase-child-1', outputMode: 'plain' },
statusIo.io,
),
@ -1046,12 +1046,12 @@ describe('runKloIngest', () => {
it('prints metabase fan-out JSON results', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeMetabaseConfig(projectDir);
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1081,12 +1081,12 @@ describe('runKloIngest', () => {
it('rejects source-dir uploads through the metabase fan-out route', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeMetabaseConfig(projectDir);
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1105,13 +1105,13 @@ describe('runKloIngest', () => {
).resolves.toBe(1);
expect(io.stderr()).toContain('source-dir uploads are not supported for the Metabase fan-out adapter');
expect(io.stderr()).not.toContain('klo dev ingest run requires llm.provider.backend');
expect(io.stderr()).not.toContain('ktx dev ingest run requires llm.provider.backend');
expect(io.stdout()).toBe('');
});
it('prints previous run and diff summary for local ingest results', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1120,7 +1120,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1145,15 +1145,15 @@ describe('runKloIngest', () => {
it('passes the debug LLM request file to local ingest runs', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const runLocalIngest = vi.fn(async (input: RunLocalIngestOptions) =>
completedLocalBundleRun(input, 'job-debug'),
);
const io = makeIo();
const debugFile = join(projectDir, '.klo', 'llm-debug.jsonl');
const debugFile = join(projectDir, '.ktx', 'llm-debug.jsonl');
const exitCode = await runKloIngest(
const exitCode = await runKtxIngest(
{
command: 'run',
projectDir,
@ -1172,7 +1172,7 @@ describe('runKloIngest', () => {
it('passes daemon database introspection URL to default local ingest adapters', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1187,7 +1187,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1196,7 +1196,7 @@ describe('runKloIngest', () => {
sourceDir,
databaseIntrospectionUrl: 'http://127.0.0.1:8765',
outputMode: 'plain',
} satisfies KloIngestArgs,
} satisfies KtxIngestArgs,
io.io,
{
createAdapters,
@ -1220,9 +1220,9 @@ describe('runKloIngest', () => {
it('passes the target connection id when constructing local historic-sql adapters', async () => {
const projectDir = join(tempDir, 'historic-sql-project');
await initKloProject({ projectDir, projectName: 'historic-sql-project' });
await initKtxProject({ projectDir, projectName: 'historic-sql-project' });
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: historic-sql-project',
'connections:',
@ -1250,7 +1250,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1281,7 +1281,7 @@ describe('runKloIngest', () => {
it('passes local Looker pull-config options and agent runner into scheduled ingest for Looker scheduled ingest', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const pullConfigOptions = {
looker: {
@ -1299,14 +1299,14 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
connectionId: 'warehouse',
adapter: 'fake',
outputMode: 'plain',
} satisfies KloIngestArgs,
} satisfies KtxIngestArgs,
io.io,
{
createAdapters,
@ -1335,9 +1335,9 @@ describe('runKloIngest', () => {
it('runs Looker scheduled ingest through the public CLI command path', async () => {
const projectDir = join(tempDir, 'looker-project');
await initKloProject({ projectDir, projectName: 'looker-cli' });
await initKtxProject({ projectDir, projectName: 'looker-cli' });
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: looker-cli',
'connections:',
@ -1357,8 +1357,8 @@ describe('runKloIngest', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir });
const store = new LocalLookerRuntimeStore({ dbPath: kloLocalStateDbPath(project) });
const project = await loadKtxProject({ projectDir });
const store = new LocalLookerRuntimeStore({ dbPath: ktxLocalStateDbPath(project) });
await store.setCursors('prod-looker', {
dashboardsLastSyncedAt: null,
looksLastSyncedAt: null,
@ -1366,7 +1366,7 @@ describe('runKloIngest', () => {
await store.upsertConnectionMapping({
lookerConnectionId: 'prod-looker',
lookerConnectionName: 'analytics',
kloConnectionId: 'prod-warehouse',
ktxConnectionId: 'prod-warehouse',
source: 'cli',
});
const runtimeClient = makeCliLookerRuntimeClient();
@ -1375,7 +1375,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1419,7 +1419,7 @@ describe('runKloIngest', () => {
const statusIo = makeIo();
await expect(
runKloIngest(
runKtxIngest(
{ command: 'status', projectDir, runId: 'cli-looker-job', outputMode: 'plain' },
statusIo.io,
),
@ -1431,7 +1431,7 @@ describe('runKloIngest', () => {
it('renders live memory-flow frames for run --viz when stdout is interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1462,7 +1462,7 @@ describe('runKloIngest', () => {
const startLiveMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => null);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1483,15 +1483,15 @@ describe('runKloIngest', () => {
expect(runLocal).toHaveBeenCalledWith(expect.objectContaining({ memoryFlow: expect.any(Object) }));
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
expect((io.stdout().match(/KLO memory flow/g) ?? []).length).toBeGreaterThan(1);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect((io.stdout().match(/KTX memory flow/g) ?? []).length).toBeGreaterThan(1);
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).toContain('fake-orders');
expect(io.stderr()).toBe('');
});
it('uses the TUI live session for run --viz when stdin and stdout are interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1510,7 +1510,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1539,13 +1539,13 @@ describe('runKloIngest', () => {
expect(liveSession.update).toHaveBeenCalled();
expect(liveSession.close).toHaveBeenCalledTimes(1);
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toBe('');
});
it('prints a final plain summary after live viz completes', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
const liveSession = {
@ -1560,7 +1560,7 @@ describe('runKloIngest', () => {
});
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1580,7 +1580,7 @@ describe('runKloIngest', () => {
it('falls back to text live rendering when the TUI live session is unavailable', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1594,7 +1594,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1614,12 +1614,12 @@ describe('runKloIngest', () => {
expect(startLiveMemoryFlow).toHaveBeenCalledTimes(1);
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
});
it('falls back to text live rendering when TUI startup fails with a redacted warning', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1638,7 +1638,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1657,13 +1657,13 @@ describe('runKloIngest', () => {
).resolves.toBe(0);
expect(io.stderr()).toContain('TUI visualization unavailable: Failed [redacted-url] [redacted]');
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
});
it('does not start live TUI when run --viz disables input', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1680,7 +1680,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1697,12 +1697,12 @@ describe('runKloIngest', () => {
expect(startLiveMemoryFlow).not.toHaveBeenCalled();
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
});
it('does not attach a live memory-flow sink for plain run output', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1712,7 +1712,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1728,12 +1728,12 @@ describe('runKloIngest', () => {
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
expect(io.stdout()).toContain('Job: plain-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
});
it('falls back to plain run output for run --viz when stdout is not interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1742,7 +1742,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: false });
const runLocal = vi.fn(async (input: RunLocalIngestOptions) => completedLocalBundleRun(input, 'non-tty-viz-run'));
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1760,7 +1760,7 @@ describe('runKloIngest', () => {
).resolves.toBe(0);
expect(io.stdout()).toContain('Job: non-tty-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
);
@ -1768,7 +1768,7 @@ describe('runKloIngest', () => {
it('falls back to plain run output for run --viz when stdin raw mode is unavailable', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1783,7 +1783,7 @@ describe('runKloIngest', () => {
}));
await expect(
runKloIngest(
runKtxIngest(
{
command: 'run',
projectDir,
@ -1804,7 +1804,7 @@ describe('runKloIngest', () => {
expect(startLiveMemoryFlow).not.toHaveBeenCalled();
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
expect(io.stdout()).toContain('Job: raw-missing-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
);
@ -1812,11 +1812,11 @@ describe('runKloIngest', () => {
it('returns an error code for missing status', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const io = makeIo();
await expect(
runKloIngest({ command: 'status', projectDir, runId: 'missing-run', outputMode: 'plain' }, io.io),
runKtxIngest({ command: 'status', projectDir, runId: 'missing-run', outputMode: 'plain' }, io.io),
).resolves.toBe(1);
expect(io.stderr()).toContain('Local ingest run or report "missing-run" was not found');
@ -1824,13 +1824,13 @@ describe('runKloIngest', () => {
it('uses the latest local ingest report when status has no run id', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
await persistLocalBundleReport(projectDir, localFakeBundleReport('older-run'));
await persistLocalBundleReport(projectDir, localFakeBundleReport('newer-run'));
const io = makeIo();
await expect(runKloIngest({ command: 'status', projectDir, outputMode: 'plain' }, io.io)).resolves.toBe(0);
await expect(runKtxIngest({ command: 'status', projectDir, outputMode: 'plain' }, io.io)).resolves.toBe(0);
expect(io.stdout()).toContain('Run: run-newer-run');
expect(io.stdout()).toContain('Job: newer-run');
@ -1839,28 +1839,28 @@ describe('runKloIngest', () => {
it('renders the latest local ingest report through watch when run id is omitted', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
await persistLocalBundleReport(projectDir, localFakeBundleReport('watch-latest'));
const io = makeIo({ isTTY: true });
await expect(
runKloIngest({ command: 'watch', projectDir, outputMode: 'viz', inputMode: 'disabled' }, io.io),
runKtxIngest({ command: 'watch', projectDir, outputMode: 'viz', inputMode: 'disabled' }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).toContain('Run: run-watch-latest');
expect(io.stderr()).toBe('');
});
it('renders report-file replay through the memory-flow TUI', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const reportFile = await writeBundleReportFile(tempDir);
const io = makeIo({ isTTY: true });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -1873,7 +1873,7 @@ describe('runKloIngest', () => {
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/metabase done');
expect(io.stdout()).toContain('KTX memory flow warehouse/metabase done');
expect(io.stdout()).toContain('Saved 2 memories from 2 raw files');
expect(io.stdout()).toContain('Commit: abc12345 Run: run-1 Report: report-1');
expect(io.stdout()).toContain('SOURCE');
@ -1884,12 +1884,12 @@ describe('runKloIngest', () => {
it('prints report-file JSON without looking up local ingest status', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const reportFile = await writeBundleReportFile(tempDir);
const io = makeIo();
await expect(
runKloIngest({ command: 'status', projectDir, runId: 'report-1', reportFile, outputMode: 'json' }, io.io),
runKtxIngest({ command: 'status', projectDir, runId: 'report-1', reportFile, outputMode: 'json' }, io.io),
).resolves.toBe(0);
const parsed = JSON.parse(io.stdout());
@ -1905,13 +1905,13 @@ describe('runKloIngest', () => {
it('routes interactive report-file replay through the stored TUI renderer', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const reportFile = await writeBundleReportFile(tempDir);
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -1937,12 +1937,12 @@ describe('runKloIngest', () => {
it('rejects report-file replay when the requested id does not match the report', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const reportFile = await writeBundleReportFile(tempDir);
const io = makeIo();
await expect(
runKloIngest({ command: 'replay', projectDir, runId: 'unrelated-id', reportFile, outputMode: 'plain' }, io.io),
runKtxIngest({ command: 'replay', projectDir, runId: 'unrelated-id', reportFile, outputMode: 'plain' }, io.io),
).resolves.toBe(1);
expect(io.stderr()).toContain(
@ -1953,7 +1953,7 @@ describe('runKloIngest', () => {
it('renders memory-flow snapshot for status --viz when stdout is interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1963,13 +1963,13 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true });
await expect(
runKloIngest(
runKtxIngest(
{ command: 'status', projectDir, runId: 'viz-run-1', outputMode: 'viz', inputMode: 'disabled' },
io.io,
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).toContain('SOURCE');
expect(io.stdout()).toContain('CHUNKS');
expect(io.stdout()).toContain('WORKUNITS');
@ -1979,7 +1979,7 @@ describe('runKloIngest', () => {
it('uses the TUI renderer for stored status --viz when stdin and stdout are interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -1991,7 +1991,7 @@ describe('runKloIngest', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'status',
projectDir,
@ -2015,7 +2015,7 @@ describe('runKloIngest', () => {
it('falls back to the text renderer when TUI declines stored status --viz', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2027,7 +2027,7 @@ describe('runKloIngest', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => false);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'status',
projectDir,
@ -2040,12 +2040,12 @@ describe('runKloIngest', () => {
).resolves.toBe(0);
expect(renderStoredMemoryFlow).toHaveBeenCalledTimes(1);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
});
it('does not use TUI for stored --viz when input is disabled', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2057,7 +2057,7 @@ describe('runKloIngest', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2071,12 +2071,12 @@ describe('runKloIngest', () => {
).resolves.toBe(0);
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
});
it('falls back to plain status for stored --viz when stdin raw mode is unavailable', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2088,7 +2088,7 @@ describe('runKloIngest', () => {
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2103,7 +2103,7 @@ describe('runKloIngest', () => {
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
expect(io.stdout()).toContain('Run: run-raw-missing-stored-viz-run');
expect(io.stdout()).toContain('Job: raw-missing-stored-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
);
@ -2111,7 +2111,7 @@ describe('runKloIngest', () => {
it('keeps stored --viz snapshot-only when input is disabled', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2121,7 +2121,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2133,14 +2133,14 @@ describe('runKloIngest', () => {
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
expect(io.stderr()).toBe('');
});
it('keeps disabled-input stored --viz snapshot output even when stdin raw mode is unavailable', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2150,7 +2150,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true, stdinIsTTY: true, rawMode: false, columns: 120 });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2162,14 +2162,14 @@ describe('runKloIngest', () => {
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
expect(io.stderr()).toBe('');
});
it('degrades stored --viz snapshots to plain status when stdout is redirected even when input is disabled', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2179,7 +2179,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: false });
await expect(
runKloIngest(
runKtxIngest(
{
command: 'replay',
projectDir,
@ -2193,7 +2193,7 @@ describe('runKloIngest', () => {
expect(io.stdout()).toContain('Run: run-redirected-no-input-viz-run');
expect(io.stdout()).toContain('Job: redirected-no-input-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
);
@ -2201,7 +2201,7 @@ describe('runKloIngest', () => {
it('degrades ingest replay --viz to plain status when TERM is dumb', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2211,7 +2211,7 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: true });
await expect(
runKloIngest(
runKtxIngest(
{ command: 'replay', projectDir, runId: 'dumb-terminal-viz-run', outputMode: 'viz' },
io.io,
{ env: { ...process.env, TERM: 'dumb' } },
@ -2220,7 +2220,7 @@ describe('runKloIngest', () => {
expect(io.stdout()).toContain('Run: run-dumb-terminal-viz-run');
expect(io.stdout()).toContain('Job: dumb-terminal-viz-run');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but TERM=dumb does not support the visual renderer; printing plain output.',
);
@ -2228,7 +2228,7 @@ describe('runKloIngest', () => {
it('falls back to plain status for --viz when stdout is not interactive', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2238,12 +2238,12 @@ describe('runKloIngest', () => {
const io = makeIo({ isTTY: false });
await expect(
runKloIngest({ command: 'replay', projectDir, runId: 'viz-run-2', outputMode: 'viz' }, io.io),
runKtxIngest({ command: 'replay', projectDir, runId: 'viz-run-2', outputMode: 'viz' }, io.io),
).resolves.toBe(0);
expect(io.stdout()).toContain('Run: run-viz-run-2');
expect(io.stdout()).toContain('Job: viz-run-2');
expect(io.stdout()).not.toContain('KLO memory flow');
expect(io.stdout()).not.toContain('KTX memory flow');
expect(io.stderr()).toContain(
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
);
@ -2251,7 +2251,7 @@ describe('runKloIngest', () => {
it('prints JSON for status --json', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeWarehouseConfig(projectDir);
const sourceDir = join(tempDir, 'source');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
@ -2261,7 +2261,7 @@ describe('runKloIngest', () => {
const io = makeIo();
await expect(
runKloIngest({ command: 'status', projectDir, runId: 'json-run-1', outputMode: 'json' }, io.io),
runKtxIngest({ command: 'status', projectDir, runId: 'json-run-1', outputMode: 'json' }, io.io),
).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toMatchObject({

View file

@ -13,13 +13,13 @@ import {
renderMemoryFlowReplay,
runLocalIngest,
runLocalMetabaseIngest,
} from '@klo/context/ingest';
import { loadKloProject } from '@klo/context/project';
} from '@ktx/context/ingest';
import { loadKtxProject } from '@ktx/context/project';
import { readIngestReportSnapshotFile } from './ingest-report-file.js';
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
import { type KloMemoryFlowStdin, renderMemoryFlowInteractively } from './memory-flow-interactive.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
import { type KtxMemoryFlowStdin, renderMemoryFlowInteractively } from './memory-flow-interactive.js';
import {
type KloMemoryFlowTuiIo,
type KtxMemoryFlowTuiIo,
type MemoryFlowTuiLiveSession,
renderMemoryFlowTui,
startLiveMemoryFlowTui,
@ -29,10 +29,10 @@ import { profileMark } from './startup-profile.js';
profileMark('module:ingest');
export type KloIngestOutputMode = 'plain' | 'json' | 'viz';
type KloIngestInputMode = 'auto' | 'disabled';
export type KtxIngestOutputMode = 'plain' | 'json' | 'viz';
type KtxIngestInputMode = 'auto' | 'disabled';
export type KloIngestArgs =
export type KtxIngestArgs =
| {
command: 'run';
projectDir: string;
@ -41,28 +41,28 @@ export type KloIngestArgs =
sourceDir?: string;
databaseIntrospectionUrl?: string;
debugLlmRequestFile?: string;
outputMode: KloIngestOutputMode;
inputMode?: KloIngestInputMode;
outputMode: KtxIngestOutputMode;
inputMode?: KtxIngestInputMode;
}
| {
command: 'status' | 'replay' | 'watch';
projectDir: string;
runId?: string;
reportFile?: string;
outputMode: KloIngestOutputMode;
inputMode?: KloIngestInputMode;
outputMode: KtxIngestOutputMode;
inputMode?: KtxIngestInputMode;
};
interface KloIngestIo {
stdin?: KloMemoryFlowStdin;
interface KtxIngestIo {
stdin?: KtxMemoryFlowStdin;
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
stderr: { write(chunk: string): void };
}
interface KloIngestDeps {
interface KtxIngestDeps {
jobIdFactory?: () => string;
now?: () => Date;
createAdapters?: typeof createKloCliLocalIngestAdapters;
createAdapters?: typeof createKtxCliLocalIngestAdapters;
runLocalIngest?: typeof runLocalIngest;
runLocalMetabaseIngest?: typeof runLocalMetabaseIngest;
readReportFile?: typeof readIngestReportSnapshotFile;
@ -93,7 +93,7 @@ function reportActionCounts(report: IngestReportSnapshot): { wikiCount: number;
};
}
function writeReportStatus(report: IngestReportSnapshot, io: KloIngestIo): void {
function writeReportStatus(report: IngestReportSnapshot, io: KtxIngestIo): void {
const counts = reportActionCounts(report);
io.stdout.write(`Report: ${report.id}\n`);
io.stdout.write(`Run: ${report.runId}\n`);
@ -110,7 +110,7 @@ function writeReportStatus(report: IngestReportSnapshot, io: KloIngestIo): void
io.stdout.write(`Provenance rows: ${report.body.provenanceRows.length}\n`);
}
function writeMetabaseFanoutStatus(result: LocalMetabaseFanoutResult, io: KloIngestIo): void {
function writeMetabaseFanoutStatus(result: LocalMetabaseFanoutResult, io: KtxIngestIo): void {
io.stdout.write(`Metabase fan-out: ${result.status}\n`);
io.stdout.write(`Source: ${result.metabaseConnectionId}\n`);
io.stdout.write(`Children: ${result.children.length}\n`);
@ -132,7 +132,7 @@ function pluralize(count: number, singular: string, plural = `${singular}s`): st
function createMetabaseFanoutProgress(
connectionId: string,
io: KloIngestIo,
io: KtxIngestIo,
): LocalMetabaseFanoutProgress {
io.stdout.write(`Metabase ingest: ${connectionId}\n`);
io.stdout.write('Checking mappings and scheduled-pull targets...\n');
@ -156,7 +156,7 @@ function createMetabaseFanoutProgress(
};
}
function writeReportJson(report: IngestReportSnapshot, io: KloIngestIo): void {
function writeReportJson(report: IngestReportSnapshot, io: KtxIngestIo): void {
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
}
@ -172,21 +172,21 @@ function assertReportMatchesReplayId(report: IngestReportSnapshot, requestedId:
}
async function readStoredIngestReport(
project: Awaited<ReturnType<typeof loadKloProject>>,
project: Awaited<ReturnType<typeof loadKtxProject>>,
runId: string | undefined,
): Promise<IngestReportSnapshot | null> {
return runId ? await getLocalIngestStatus(project, runId) : await getLatestLocalIngestStatus(project);
}
function isInteractiveTerminal(io: KloIngestIo): boolean {
function isInteractiveTerminal(io: KtxIngestIo): boolean {
return io.stdout.isTTY === true;
}
function terminalWidth(io: KloIngestIo): number | undefined {
function terminalWidth(io: KtxIngestIo): number | undefined {
return io.stdout.columns ?? process.stdout.columns;
}
function isTuiCapableIo(io: KloIngestIo): io is KloIngestIo & KloMemoryFlowTuiIo {
function isTuiCapableIo(io: KtxIngestIo): io is KtxIngestIo & KtxMemoryFlowTuiIo {
return (
io.stdin?.isTTY === true &&
io.stdout.isTTY === true &&
@ -201,11 +201,11 @@ interface EffectiveIngestOutputModeOptions {
}
function effectiveIngestOutputMode(
outputMode: KloIngestOutputMode,
io: KloIngestIo,
outputMode: KtxIngestOutputMode,
io: KtxIngestIo,
env: NodeJS.ProcessEnv,
options: EffectiveIngestOutputModeOptions = {},
): KloIngestOutputMode {
): KtxIngestOutputMode {
if (outputMode !== 'viz') {
return outputMode;
}
@ -219,7 +219,7 @@ function effectiveIngestOutputMode(
return 'plain';
}
function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KloIngestIo, options: { clear?: boolean } = {}): void {
function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KtxIngestIo, options: { clear?: boolean } = {}): void {
if (options.clear) {
io.stdout.write('\u001b[2J\u001b[H');
}
@ -228,7 +228,7 @@ function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KloIngestIo, opt
}
function initialRunMemoryFlowInput(
args: Extract<KloIngestArgs, { command: 'run' }>,
args: Extract<KtxIngestArgs, { command: 'run' }>,
runId: string,
): MemoryFlowReplayInput {
return {
@ -247,8 +247,8 @@ function initialRunMemoryFlowInput(
async function writeReportRecord(
report: IngestReportSnapshot,
outputMode: KloIngestOutputMode,
io: KloIngestIo,
outputMode: KtxIngestOutputMode,
io: KtxIngestIo,
options: {
interactive?: boolean;
renderStoredMemoryFlow?: typeof renderMemoryFlowTui;
@ -288,16 +288,16 @@ async function writeReportRecord(
writeReportStatus(report, io);
}
export async function runKloIngest(
args: KloIngestArgs,
io: KloIngestIo = process,
deps: KloIngestDeps = {},
export async function runKtxIngest(
args: KtxIngestArgs,
io: KtxIngestIo = process,
deps: KtxIngestDeps = {},
): Promise<number> {
try {
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
const env = deps.env ?? process.env;
if (args.command === 'run') {
const createAdapters = deps.createAdapters ?? createKloCliLocalIngestAdapters;
const createAdapters = deps.createAdapters ?? createKtxCliLocalIngestAdapters;
const executeLocalIngest = deps.runLocalIngest ?? runLocalIngest;
const localIngestOptions = deps.localIngestOptions ?? {};
const adapterOptions = {
@ -409,7 +409,7 @@ export async function runKloIngest(
throw new Error(
args.runId
? `Local ingest run or report "${args.runId}" was not found`
: 'No local ingest reports were found. Run `klo ingest --all` first.',
: 'No local ingest reports were found. Run `ktx ingest --all` first.',
);
}
await writeReportRecord(report, args.outputMode, io, {

View file

@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import type { KloCliIo } from '../cli-runtime.js';
import type { KtxCliIo } from '../cli-runtime.js';
import { resolveOutputMode } from './mode.js';
function ioWith(isTTY: boolean | undefined): KloCliIo {
function ioWith(isTTY: boolean | undefined): KtxCliIo {
return {
stdout: { isTTY, write: () => {} },
stderr: { write: () => {} },
@ -24,14 +24,14 @@ describe('resolveOutputMode', () => {
expect(() => resolveOutputMode({ explicit: 'fancy', io: ioWith(true), env: {} })).toThrow(/Invalid --output/);
});
it('honors KLO_OUTPUT env var when no explicit value', () => {
expect(resolveOutputMode({ io: ioWith(true), env: { KLO_OUTPUT: 'plain' } })).toBe('plain');
expect(resolveOutputMode({ io: ioWith(false), env: { KLO_OUTPUT: 'pretty' } })).toBe('pretty');
expect(resolveOutputMode({ io: ioWith(false), env: { KLO_OUTPUT: 'json' } })).toBe('json');
it('honors KTX_OUTPUT env var when no explicit value', () => {
expect(resolveOutputMode({ io: ioWith(true), env: { KTX_OUTPUT: 'plain' } })).toBe('plain');
expect(resolveOutputMode({ io: ioWith(false), env: { KTX_OUTPUT: 'pretty' } })).toBe('pretty');
expect(resolveOutputMode({ io: ioWith(false), env: { KTX_OUTPUT: 'json' } })).toBe('json');
});
it('throws on unknown KLO_OUTPUT', () => {
expect(() => resolveOutputMode({ io: ioWith(true), env: { KLO_OUTPUT: 'fancy' } })).toThrow(/Invalid KLO_OUTPUT/);
it('throws on unknown KTX_OUTPUT', () => {
expect(() => resolveOutputMode({ io: ioWith(true), env: { KTX_OUTPUT: 'fancy' } })).toThrow(/Invalid KTX_OUTPUT/);
});
it('returns plain when CI is set to a truthy value', () => {
@ -54,7 +54,7 @@ describe('resolveOutputMode', () => {
expect(resolveOutputMode({ io: ioWith(undefined), env: {} })).toBe('plain');
});
it('explicit value beats KLO_OUTPUT env var', () => {
expect(resolveOutputMode({ explicit: 'json', io: ioWith(true), env: { KLO_OUTPUT: 'plain' } })).toBe('json');
it('explicit value beats KTX_OUTPUT env var', () => {
expect(resolveOutputMode({ explicit: 'json', io: ioWith(true), env: { KTX_OUTPUT: 'plain' } })).toBe('json');
});
});

View file

@ -1,17 +1,17 @@
import type { KloCliIo } from '../cli-runtime.js';
import type { KtxCliIo } from '../cli-runtime.js';
export type KloOutputMode = 'pretty' | 'plain' | 'json';
export type KtxOutputMode = 'pretty' | 'plain' | 'json';
const MODES: ReadonlySet<string> = new Set(['pretty', 'plain', 'json']);
export interface ResolveOutputModeArgs {
explicit?: string;
json?: boolean;
io: KloCliIo;
io: KtxCliIo;
env?: NodeJS.ProcessEnv;
}
export function resolveOutputMode(args: ResolveOutputModeArgs): KloOutputMode {
export function resolveOutputMode(args: ResolveOutputModeArgs): KtxOutputMode {
if (args.json === true) {
return 'json';
}
@ -19,15 +19,15 @@ export function resolveOutputMode(args: ResolveOutputModeArgs): KloOutputMode {
if (!MODES.has(args.explicit)) {
throw new Error(`Invalid --output value: ${args.explicit}. Expected one of pretty, plain, json.`);
}
return args.explicit as KloOutputMode;
return args.explicit as KtxOutputMode;
}
const env = args.env ?? process.env;
const envMode = env.KLO_OUTPUT;
const envMode = env.KTX_OUTPUT;
if (envMode !== undefined && envMode !== '') {
if (!MODES.has(envMode)) {
throw new Error(`Invalid KLO_OUTPUT value: ${envMode}. Expected one of pretty, plain, json.`);
throw new Error(`Invalid KTX_OUTPUT value: ${envMode}. Expected one of pretty, plain, json.`);
}
return envMode as KloOutputMode;
return envMode as KtxOutputMode;
}
const ci = env.CI;
if (ci !== undefined && ci !== '' && ci !== '0' && ci !== 'false') {

View file

@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest';
import type { KloCliIo } from '../cli-runtime.js';
import type { KtxCliIo } from '../cli-runtime.js';
import { printList, type PrintListColumn } from './print-list.js';
import { SYMBOLS } from './symbols.js';
function recorder(): { io: KloCliIo; out: () => string; err: () => string } {
function recorder(): { io: KtxCliIo; out: () => string; err: () => string } {
let stdout = '';
let stderr = '';
return {

View file

@ -1,5 +1,5 @@
import type { KloCliIo } from '../cli-runtime.js';
import type { KloOutputMode } from './mode.js';
import type { KtxCliIo } from '../cli-runtime.js';
import type { KtxOutputMode } from './mode.js';
import { bold, dim, SYMBOLS } from './symbols.js';
export interface PrintListColumn<Row> {
@ -24,8 +24,8 @@ export interface PrintListArgs<Row> {
groupBy?: keyof Row & string;
emptyMessage: string;
command: string;
mode: KloOutputMode;
io: KloCliIo;
mode: KtxOutputMode;
io: KtxCliIo;
}
export function printList<Row extends object>(args: PrintListArgs<Row>): void {

View file

@ -1,9 +1,9 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initKloProject } from '@klo/context/project';
import { initKtxProject } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { runKloKnowledge } from './knowledge.js';
import { runKtxKnowledge } from './knowledge.js';
function makeIo() {
let stdout = '';
@ -26,11 +26,11 @@ function makeIo() {
};
}
describe('runKloKnowledge', () => {
describe('runKtxKnowledge', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-knowledge-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-knowledge-'));
});
afterEach(async () => {
@ -39,11 +39,11 @@ describe('runKloKnowledge', () => {
it('writes, reads, lists, and searches knowledge pages', async () => {
const projectDir = join(tempDir, 'project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const writeIo = makeIo();
await expect(
runKloKnowledge(
runKtxKnowledge(
{
command: 'write',
projectDir,
@ -63,33 +63,33 @@ describe('runKloKnowledge', () => {
const readIo = makeIo();
await expect(
runKloKnowledge({ command: 'read', projectDir, key: 'metrics/revenue', userId: 'local' }, readIo.io),
runKtxKnowledge({ command: 'read', projectDir, key: 'metrics/revenue', userId: 'local' }, readIo.io),
).resolves.toBe(0);
expect(readIo.stdout()).toContain('# metrics/revenue');
expect(readIo.stdout()).toContain('Revenue is paid order value.');
const listIo = makeIo();
await expect(runKloKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
expect(listIo.stdout()).toContain('GLOBAL\tmetrics/revenue\tRevenue');
const searchIo = makeIo();
await expect(
runKloKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io),
runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io),
).resolves.toBe(0);
expect(searchIo.stdout()).toContain('metrics/revenue');
});
it('explains empty search results for a project without wiki pages', async () => {
const projectDir = join(tempDir, 'empty-project');
await initKloProject({ projectDir, projectName: 'warehouse' });
await initKtxProject({ projectDir, projectName: 'warehouse' });
const searchIo = makeIo();
await expect(
runKloKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io),
runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io),
).resolves.toBe(0);
expect(searchIo.stdout()).toBe('');
expect(searchIo.stderr()).toContain('No local wiki pages found');
expect(searchIo.stderr()).toContain('klo wiki write');
expect(searchIo.stderr()).toContain('ktx wiki write');
});
});

View file

@ -1,13 +1,13 @@
import { loadKloProject } from '@klo/context/project';
import { loadKtxProject } from '@ktx/context/project';
import {
type LocalKnowledgeScope,
listLocalKnowledgePages,
readLocalKnowledgePage,
searchLocalKnowledgePages,
writeLocalKnowledgePage,
} from '@klo/context/wiki';
} from '@ktx/context/wiki';
export type KloKnowledgeArgs =
export type KtxKnowledgeArgs =
| { command: 'list'; projectDir: string; userId: string }
| { command: 'read'; projectDir: string; key: string; userId: string }
| { command: 'search'; projectDir: string; query: string; userId: string }
@ -24,14 +24,14 @@ export type KloKnowledgeArgs =
slRefs: string[];
};
interface KloKnowledgeIo {
interface KtxKnowledgeIo {
stdout: { write(chunk: string): void };
stderr: { write(chunk: string): void };
}
export async function runKloKnowledge(args: KloKnowledgeArgs, io: KloKnowledgeIo = process): Promise<number> {
export async function runKtxKnowledge(args: KtxKnowledgeArgs, io: KtxKnowledgeIo = process): Promise<number> {
try {
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
if (args.command === 'list') {
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
for (const page of pages) {
@ -56,11 +56,11 @@ export async function runKloKnowledge(args: KloKnowledgeArgs, io: KloKnowledgeIo
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
if (pages.length === 0) {
io.stderr.write(
`No local wiki pages found in ${project.projectDir}. Create one with \`klo wiki write <key> --summary <summary> --content <content>\` or run ingest.\n`,
`No local wiki pages found in ${project.projectDir}. Create one with \`ktx wiki write <key> --summary <summary> --content <content>\` or run ingest.\n`,
);
} else {
io.stderr.write(
`No local wiki pages matched "${args.query}". Run \`klo wiki list\` to inspect available pages.\n`,
`No local wiki pages matched "${args.query}". Run \`ktx wiki list\` to inspect available pages.\n`,
);
}
return 0;

View file

@ -1,15 +1,15 @@
import { join } from 'node:path';
import { createBigQueryLiveDatabaseIntrospection, isKloBigQueryConnectionConfig } from '@klo/connector-bigquery';
import { createClickHouseLiveDatabaseIntrospection, isKloClickHouseConnectionConfig } from '@klo/connector-clickhouse';
import { createMysqlLiveDatabaseIntrospection, isKloMysqlConnectionConfig } from '@klo/connector-mysql';
import { createBigQueryLiveDatabaseIntrospection, isKtxBigQueryConnectionConfig } from '@ktx/connector-bigquery';
import { createClickHouseLiveDatabaseIntrospection, isKtxClickHouseConnectionConfig } from '@ktx/connector-clickhouse';
import { createMysqlLiveDatabaseIntrospection, isKtxMysqlConnectionConfig } from '@ktx/connector-mysql';
import {
createPostgresLiveDatabaseIntrospection,
isKloPostgresConnectionConfig,
type KloPostgresConnectionConfig,
KloPostgresHistoricSqlQueryClient,
} from '@klo/connector-postgres';
import { createSqliteLiveDatabaseIntrospection, isKloSqliteConnectionConfig } from '@klo/connector-sqlite';
import { createSqlServerLiveDatabaseIntrospection, isKloSqlServerConnectionConfig } from '@klo/connector-sqlserver';
isKtxPostgresConnectionConfig,
type KtxPostgresConnectionConfig,
KtxPostgresHistoricSqlQueryClient,
} from '@ktx/connector-postgres';
import { createSqliteLiveDatabaseIntrospection, isKtxSqliteConnectionConfig } from '@ktx/connector-sqlite';
import { createSqlServerLiveDatabaseIntrospection, isKtxSqlServerConnectionConfig } from '@ktx/connector-sqlserver';
import {
createDaemonLiveDatabaseIntrospection,
createDefaultLocalIngestAdapters,
@ -17,9 +17,9 @@ import {
type LiveDatabaseIntrospectionPort,
LiveDatabaseSourceAdapter,
type SourceAdapter,
} from '@klo/context/ingest';
import type { KloLocalProject } from '@klo/context/project';
import { createHttpSqlAnalysisPort } from '@klo/context/sql-analysis';
} from '@ktx/context/ingest';
import type { KtxLocalProject } from '@ktx/context/project';
import { createHttpSqlAnalysisPort } from '@ktx/context/sql-analysis';
function hasSnowflakeDriver(connection: unknown): boolean {
return (
@ -29,8 +29,8 @@ function hasSnowflakeDriver(connection: unknown): boolean {
);
}
function createKloCliLiveDatabaseIntrospection(
project: KloLocalProject,
function createKtxCliLiveDatabaseIntrospection(
project: KtxLocalProject,
options: DefaultLocalIngestAdaptersOptions = {},
): LiveDatabaseIntrospectionPort {
const daemon = createDaemonLiveDatabaseIntrospection({
@ -60,29 +60,29 @@ function createKloCliLiveDatabaseIntrospection(
return {
async extractSchema(connectionId: string) {
const connection = project.config.connections[connectionId];
if (isKloPostgresConnectionConfig(connection)) {
if (isKtxPostgresConnectionConfig(connection)) {
return postgres.extractSchema(connectionId);
}
if (isKloSqliteConnectionConfig(connection)) {
if (isKtxSqliteConnectionConfig(connection)) {
return sqlite.extractSchema(connectionId);
}
if (isKloMysqlConnectionConfig(connection)) {
if (isKtxMysqlConnectionConfig(connection)) {
return mysql.extractSchema(connectionId);
}
if (isKloClickHouseConnectionConfig(connection)) {
if (isKtxClickHouseConnectionConfig(connection)) {
return clickhouse.extractSchema(connectionId);
}
if (isKloSqlServerConnectionConfig(connection)) {
if (isKtxSqlServerConnectionConfig(connection)) {
return sqlserver.extractSchema(connectionId);
}
if (isKloBigQueryConnectionConfig(connection)) {
if (isKtxBigQueryConnectionConfig(connection)) {
return bigquery.extractSchema(connectionId);
}
if (hasSnowflakeDriver(connection)) {
const { createSnowflakeLiveDatabaseIntrospection, isKloSnowflakeConnectionConfig } = await import(
'@klo/connector-snowflake'
const { createSnowflakeLiveDatabaseIntrospection, isKtxSnowflakeConnectionConfig } = await import(
'@ktx/connector-snowflake'
);
if (!isKloSnowflakeConnectionConfig(connection)) {
if (!isKtxSnowflakeConnectionConfig(connection)) {
return daemon.extractSchema(connectionId);
}
const snowflake = createSnowflakeLiveDatabaseIntrospection({
@ -95,13 +95,13 @@ function createKloCliLiveDatabaseIntrospection(
};
}
interface KloCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
interface KtxCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
historicSqlConnectionId?: string;
sqlAnalysisUrl?: string;
}
function isEnabledPostgresHistoricSqlConnection(connection: KloPostgresConnectionConfig | undefined): boolean {
if (!connection || !isKloPostgresConnectionConfig(connection)) {
function isEnabledPostgresHistoricSqlConnection(connection: KtxPostgresConnectionConfig | undefined): boolean {
if (!connection || !isKtxPostgresConnectionConfig(connection)) {
return false;
}
const historicSql =
@ -113,16 +113,16 @@ function isEnabledPostgresHistoricSqlConnection(connection: KloPostgresConnectio
return historicSql?.enabled === true && historicSql.dialect === 'postgres';
}
function createEphemeralPostgresHistoricSqlClient(project: KloLocalProject, connectionId: string) {
const connection = project.config.connections[connectionId] as KloPostgresConnectionConfig | undefined;
if (!isKloPostgresConnectionConfig(connection)) {
function createEphemeralPostgresHistoricSqlClient(project: KtxLocalProject, connectionId: string) {
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
if (!isKtxPostgresConnectionConfig(connection)) {
throw new Error(
`Historic SQL local ingest requires a Postgres connection, got ${String(connection?.driver ?? 'unknown')}`,
);
}
return {
async executeQuery(sql: string, params?: unknown[]) {
const client = new KloPostgresHistoricSqlQueryClient({
const client = new KtxPostgresHistoricSqlQueryClient({
connectionId,
connection,
});
@ -135,12 +135,12 @@ function createEphemeralPostgresHistoricSqlClient(project: KloLocalProject, conn
};
}
function historicSqlOptionsForLocalRun(project: KloLocalProject, options: KloCliLocalIngestAdaptersOptions) {
function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCliLocalIngestAdaptersOptions) {
const connectionId = options.historicSqlConnectionId;
if (!connectionId) {
return undefined;
}
const connection = project.config.connections[connectionId] as KloPostgresConnectionConfig | undefined;
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
if (!isEnabledPostgresHistoricSqlConnection(connection)) {
return undefined;
}
@ -148,18 +148,18 @@ function historicSqlOptionsForLocalRun(project: KloLocalProject, options: KloCli
sqlAnalysis: createHttpSqlAnalysisPort({
baseUrl:
options.sqlAnalysisUrl ??
process.env.KLO_SQL_ANALYSIS_URL ??
process.env.KLO_DAEMON_URL ??
process.env.KTX_SQL_ANALYSIS_URL ??
process.env.KTX_DAEMON_URL ??
'http://127.0.0.1:8765',
}),
postgresQueryClient: createEphemeralPostgresHistoricSqlClient(project, connectionId),
postgresBaselineRootDir: join(project.projectDir, '.klo/cache/historic-sql'),
postgresBaselineRootDir: join(project.projectDir, '.ktx/cache/historic-sql'),
};
}
export function createKloCliLocalIngestAdapters(
project: KloLocalProject,
options: KloCliLocalIngestAdaptersOptions = {},
export function createKtxCliLocalIngestAdapters(
project: KtxLocalProject,
options: KtxCliLocalIngestAdaptersOptions = {},
): SourceAdapter[] {
const historicSql = historicSqlOptionsForLocalRun(project, options);
const base = createDefaultLocalIngestAdapters(project, {
@ -167,7 +167,7 @@ export function createKloCliLocalIngestAdapters(
...(historicSql ? { historicSql } : {}),
});
const liveDatabase = new LiveDatabaseSourceAdapter({
introspection: createKloCliLiveDatabaseIntrospection(project, options),
introspection: createKtxCliLiveDatabaseIntrospection(project, options),
});
return base.map((adapter) => (adapter.source === 'live-database' ? liveDatabase : adapter));
}

View file

@ -1,9 +1,9 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initKloProject, loadKloProject } from '@klo/context/project';
import { initKtxProject, loadKtxProject } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createKloCliScanConnector } from './local-scan-connectors.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js';
const bigQueryMock = vi.hoisted(() => ({
constructorInputs: [] as Array<{
@ -13,10 +13,10 @@ const bigQueryMock = vi.hoisted(() => ({
}>,
}));
vi.mock('@klo/connector-bigquery', () => ({
isKloBigQueryConnectionConfig: (connection: { driver?: unknown } | undefined) =>
vi.mock('@ktx/connector-bigquery', () => ({
isKtxBigQueryConnectionConfig: (connection: { driver?: unknown } | undefined) =>
String(connection?.driver ?? '').toLowerCase() === 'bigquery',
KloBigQueryScanConnector: class {
KtxBigQueryScanConnector: class {
readonly id: string;
readonly driver = 'bigquery';
@ -27,12 +27,12 @@ vi.mock('@klo/connector-bigquery', () => ({
},
}));
describe('createKloCliScanConnector', () => {
describe('createKtxCliScanConnector', () => {
let tempDir: string;
beforeEach(async () => {
bigQueryMock.constructorInputs.length = 0;
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-scan-connector-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-scan-connector-'));
});
afterEach(async () => {
@ -40,9 +40,9 @@ describe('createKloCliScanConnector', () => {
});
it('creates a native sqlite connector from standalone config', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -54,9 +54,9 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
const connector = await createKloCliScanConnector(project, 'warehouse');
const connector = await createKtxCliScanConnector(project, 'warehouse');
expect(connector.id).toBe('sqlite:warehouse');
expect(connector.driver).toBe('sqlite');
@ -66,9 +66,9 @@ describe('createKloCliScanConnector', () => {
['maxBytesBilled', ' maxBytesBilled: 123456789', 123456789],
['max_bytes_billed', ' max_bytes_billed: "987654321"', '987654321'],
])('passes BigQuery %s from standalone config', async (_label, byteCapLine, expectedMaxBytesBilled) => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -81,9 +81,9 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
const connector = await createKloCliScanConnector(project, 'warehouse');
const connector = await createKtxCliScanConnector(project, 'warehouse');
expect(connector.id).toBe('bigquery:warehouse');
expect(connector.driver).toBe('bigquery');
@ -96,9 +96,9 @@ describe('createKloCliScanConnector', () => {
});
it('does not create a standalone PostHog scan connector', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -111,17 +111,17 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
await expect(createKloCliScanConnector(project, 'product')).rejects.toThrow(
'Connection "product" uses driver "posthog", which has no native standalone KLO scan connector',
await expect(createKtxCliScanConnector(project, 'product')).rejects.toThrow(
'Connection "product" uses driver "posthog", which has no native standalone KTX scan connector',
);
});
it('throws for structural daemon-only fallback configs', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -132,17 +132,17 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
await expect(createKloCliScanConnector(project, 'warehouse')).rejects.toThrow(
'Connection "warehouse" uses driver "duckdb", which has no native standalone KLO scan connector',
await expect(createKtxCliScanConnector(project, 'warehouse')).rejects.toThrow(
'Connection "warehouse" uses driver "duckdb", which has no native standalone KTX scan connector',
);
});
it('throws a clear error when the connection block has no driver field', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -154,10 +154,10 @@ describe('createKloCliScanConnector', () => {
].join('\n'),
'utf-8',
);
const project = await loadKloProject({ projectDir: tempDir });
const project = await loadKtxProject({ projectDir: tempDir });
await expect(createKloCliScanConnector(project, 'warehouse')).rejects.toThrow(
'Connection "warehouse" has no `driver` field in klo.yaml',
await expect(createKtxCliScanConnector(project, 'warehouse')).rejects.toThrow(
'Connection "warehouse" has no `driver` field in ktx.yaml',
);
});
});

View file

@ -1,10 +1,10 @@
import type { KloLocalProject } from '@klo/context/project';
import type { KloScanConnector } from '@klo/context/scan';
import type { KtxLocalProject } from '@ktx/context/project';
import type { KtxScanConnector } from '@ktx/context/scan';
const SUPPORTED_DRIVERS = 'sqlite, postgres, mysql, clickhouse, sqlserver, bigquery, snowflake';
function bigQueryMaxBytesBilled(
connection: KloLocalProject['config']['connections'][string],
connection: KtxLocalProject['config']['connections'][string],
): number | string | undefined {
const raw = connection.maxBytesBilled ?? connection.max_bytes_billed;
if (typeof raw === 'number') {
@ -17,55 +17,55 @@ function bigQueryMaxBytesBilled(
return undefined;
}
export async function createKloCliScanConnector(
project: KloLocalProject,
export async function createKtxCliScanConnector(
project: KtxLocalProject,
connectionId: string,
): Promise<KloScanConnector> {
): Promise<KtxScanConnector> {
const connection = project.config.connections[connectionId];
if (!connection) {
throw new Error(`Connection "${connectionId}" is not configured in klo.yaml`);
throw new Error(`Connection "${connectionId}" is not configured in ktx.yaml`);
}
const driver = String(connection.driver ?? '').toLowerCase();
if (!driver) {
throw new Error(
`Connection "${connectionId}" has no \`driver\` field in klo.yaml. Supported drivers: ${SUPPORTED_DRIVERS}.`,
`Connection "${connectionId}" has no \`driver\` field in ktx.yaml. Supported drivers: ${SUPPORTED_DRIVERS}.`,
);
}
if (driver === 'sqlite' || driver === 'sqlite3') {
const { KloSqliteScanConnector, isKloSqliteConnectionConfig } = await import('@klo/connector-sqlite');
if (isKloSqliteConnectionConfig(connection)) {
return new KloSqliteScanConnector({ connectionId, connection, projectDir: project.projectDir });
const { KtxSqliteScanConnector, isKtxSqliteConnectionConfig } = await import('@ktx/connector-sqlite');
if (isKtxSqliteConnectionConfig(connection)) {
return new KtxSqliteScanConnector({ connectionId, connection, projectDir: project.projectDir });
}
}
if (driver === 'postgres' || driver === 'postgresql') {
const { KloPostgresScanConnector, isKloPostgresConnectionConfig } = await import('@klo/connector-postgres');
if (isKloPostgresConnectionConfig(connection)) {
return new KloPostgresScanConnector({ connectionId, connection });
const { KtxPostgresScanConnector, isKtxPostgresConnectionConfig } = await import('@ktx/connector-postgres');
if (isKtxPostgresConnectionConfig(connection)) {
return new KtxPostgresScanConnector({ connectionId, connection });
}
}
if (driver === 'mysql') {
const { KloMysqlScanConnector, isKloMysqlConnectionConfig } = await import('@klo/connector-mysql');
if (isKloMysqlConnectionConfig(connection)) {
return new KloMysqlScanConnector({ connectionId, connection });
const { KtxMysqlScanConnector, isKtxMysqlConnectionConfig } = await import('@ktx/connector-mysql');
if (isKtxMysqlConnectionConfig(connection)) {
return new KtxMysqlScanConnector({ connectionId, connection });
}
}
if (driver === 'clickhouse') {
const { KloClickHouseScanConnector, isKloClickHouseConnectionConfig } = await import('@klo/connector-clickhouse');
if (isKloClickHouseConnectionConfig(connection)) {
return new KloClickHouseScanConnector({ connectionId, connection });
const { KtxClickHouseScanConnector, isKtxClickHouseConnectionConfig } = await import('@ktx/connector-clickhouse');
if (isKtxClickHouseConnectionConfig(connection)) {
return new KtxClickHouseScanConnector({ connectionId, connection });
}
}
if (driver === 'sqlserver') {
const { KloSqlServerScanConnector, isKloSqlServerConnectionConfig } = await import('@klo/connector-sqlserver');
if (isKloSqlServerConnectionConfig(connection)) {
return new KloSqlServerScanConnector({ connectionId, connection });
const { KtxSqlServerScanConnector, isKtxSqlServerConnectionConfig } = await import('@ktx/connector-sqlserver');
if (isKtxSqlServerConnectionConfig(connection)) {
return new KtxSqlServerScanConnector({ connectionId, connection });
}
}
if (driver === 'bigquery') {
const { KloBigQueryScanConnector, isKloBigQueryConnectionConfig } = await import('@klo/connector-bigquery');
if (isKloBigQueryConnectionConfig(connection)) {
const { KtxBigQueryScanConnector, isKtxBigQueryConnectionConfig } = await import('@ktx/connector-bigquery');
if (isKtxBigQueryConnectionConfig(connection)) {
const maxBytesBilled = bigQueryMaxBytesBilled(connection);
return new KloBigQueryScanConnector({
return new KtxBigQueryScanConnector({
connectionId,
connection,
...(maxBytesBilled !== undefined ? { maxBytesBilled } : {}),
@ -73,12 +73,12 @@ export async function createKloCliScanConnector(
}
}
if (driver === 'snowflake') {
const { KloSnowflakeScanConnector, isKloSnowflakeConnectionConfig } = await import('@klo/connector-snowflake');
if (isKloSnowflakeConnectionConfig(connection)) {
return new KloSnowflakeScanConnector({ connectionId, connection });
const { KtxSnowflakeScanConnector, isKtxSnowflakeConnectionConfig } = await import('@ktx/connector-snowflake');
if (isKtxSnowflakeConnectionConfig(connection)) {
return new KtxSnowflakeScanConnector({ connectionId, connection });
}
}
throw new Error(
`Connection "${connectionId}" uses driver "${driver}", which has no native standalone KLO scan connector. Supported drivers: ${SUPPORTED_DRIVERS}.`,
`Connection "${connectionId}" uses driver "${driver}", which has no native standalone KTX scan connector. Supported drivers: ${SUPPORTED_DRIVERS}.`,
);
}

View file

@ -1,5 +1,5 @@
/* @jsxImportSource react */
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { Box, Text } from 'ink';
import React, { type ReactNode } from 'react';
import { buildDemoMetrics, formatCost, formatDuration } from './demo-metrics.js';
@ -315,7 +315,7 @@ function pad(str: string, width: number): string {
return str.length >= width ? str : str + ' '.repeat(width - str.length);
}
const KLO_LOGO_SMALL = [
const KTX_LOGO_SMALL = [
'██╗ ██╗██╗ ██████╗ ',
'██║ ██╔╝██║ ██╔═══██╗',
'█████╔╝ ██║ ██║ ██║',
@ -328,7 +328,7 @@ export function Logo(props: { theme: HudTheme; done: boolean }): ReactNode {
const color = props.done ? props.theme.complete : props.theme.active;
return (
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
{KLO_LOGO_SMALL.map((line, idx) => (
{KTX_LOGO_SMALL.map((line, idx) => (
<Text key={idx} color={color}>
{line}
</Text>
@ -492,7 +492,7 @@ export function ActivityFeed(props: {
</Box>
)}
{/* Results — what KLO has created */}
{/* Results — what KTX has created */}
{insights.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text color={props.theme.text}> Created so far:</Text>
@ -524,7 +524,7 @@ export function ActivityFeed(props: {
<Text color={props.theme.active}>{spinner(props.frame)} Saving to context layer...</Text>
)}
{savedEvent && (
<Text color={props.theme.complete}> Saved your agents can now use the KLO context layer</Text>
<Text color={props.theme.complete}> Saved your agents can now use the KTX context layer</Text>
)}
{/* Phase 7: Completion */}
@ -559,12 +559,12 @@ function CompletionSummary(props: {
<>
<Text color={props.theme.border}>{'─'.repeat(60)}</Text>
<Text bold color={props.theme.complete}>
KLO finished ingesting your data
KTX finished ingesting your data
</Text>
{(sl > 0 || wiki > 0) && (
<>
<Text />
<Text color={props.theme.text}>KLO created:</Text>
<Text color={props.theme.text}>KTX created:</Text>
{sl > 0 && (
<Text color={props.theme.active}>
{' '}📊 {sl} query definition{sl === 1 ? '' : 's'} so agents can write accurate SQL for your data

View file

@ -1,5 +1,5 @@
import { EventEmitter } from 'node:events';
import type { MemoryFlowReplayInput } from '@klo/context/ingest';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest';
import { describe, expect, it, vi } from 'vitest';
import { memoryFlowCommandForKey, renderMemoryFlowInteractively } from './memory-flow-interactive.js';

View file

@ -7,26 +7,26 @@ import {
type MemoryFlowInteractionCommand,
type MemoryFlowInteractionState,
type MemoryFlowReplayInput,
} from '@klo/context/ingest';
} from '@ktx/context/ingest';
interface KloMemoryFlowKey {
interface KtxMemoryFlowKey {
name?: string;
ctrl?: boolean;
}
export interface KloMemoryFlowStdin {
export interface KtxMemoryFlowStdin {
isTTY?: boolean;
isRaw?: boolean;
setRawMode?(value: boolean): void;
resume?(): void;
pause?(): void;
on(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
off?(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
removeListener?(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
on(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
off?(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
removeListener?(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
}
interface KloMemoryFlowInteractiveIo {
stdin?: KloMemoryFlowStdin;
interface KtxMemoryFlowInteractiveIo {
stdin?: KtxMemoryFlowStdin;
stdout: {
isTTY?: boolean;
columns?: number;
@ -35,17 +35,17 @@ interface KloMemoryFlowInteractiveIo {
}
interface RenderMemoryFlowInteractiveOptions {
prepareKeypressEvents?(stdin: KloMemoryFlowStdin): void;
prepareKeypressEvents?(stdin: KtxMemoryFlowStdin): void;
}
function defaultPrepareKeypressEvents(stdin: KloMemoryFlowStdin): void {
function defaultPrepareKeypressEvents(stdin: KtxMemoryFlowStdin): void {
emitKeypressEvents(stdin as Parameters<typeof emitKeypressEvents>[0]);
}
export function memoryFlowCommandForKey(
chunk: string,
search: MemoryFlowInteractionState['search'],
key: KloMemoryFlowKey,
key: KtxMemoryFlowKey,
): MemoryFlowInteractionCommand | null {
if (search.editing) {
if (key.name === 'escape') return 'search-clear';
@ -76,8 +76,8 @@ export function memoryFlowCommandForKey(
}
function removeKeypressListener(
stdin: KloMemoryFlowStdin,
handler: (chunk: string, key: KloMemoryFlowKey) => void,
stdin: KtxMemoryFlowStdin,
handler: (chunk: string, key: KtxMemoryFlowKey) => void,
): void {
if (stdin.off) {
stdin.off('keypress', handler);
@ -86,7 +86,7 @@ function removeKeypressListener(
stdin.removeListener?.('keypress', handler);
}
function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState, io: KloMemoryFlowInteractiveIo): void {
function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState, io: KtxMemoryFlowInteractiveIo): void {
const view = buildMemoryFlowViewModel(input);
io.stdout.write('\u001b[2J\u001b[H');
io.stdout.write(renderMemoryFlowInteractive(view, state, { terminalWidth: io.stdout.columns }));
@ -94,7 +94,7 @@ function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState
export async function renderMemoryFlowInteractively(
input: MemoryFlowReplayInput,
io: KloMemoryFlowInteractiveIo,
io: KtxMemoryFlowInteractiveIo,
options: RenderMemoryFlowInteractiveOptions = {},
): Promise<void> {
const stdin = io.stdin;
@ -119,7 +119,7 @@ export async function renderMemoryFlowInteractively(
stdin.pause?.();
};
const handleKeypress = (_chunk: string, key: KloMemoryFlowKey): void => {
const handleKeypress = (_chunk: string, key: KtxMemoryFlowKey): void => {
const command = memoryFlowCommandForKey(_chunk, state.search, key);
if (!command) {
return;

View file

@ -1,5 +1,5 @@
/* @jsxImportSource react */
import type { MemoryFlowReplayInput } from '@klo/context/ingest';
import type { MemoryFlowReplayInput } from '@ktx/context/ingest';
import { render as renderInkTest } from 'ink-testing-library';
import React, { type ReactNode } from 'react';
import { describe, expect, it, vi } from 'vitest';
@ -9,7 +9,7 @@ import {
renderMemoryFlowTui,
sanitizeMemoryFlowTuiError,
startLiveMemoryFlowTui,
type KloMemoryFlowTuiIo,
type KtxMemoryFlowTuiIo,
type MemoryFlowInkInstance,
type MemoryFlowInkRenderOptions,
} from './memory-flow-tui.js';
@ -72,7 +72,7 @@ function packagedReplayInput(overrides: Partial<MemoryFlowReplayInput> = {}): Me
};
}
function makeIo(): { io: KloMemoryFlowTuiIo; stderr: () => string } {
function makeIo(): { io: KtxMemoryFlowTuiIo; stderr: () => string } {
let stderr = '';
return { io: { stdin: { isTTY: true, setRawMode: vi.fn() }, stdout: { isTTY: true, columns: 120, write: vi.fn() }, stderr: { write(chunk: string) { stderr += chunk; } } }, stderr: () => stderr };
}
@ -103,7 +103,7 @@ describe('sanitizeMemoryFlowTuiError', () => {
});
describe('MemoryFlowTuiApp', () => {
it('always shows the KLO logo', () => {
it('always shows the KTX logo', () => {
const { lastFrame } = renderInkTest(<MemoryFlowTuiApp input={replayInput()} terminalWidth={120} onExit={vi.fn()} showBoot={false} />);
expect(lastFrame()).toContain('█████╔╝');
});
@ -198,12 +198,12 @@ describe('MemoryFlowTuiApp', () => {
expect(frame).toContain('Created so far:');
expect(frame).toContain('order lifecycle');
expect(frame).toContain('customer metrics');
expect(frame).toContain('KLO finished ingesting your data');
expect(frame).toContain('klo sl list');
expect(frame).toContain('klo wiki list');
expect(frame).toContain('klo serve --mcp stdio --user-id local');
expect(frame).not.toContain(['klo', 'ask'].join(' '));
expect(frame).not.toContain(['klo', 'mcp'].join(' '));
expect(frame).toContain('KTX finished ingesting your data');
expect(frame).toContain('ktx sl list');
expect(frame).toContain('ktx wiki list');
expect(frame).toContain('ktx serve --mcp stdio --user-id local');
expect(frame).not.toContain(['ktx', 'ask'].join(' '));
expect(frame).not.toContain(['ktx', 'mcp'].join(' '));
});
it('handles quit while running', async () => {
@ -254,7 +254,7 @@ describe('MemoryFlowTuiApp', () => {
it('hides completion while running', () => {
const { lastFrame } = renderInkTest(<MemoryFlowTuiApp input={runningReplayInput()} terminalWidth={120} onExit={vi.fn()} showBoot={false} />);
expect(lastFrame()).not.toContain('KLO finished ingesting');
expect(lastFrame()).not.toContain('KTX finished ingesting');
});
});

View file

@ -12,7 +12,7 @@ import {
reduceMemoryFlowInteractionState,
selectedMemoryFlowColumn,
selectedMemoryFlowDetails,
} from '@klo/context/ingest';
} from '@ktx/context/ingest';
import { Box, Text, render as renderInkRuntime, useApp, useInput } from 'ink';
import React, { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { buildDemoMetrics } from './demo-metrics.js';
@ -56,7 +56,7 @@ const STAGE_LABELS = {
saved: 'MEMORY',
} satisfies Record<MemoryFlowColumnId, string>;
export interface KloMemoryFlowTuiIo {
export interface KtxMemoryFlowTuiIo {
stdin?: { isTTY?: boolean; setRawMode?(value: boolean): void };
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
stderr: { write(chunk: string): void };
@ -76,9 +76,9 @@ export interface MemoryFlowInkInstance {
}
export interface MemoryFlowInkRenderOptions {
stdin?: KloMemoryFlowTuiIo['stdin'];
stdout: KloMemoryFlowTuiIo['stdout'];
stderr: KloMemoryFlowTuiIo['stderr'];
stdin?: KtxMemoryFlowTuiIo['stdin'];
stdout: KtxMemoryFlowTuiIo['stdout'];
stderr: KtxMemoryFlowTuiIo['stderr'];
exitOnCtrlC: boolean;
patchConsole: boolean;
maxFps: number;
@ -429,7 +429,7 @@ export function MemoryFlowTuiApp(props: MemoryFlowTuiAppProps): ReactNode {
function renderTree(
input: MemoryFlowReplayInput,
io: KloMemoryFlowTuiIo,
io: KtxMemoryFlowTuiIo,
onExit: () => void,
options: RenderTreeOptions = {},
): ReactNode {
@ -459,7 +459,7 @@ function renderInk(tree: ReactNode, options: MemoryFlowInkRenderOptions): Memory
}) as MemoryFlowInkInstance;
}
function renderOptions(io: KloMemoryFlowTuiIo): MemoryFlowInkRenderOptions {
function renderOptions(io: KtxMemoryFlowTuiIo): MemoryFlowInkRenderOptions {
return {
stdin: io.stdin,
stdout: io.stdout,
@ -491,7 +491,7 @@ function resolveTiming(options: RenderMemoryFlowTuiOptions): MemoryFlowTuiTiming
export async function renderMemoryFlowTui(
input: MemoryFlowReplayInput,
io: KloMemoryFlowTuiIo,
io: KtxMemoryFlowTuiIo,
options: RenderMemoryFlowTuiOptions = {},
): Promise<boolean> {
let instance: MemoryFlowInkInstance | null = null;
@ -516,7 +516,7 @@ export async function renderMemoryFlowTui(
export async function startLiveMemoryFlowTui(
input: MemoryFlowReplayInput,
io: KloMemoryFlowTuiIo,
io: KtxMemoryFlowTuiIo,
options: StartLiveMemoryFlowTuiOptions = {},
): Promise<MemoryFlowTuiLiveSession | null> {
let instance: MemoryFlowInkInstance | null = null;

View file

@ -1,96 +1,96 @@
import { describe, expect, it } from 'vitest';
import {
KLO_CONTEXT_BUILD_COMMANDS,
KLO_NEXT_STEP_COMMANDS,
KTX_CONTEXT_BUILD_COMMANDS,
KTX_NEXT_STEP_COMMANDS,
formatNextStepLines,
formatSetupNextStepLines,
} from './next-steps.js';
const command = (...parts: string[]) => parts.join(' ');
describe('KLO demo next steps', () => {
describe('KTX demo next steps', () => {
it('uses supported context-build commands before agent usage', () => {
expect(KLO_CONTEXT_BUILD_COMMANDS).toEqual([
expect(KTX_CONTEXT_BUILD_COMMANDS).toEqual([
{
command: 'klo setup context build',
command: 'ktx setup context build',
description: 'Build agent-ready context from configured primary and context sources',
},
{
command: 'klo status',
command: 'ktx status',
description: 'Check setup and context readiness',
},
{
command: 'klo setup context status',
command: 'ktx setup context status',
description: 'Check the setup-managed context build state',
},
]);
});
it('uses supported final public commands', () => {
expect(KLO_NEXT_STEP_COMMANDS).toEqual([
expect(KTX_NEXT_STEP_COMMANDS).toEqual([
{
command: 'klo agent context --json',
command: 'ktx agent context --json',
description: 'Verify the project context your agent can read',
},
{
command: 'klo agent tools --json',
command: 'ktx agent tools --json',
description: 'List direct CLI tools available to agents',
},
{
command: 'klo sl list',
command: 'ktx sl list',
description: 'Inspect generated semantic-layer sources',
},
{
command: 'klo wiki list',
command: 'ktx wiki list',
description: 'Inspect generated wiki pages',
},
{
command: 'klo serve --mcp stdio --user-id local',
command: 'ktx serve --mcp stdio --user-id local',
description: 'Optional MCP server route for clients that require MCP',
},
]);
});
it('prefers the direct CLI route before MCP serving', () => {
const commands = KLO_NEXT_STEP_COMMANDS.map((step) => step.command);
const commands = KTX_NEXT_STEP_COMMANDS.map((step) => step.command);
expect(commands.indexOf('klo agent context --json')).toBeLessThan(
commands.indexOf('klo serve --mcp stdio --user-id local'),
expect(commands.indexOf('ktx agent context --json')).toBeLessThan(
commands.indexOf('ktx serve --mcp stdio --user-id local'),
);
expect(commands.indexOf('klo agent tools --json')).toBeLessThan(
commands.indexOf('klo serve --mcp stdio --user-id local'),
expect(commands.indexOf('ktx agent tools --json')).toBeLessThan(
commands.indexOf('ktx serve --mcp stdio --user-id local'),
);
});
it('explains what the next-step commands are for', () => {
const rendered = formatNextStepLines().join('\n');
expect(rendered).toContain('KLO context is ready for agents.');
expect(rendered).toContain('KTX context is ready for agents.');
expect(rendered).toContain('Preferred route: CLI + Skills');
expect(rendered).toContain('no MCP server is required');
expect(rendered).toContain('Direct CLI checks:');
expect(rendered).toContain('Optional MCP:');
expect(rendered).not.toContain('Ask your agent to use KLO');
expect(rendered).not.toContain('Ask your agent to use KTX');
});
it('does not advertise removed Commander migration commands', () => {
const rendered = formatNextStepLines().join('\n');
expect(rendered).toContain('klo agent tools --json');
expect(rendered).toContain('klo agent context --json');
expect(rendered).toContain('klo sl list');
expect(rendered).toContain('klo wiki list');
expect(rendered).toContain('klo serve --mcp stdio --user-id local');
expect(rendered).toContain('ktx agent tools --json');
expect(rendered).toContain('ktx agent context --json');
expect(rendered).toContain('ktx sl list');
expect(rendered).toContain('ktx wiki list');
expect(rendered).toContain('ktx serve --mcp stdio --user-id local');
for (const removed of [
command('klo', 'ask'),
command('klo', 'mcp'),
command('klo', 'connect'),
command('klo', 'knowledge'),
command('ktx', 'ask'),
command('ktx', 'mcp'),
command('ktx', 'connect'),
command('ktx', 'knowledge'),
command('dev', 'model'),
command('dev', 'knowledge'),
command('klo', 'ingest', 'run'),
command('klo', 'ingest', 'replay'),
command('ktx', 'ingest', 'run'),
command('ktx', 'ingest', 'replay'),
]) {
expect(rendered).not.toContain(removed);
}
@ -104,13 +104,13 @@ describe('KLO demo next steps', () => {
agentIntegrationReady: true,
}).join('\n');
expect(rendered).toContain('Build KLO context next.');
expect(rendered).toContain('Build KTX context next.');
expect(rendered).toContain('primary-source scans and context-source ingests');
expect(rendered).toContain('klo setup context build');
expect(rendered).toContain('klo status');
expect(rendered).toContain('klo setup context status');
expect(rendered).not.toContain('klo agent context --json');
expect(rendered).not.toContain('klo serve --mcp');
expect(rendered).toContain('ktx setup context build');
expect(rendered).toContain('ktx status');
expect(rendered).toContain('ktx setup context status');
expect(rendered).not.toContain('ktx agent context --json');
expect(rendered).not.toContain('ktx serve --mcp');
});
it('shows agent commands only after setup and context build are ready', () => {
@ -121,9 +121,9 @@ describe('KLO demo next steps', () => {
agentIntegrationReady: true,
}).join('\n');
expect(rendered).toContain('KLO context is ready for agents.');
expect(rendered).toContain('klo agent context --json');
expect(rendered).toContain('klo serve --mcp stdio --user-id local');
expect(rendered).not.toContain('Build KLO context next.');
expect(rendered).toContain('KTX context is ready for agents.');
expect(rendered).toContain('ktx agent context --json');
expect(rendered).toContain('ktx serve --mcp stdio --user-id local');
expect(rendered).not.toContain('Build KTX context next.');
});
});

View file

@ -1,51 +1,51 @@
export const KLO_CONTEXT_BUILD_COMMANDS = [
export const KTX_CONTEXT_BUILD_COMMANDS = [
{
command: 'klo setup context build',
command: 'ktx setup context build',
description: 'Build agent-ready context from configured primary and context sources',
},
{
command: 'klo status',
command: 'ktx status',
description: 'Check setup and context readiness',
},
{
command: 'klo setup context status',
command: 'ktx setup context status',
description: 'Check the setup-managed context build state',
},
] as const;
export const KLO_NEXT_STEP_DIRECT_COMMANDS = [
export const KTX_NEXT_STEP_DIRECT_COMMANDS = [
{
command: 'klo agent context --json',
command: 'ktx agent context --json',
description: 'Verify the project context your agent can read',
},
{
command: 'klo agent tools --json',
command: 'ktx agent tools --json',
description: 'List direct CLI tools available to agents',
},
{
command: 'klo sl list',
command: 'ktx sl list',
description: 'Inspect generated semantic-layer sources',
},
{
command: 'klo wiki list',
command: 'ktx wiki list',
description: 'Inspect generated wiki pages',
},
] as const;
export const KLO_NEXT_STEP_MCP_COMMANDS = [
export const KTX_NEXT_STEP_MCP_COMMANDS = [
{
command: 'klo serve --mcp stdio --user-id local',
command: 'ktx serve --mcp stdio --user-id local',
description: 'Optional MCP server route for clients that require MCP',
},
] as const;
export const KLO_NEXT_STEP_COMMANDS = [...KLO_NEXT_STEP_DIRECT_COMMANDS, ...KLO_NEXT_STEP_MCP_COMMANDS] as const;
export const KTX_NEXT_STEP_COMMANDS = [...KTX_NEXT_STEP_DIRECT_COMMANDS, ...KTX_NEXT_STEP_MCP_COMMANDS] as const;
export const KLO_NEXT_STEP_COMMAND_WIDTH = Math.max(
...[...KLO_CONTEXT_BUILD_COMMANDS, ...KLO_NEXT_STEP_COMMANDS].map((step) => step.command.length),
export const KTX_NEXT_STEP_COMMAND_WIDTH = Math.max(
...[...KTX_CONTEXT_BUILD_COMMANDS, ...KTX_NEXT_STEP_COMMANDS].map((step) => step.command.length),
);
export interface KloSetupNextStepState {
export interface KtxSetupNextStepState {
setupReady: boolean;
hasContextTargets: boolean;
contextReady: boolean;
@ -53,50 +53,50 @@ export interface KloSetupNextStepState {
}
function commandLines(commands: ReadonlyArray<{ command: string; description: string }>, indent: string): string[] {
return commands.map((step) => `${indent}$ ${step.command.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} ${step.description}`);
return commands.map((step) => `${indent}$ ${step.command.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} ${step.description}`);
}
export function formatNextStepLines(indent = ' '): string[] {
return [
`${indent}KLO context is ready for agents.`,
`${indent}Preferred route: CLI + Skills; installed rules call \`klo agent ...\` directly, so no MCP server is required.`,
`${indent}KTX context is ready for agents.`,
`${indent}Preferred route: CLI + Skills; installed rules call \`ktx agent ...\` directly, so no MCP server is required.`,
`${indent}Direct CLI checks:`,
...commandLines(KLO_NEXT_STEP_DIRECT_COMMANDS, indent),
...commandLines(KTX_NEXT_STEP_DIRECT_COMMANDS, indent),
`${indent}Optional MCP:`,
...commandLines(KLO_NEXT_STEP_MCP_COMMANDS, indent),
...commandLines(KTX_NEXT_STEP_MCP_COMMANDS, indent),
];
}
export function formatSetupNextStepLines(state: KloSetupNextStepState, indent = ' '): string[] {
export function formatSetupNextStepLines(state: KtxSetupNextStepState, indent = ' '): string[] {
if (!state.setupReady) {
return [
`${indent}Finish setup first.`,
`${indent}$ ${'klo setup'.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} Resume configuration and validation`,
`${indent}$ ${'klo status'.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} Check which setup steps still need attention`,
`${indent}$ ${'ktx setup'.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} Resume configuration and validation`,
`${indent}$ ${'ktx status'.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} Check which setup steps still need attention`,
];
}
if (!state.hasContextTargets) {
return [
`${indent}Connect data, then build context.`,
`${indent}$ ${'klo setup'.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} Add primary or context sources`,
`${indent}$ ${'klo status'.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} Check setup and context readiness`,
`${indent}$ ${'ktx setup'.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} Add primary or context sources`,
`${indent}$ ${'ktx status'.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} Check setup and context readiness`,
];
}
if (!state.contextReady) {
return [
`${indent}Build KLO context next.`,
`${indent}Build KTX context next.`,
`${indent}Preferred route: run the CLI build; it covers primary-source scans and context-source ingests.`,
...commandLines(KLO_CONTEXT_BUILD_COMMANDS, indent),
...commandLines(KTX_CONTEXT_BUILD_COMMANDS, indent),
];
}
if (!state.agentIntegrationReady) {
return [
`${indent}KLO context is built. Install agent rules when you want your coding agent to use it.`,
`${indent}$ ${'klo setup --agents'.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} Install CLI-based agent rules`,
`${indent}$ ${'klo status'.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} Check setup and context readiness`,
`${indent}KTX context is built. Install agent rules when you want your coding agent to use it.`,
`${indent}$ ${'ktx setup --agents'.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} Install CLI-based agent rules`,
`${indent}$ ${'ktx status'.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} Check setup and context readiness`,
];
}

View file

@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { runKloCli, type KloCliDeps } from './index.js';
import { runKtxCli, type KtxCliDeps } from './index.js';
function makeIo() {
let stdout = '';
@ -24,11 +24,11 @@ function makeIo() {
describe('project directory defaults', () => {
afterEach(() => {
delete process.env.KLO_PROJECT_DIR;
delete process.env.KTX_PROJECT_DIR;
});
it('uses KLO_PROJECT_DIR when Commander-dispatched commands omit --project-dir', async () => {
process.env.KLO_PROJECT_DIR = '/tmp/klo-env-project';
it('uses KTX_PROJECT_DIR when Commander-dispatched commands omit --project-dir', async () => {
process.env.KTX_PROJECT_DIR = '/tmp/ktx-env-project';
const connection = vi.fn(async () => 0);
const demo = vi.fn(async () => 0);
@ -39,7 +39,7 @@ describe('project directory defaults', () => {
const serveStdio = vi.fn(async () => 0);
const setup = vi.fn(async () => 0);
const agent = vi.fn(async () => 0);
const deps: KloCliDeps = { agent, connection, demo, doctor, ingest, publicIngest, scan, serveStdio, setup };
const deps: KtxCliDeps = { agent, connection, demo, doctor, ingest, publicIngest, scan, serveStdio, setup };
const cases: Array<{
argv: string[];
@ -50,56 +50,56 @@ describe('project directory defaults', () => {
{
argv: ['connection', 'list'],
spy: connection,
expected: { command: 'list', projectDir: '/tmp/klo-env-project' },
expected: { command: 'list', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
},
{
argv: ['setup', 'demo', 'scan', '--no-input'],
spy: demo,
expected: { command: 'scan', projectDir: '/tmp/klo-env-project' },
expected: { command: 'scan', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
},
{
argv: ['dev', 'doctor', '--no-input'],
spy: doctor,
expected: { command: 'project', projectDir: '/tmp/klo-env-project' },
expected: { command: 'project', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
},
{
argv: ['ingest', 'status', 'run-1'],
spy: publicIngest,
expected: { command: 'status', projectDir: '/tmp/klo-env-project', runId: 'run-1' },
expected: { command: 'status', projectDir: '/tmp/ktx-env-project', runId: 'run-1' },
runnerType: 'cli',
},
{
argv: ['setup', 'status'],
spy: setup,
expected: { command: 'status', projectDir: '/tmp/klo-env-project' },
expected: { command: 'status', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
},
{
argv: ['dev', 'scan', 'warehouse'],
spy: scan,
expected: { command: 'run', projectDir: '/tmp/klo-env-project', connectionId: 'warehouse' },
expected: { command: 'run', projectDir: '/tmp/ktx-env-project', connectionId: 'warehouse' },
runnerType: 'cli',
},
{
argv: ['serve', '--mcp', 'stdio'],
spy: serveStdio,
expected: { mcp: 'stdio', projectDir: '/tmp/klo-env-project' },
expected: { mcp: 'stdio', projectDir: '/tmp/ktx-env-project' },
runnerType: 'serve',
},
{
argv: ['agent', 'tools', '--json'],
spy: agent,
expected: { command: 'tools', projectDir: '/tmp/klo-env-project' },
expected: { command: 'tools', projectDir: '/tmp/ktx-env-project' },
runnerType: 'cli',
},
];
for (const item of cases) {
const testIo = makeIo();
await expect(runKloCli(item.argv, testIo.io, deps)).resolves.toBe(0);
await expect(runKtxCli(item.argv, testIo.io, deps)).resolves.toBe(0);
if (item.runnerType === 'serve') {
expect(item.spy).toHaveBeenLastCalledWith(expect.objectContaining(item.expected));
} else {
@ -109,8 +109,8 @@ describe('project directory defaults', () => {
}
});
it('lets explicit global --project-dir override KLO_PROJECT_DIR before and after nested commands', async () => {
process.env.KLO_PROJECT_DIR = '/tmp/klo-env-project';
it('lets explicit global --project-dir override KTX_PROJECT_DIR before and after nested commands', async () => {
process.env.KTX_PROJECT_DIR = '/tmp/ktx-env-project';
const scan = vi.fn(async () => 0);
const publicIngest = vi.fn(async () => 0);
@ -118,38 +118,38 @@ describe('project directory defaults', () => {
const ingestIo = makeIo();
await expect(
runKloCli(['--project-dir', '/tmp/klo-explicit-project', 'dev', 'scan', 'warehouse'], scanIo.io, { scan }),
runKtxCli(['--project-dir', '/tmp/ktx-explicit-project', 'dev', 'scan', 'warehouse'], scanIo.io, { scan }),
).resolves.toBe(0);
await expect(
runKloCli(['ingest', 'status', 'run-1', '--project-dir=/tmp/klo-explicit-project'], ingestIo.io, {
runKtxCli(['ingest', 'status', 'run-1', '--project-dir=/tmp/ktx-explicit-project'], ingestIo.io, {
publicIngest,
}),
).resolves.toBe(0);
expect(scan).toHaveBeenCalledWith(
expect.objectContaining({ command: 'run', projectDir: '/tmp/klo-explicit-project' }),
expect.objectContaining({ command: 'run', projectDir: '/tmp/ktx-explicit-project' }),
scanIo.io,
);
expect(publicIngest).toHaveBeenCalledWith(
expect.objectContaining({ command: 'status', projectDir: '/tmp/klo-explicit-project' }),
expect.objectContaining({ command: 'status', projectDir: '/tmp/ktx-explicit-project' }),
ingestIo.io,
);
expect(scanIo.stderr()).toBe('');
expect(ingestIo.stderr()).toBe('');
});
it('uses nearest ancestor containing klo.yaml when no explicit or environment project-dir exists', async () => {
it('uses nearest ancestor containing ktx.yaml when no explicit or environment project-dir exists', async () => {
const { mkdir, realpath, writeFile } = await import('node:fs/promises');
const { mkdtemp, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const originalCwd = process.cwd();
const root = await mkdtemp(join(tmpdir(), 'klo-cli-nearest-project-'));
const root = await mkdtemp(join(tmpdir(), 'ktx-cli-nearest-project-'));
const projectDir = join(root, 'warehouse');
const nestedDir = join(projectDir, 'nested', 'deeper');
await mkdir(nestedDir, { recursive: true });
await writeFile(join(projectDir, 'klo.yaml'), 'project: warehouse\n', 'utf-8');
await writeFile(join(projectDir, 'ktx.yaml'), 'project: warehouse\n', 'utf-8');
const expectedProjectDir = await realpath(projectDir);
const scan = vi.fn(async () => 0);
@ -157,7 +157,7 @@ describe('project directory defaults', () => {
try {
process.chdir(nestedDir);
await expect(runKloCli(['dev', 'scan', 'warehouse'], testIo.io, { scan })).resolves.toBe(0);
await expect(runKtxCli(['dev', 'scan', 'warehouse'], testIo.io, { scan })).resolves.toBe(0);
} finally {
process.chdir(originalCwd);
await rm(root, { recursive: true, force: true });

View file

@ -3,13 +3,13 @@ import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { findNearestKloProjectDir, resolveKloProjectDir } from './project-resolver.js';
import { findNearestKtxProjectDir, resolveKtxProjectDir } from './project-resolver.js';
describe('resolveKloProjectDir', () => {
describe('resolveKtxProjectDir', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-project-resolver-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-project-resolver-'));
});
afterEach(async () => {
@ -23,48 +23,48 @@ describe('resolveKloProjectDir', () => {
await mkdir(envProject, { recursive: true });
expect(
resolveKloProjectDir({
resolveKtxProjectDir({
explicitProjectDir: explicit,
env: { KLO_PROJECT_DIR: envProject },
env: { KTX_PROJECT_DIR: envProject },
cwd: tempDir,
}),
).toBe(resolve(explicit));
});
it('uses KLO_PROJECT_DIR when no explicit project directory is set', async () => {
it('uses KTX_PROJECT_DIR when no explicit project directory is set', async () => {
const envProject = join(tempDir, 'env-project');
await mkdir(envProject, { recursive: true });
expect(resolveKloProjectDir({ env: { KLO_PROJECT_DIR: envProject }, cwd: tempDir })).toBe(resolve(envProject));
expect(resolveKtxProjectDir({ env: { KTX_PROJECT_DIR: envProject }, cwd: tempDir })).toBe(resolve(envProject));
});
it('resolves a relative KLO_PROJECT_DIR value from cwd', () => {
expect(resolveKloProjectDir({ env: { KLO_PROJECT_DIR: 'env-project' }, cwd: tempDir })).toBe(
it('resolves a relative KTX_PROJECT_DIR value from cwd', () => {
expect(resolveKtxProjectDir({ env: { KTX_PROJECT_DIR: 'env-project' }, cwd: tempDir })).toBe(
resolve(tempDir, 'env-project'),
);
});
it('uses the nearest ancestor containing klo.yaml', async () => {
it('uses the nearest ancestor containing ktx.yaml', async () => {
const project = join(tempDir, 'warehouse');
const nested = join(project, 'nested', 'deeper');
await mkdir(nested, { recursive: true });
await writeFile(join(project, 'klo.yaml'), 'project: warehouse\n', 'utf-8');
await writeFile(join(project, 'ktx.yaml'), 'project: warehouse\n', 'utf-8');
expect(resolveKloProjectDir({ env: {}, cwd: nested })).toBe(resolve(project));
expect(findNearestKloProjectDir(nested)).toBe(resolve(project));
expect(resolveKtxProjectDir({ env: {}, cwd: nested })).toBe(resolve(project));
expect(findNearestKtxProjectDir(nested)).toBe(resolve(project));
});
it('falls back to the current directory when no project marker exists', () => {
expect(resolveKloProjectDir({ env: {}, cwd: tempDir })).toBe(resolve(tempDir));
expect(findNearestKloProjectDir(tempDir)).toBeUndefined();
expect(resolveKtxProjectDir({ env: {}, cwd: tempDir })).toBe(resolve(tempDir));
expect(findNearestKtxProjectDir(tempDir)).toBeUndefined();
});
it('rejects empty explicit and environment project directory values', () => {
expect(() => resolveKloProjectDir({ explicitProjectDir: ' ', cwd: tempDir })).toThrow(
expect(() => resolveKtxProjectDir({ explicitProjectDir: ' ', cwd: tempDir })).toThrow(
'--project-dir requires a value',
);
expect(() => resolveKloProjectDir({ env: { KLO_PROJECT_DIR: ' ' }, cwd: tempDir })).toThrow(
'KLO_PROJECT_DIR must not be empty',
expect(() => resolveKtxProjectDir({ env: { KTX_PROJECT_DIR: ' ' }, cwd: tempDir })).toThrow(
'KTX_PROJECT_DIR must not be empty',
);
});
});

View file

@ -1,9 +1,9 @@
import { existsSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
export interface KloProjectResolverOptions {
export interface KtxProjectResolverOptions {
explicitProjectDir?: string;
env?: Partial<Pick<NodeJS.ProcessEnv, 'KLO_PROJECT_DIR'>>;
env?: Partial<Pick<NodeJS.ProcessEnv, 'KTX_PROJECT_DIR'>>;
cwd?: string;
}
@ -15,11 +15,11 @@ function nonEmptyValue(value: string | undefined): string | undefined {
return trimmed.length > 0 ? value : undefined;
}
export function findNearestKloProjectDir(startDir = process.cwd()): string | undefined {
export function findNearestKtxProjectDir(startDir = process.cwd()): string | undefined {
let current = resolve(startDir);
while (true) {
if (existsSync(join(current, 'klo.yaml'))) {
if (existsSync(join(current, 'ktx.yaml'))) {
return current;
}
@ -31,7 +31,7 @@ export function findNearestKloProjectDir(startDir = process.cwd()): string | und
}
}
export function resolveKloProjectDir(options: KloProjectResolverOptions = {}): string {
export function resolveKtxProjectDir(options: KtxProjectResolverOptions = {}): string {
const cwd = options.cwd ?? process.cwd();
if (options.explicitProjectDir !== undefined) {
@ -42,15 +42,15 @@ export function resolveKloProjectDir(options: KloProjectResolverOptions = {}): s
return resolve(cwd, explicit);
}
const rawEnvProjectDir = options.env ? options.env.KLO_PROJECT_DIR : process.env.KLO_PROJECT_DIR;
const rawEnvProjectDir = options.env ? options.env.KTX_PROJECT_DIR : process.env.KTX_PROJECT_DIR;
const envProjectDir = nonEmptyValue(rawEnvProjectDir);
if (rawEnvProjectDir !== undefined && envProjectDir === undefined) {
throw new Error('KLO_PROJECT_DIR must not be empty');
throw new Error('KTX_PROJECT_DIR must not be empty');
}
if (envProjectDir !== undefined) {
return resolve(cwd, envProjectDir);
}
const resolvedCwd = resolve(cwd);
return findNearestKloProjectDir(resolvedCwd) ?? resolvedCwd;
return findNearestKtxProjectDir(resolvedCwd) ?? resolvedCwd;
}

View file

@ -7,8 +7,8 @@ describe('prompt navigation helpers', () => {
});
it('adds a blank separator between multiline menu copy and the option list', () => {
expect(withMenuOptionSpacing('Which embedding option should KLO use?\n\nKLO uses embeddings for search.')).toBe(
'Which embedding option should KLO use?\n\nKLO uses embeddings for search.\n',
expect(withMenuOptionSpacing('Which embedding option should KTX use?\n\nKTX uses embeddings for search.')).toBe(
'Which embedding option should KTX use?\n\nKTX uses embeddings for search.\n',
);
});
@ -25,10 +25,10 @@ describe('prompt navigation helpers', () => {
it('adds a blank separator between text input helper copy and the editable value', () => {
expect(
withTextInputNavigation(
'Name this PostgreSQL connection\nKLO will use this short name in commands and config. You can rename it now.',
'Name this PostgreSQL connection\nKTX will use this short name in commands and config. You can rename it now.',
),
).toBe(
'Name this PostgreSQL connection\n\nKLO will use this short name in commands and config. You can rename it now.\nPress Escape to go back.\n',
'Name this PostgreSQL connection\n\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.\n',
);
});
@ -39,10 +39,10 @@ describe('prompt navigation helpers', () => {
it('normalizes already hinted text input prompts without duplicating the hint', () => {
expect(
withTextInputNavigation(
'Name this PostgreSQL connection\nKLO will use this short name in commands and config. You can rename it now.\nPress Escape to go back.',
'Name this PostgreSQL connection\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.',
),
).toBe(
'Name this PostgreSQL connection\n\nKLO will use this short name in commands and config. You can rename it now.\nPress Escape to go back.\n',
'Name this PostgreSQL connection\n\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.\n',
);
});
});

View file

@ -1,6 +1,6 @@
import { buildDefaultKloProjectConfig, type KloProjectConfig } from '@klo/context/project';
import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '@ktx/context/project';
import { describe, expect, it, vi } from 'vitest';
import { buildPublicIngestPlan, type KloPublicIngestProject, runKloPublicIngest } from './public-ingest.js';
import { buildPublicIngestPlan, type KtxPublicIngestProject, runKtxPublicIngest } from './public-ingest.js';
function makeIo(options: { isTTY?: boolean } = {}) {
let stdout = '';
@ -24,11 +24,11 @@ function makeIo(options: { isTTY?: boolean } = {}) {
};
}
function projectWithConnections(connections: KloProjectConfig['connections']): KloPublicIngestProject {
function projectWithConnections(connections: KtxProjectConfig['connections']): KtxPublicIngestProject {
return {
projectDir: '/tmp/project',
config: {
...buildDefaultKloProjectConfig('warehouse'),
...buildDefaultKtxProjectConfig('warehouse'),
connections,
},
};
@ -49,7 +49,7 @@ describe('buildPublicIngestPlan', () => {
connectionId: 'warehouse',
driver: 'postgres',
operation: 'scan',
debugCommand: 'klo scan warehouse --debug',
debugCommand: 'ktx scan warehouse --debug',
steps: ['scan'],
},
{
@ -57,7 +57,7 @@ describe('buildPublicIngestPlan', () => {
driver: 'notion',
operation: 'source-ingest',
adapter: 'notion',
debugCommand: 'klo dev ingest run --connection-id docs --adapter notion --debug',
debugCommand: 'ktx dev ingest run --connection-id docs --adapter notion --debug',
steps: ['source-ingest', 'memory-update'],
},
{
@ -65,7 +65,7 @@ describe('buildPublicIngestPlan', () => {
driver: 'metabase',
operation: 'source-ingest',
adapter: 'metabase',
debugCommand: 'klo dev ingest run --connection-id prod_metabase --adapter metabase --debug',
debugCommand: 'ktx dev ingest run --connection-id prod_metabase --adapter metabase --debug',
steps: ['source-ingest', 'memory-update'],
},
],
@ -76,7 +76,7 @@ describe('buildPublicIngestPlan', () => {
const project = projectWithConnections({ warehouse: { driver: 'postgres' } });
expect(() => buildPublicIngestPlan(project, { projectDir: '/tmp/project', all: false })).toThrow(
'klo ingest requires <connectionId> or --all in this release',
'ktx ingest requires <connectionId> or --all in this release',
);
});
@ -89,7 +89,7 @@ describe('buildPublicIngestPlan', () => {
});
});
describe('runKloPublicIngest', () => {
describe('runKtxPublicIngest', () => {
it('runs all independent targets and reports partial failures', async () => {
const io = makeIo();
const project = projectWithConnections({
@ -100,7 +100,7 @@ describe('runKloPublicIngest', () => {
const runIngest = vi.fn(async () => 0);
await expect(
runKloPublicIngest(
runKtxPublicIngest(
{ command: 'run', projectDir: '/tmp/project', all: true, json: false, inputMode: 'disabled' },
io.io,
{
@ -135,7 +135,7 @@ describe('runKloPublicIngest', () => {
);
expect(io.stdout()).toContain('Ingest finished with partial failures');
expect(io.stdout()).toContain('warehouse failed at scan.');
expect(io.stdout()).toContain('Debug: klo scan warehouse --debug');
expect(io.stdout()).toContain('Debug: ktx scan warehouse --debug');
});
it('can request enriched relationship scans for setup-managed context builds', async () => {
@ -144,7 +144,7 @@ describe('runKloPublicIngest', () => {
const runScan = vi.fn(async () => 0);
await expect(
runKloPublicIngest(
runKtxPublicIngest(
{
command: 'run',
projectDir: '/tmp/project',
@ -180,7 +180,7 @@ describe('runKloPublicIngest', () => {
const project = projectWithConnections({ warehouse: { driver: 'postgres' } });
await expect(
runKloPublicIngest(
runKtxPublicIngest(
{
command: 'run',
projectDir: '/tmp/project',
@ -203,15 +203,15 @@ describe('runKloPublicIngest', () => {
});
});
it('passes dbt source_dir from connection config to runKloIngest', async () => {
it('passes dbt source_dir from connection config to runKtxIngest', async () => {
const runIngest = vi.fn(async () => 0);
const io = makeIo();
await expect(
runKloPublicIngest(
runKtxPublicIngest(
{
command: 'run',
projectDir: '/tmp/klo',
projectDir: '/tmp/ktx',
targetConnectionId: 'analytics_dbt',
all: false,
json: false,
@ -221,7 +221,7 @@ describe('runKloPublicIngest', () => {
{
loadProject: async () =>
({
projectDir: '/tmp/klo',
projectDir: '/tmp/ktx',
config: {
connections: {
analytics_dbt: {
@ -253,15 +253,15 @@ describe('runKloPublicIngest', () => {
const watchIo = makeIo();
await expect(
runKloPublicIngest(
{ command: 'status', projectDir: '/tmp/klo', json: false, inputMode: 'disabled' },
runKtxPublicIngest(
{ command: 'status', projectDir: '/tmp/ktx', json: false, inputMode: 'disabled' },
statusIo.io,
{ runIngest },
),
).resolves.toBe(0);
await expect(
runKloPublicIngest(
{ command: 'watch', projectDir: '/tmp/klo', runId: 'run-1', json: false, inputMode: 'auto' },
runKtxPublicIngest(
{ command: 'watch', projectDir: '/tmp/ktx', runId: 'run-1', json: false, inputMode: 'auto' },
watchIo.io,
{ runIngest },
),
@ -271,7 +271,7 @@ describe('runKloPublicIngest', () => {
1,
{
command: 'status',
projectDir: '/tmp/klo',
projectDir: '/tmp/ktx',
outputMode: 'plain',
inputMode: 'disabled',
},
@ -281,7 +281,7 @@ describe('runKloPublicIngest', () => {
2,
{
command: 'watch',
projectDir: '/tmp/klo',
projectDir: '/tmp/ktx',
runId: 'run-1',
outputMode: 'viz',
inputMode: 'auto',

View file

@ -1,24 +1,24 @@
import { type KloLocalProject, type KloProjectConnectionConfig, loadKloProject } from '@klo/context/project';
import type { KloCliIo } from './index.js';
import type { KloIngestArgs } from './ingest.js';
import type { KloScanArgs } from './scan.js';
import { type KtxLocalProject, type KtxProjectConnectionConfig, loadKtxProject } from '@ktx/context/project';
import type { KtxCliIo } from './index.js';
import type { KtxIngestArgs } from './ingest.js';
import type { KtxScanArgs } from './scan.js';
import { profileMark } from './startup-profile.js';
profileMark('module:public-ingest');
export type KloPublicIngestStepName = 'scan' | 'source-ingest' | 'enrich' | 'memory-update';
export type KloPublicIngestStepStatus = 'done' | 'skipped' | 'failed' | 'not-run';
export type KloPublicIngestInputMode = 'auto' | 'disabled';
export type KtxPublicIngestStepName = 'scan' | 'source-ingest' | 'enrich' | 'memory-update';
export type KtxPublicIngestStepStatus = 'done' | 'skipped' | 'failed' | 'not-run';
export type KtxPublicIngestInputMode = 'auto' | 'disabled';
export type KloPublicIngestArgs =
export type KtxPublicIngestArgs =
| {
command: 'run';
projectDir: string;
targetConnectionId?: string;
all: boolean;
json: boolean;
inputMode: KloPublicIngestInputMode;
scanMode?: Extract<KloScanArgs, { command: 'run' }>['mode'];
inputMode: KtxPublicIngestInputMode;
scanMode?: Extract<KtxScanArgs, { command: 'run' }>['mode'];
detectRelationships?: boolean;
}
| {
@ -26,41 +26,41 @@ export type KloPublicIngestArgs =
projectDir: string;
runId?: string;
json: boolean;
inputMode: KloPublicIngestInputMode;
inputMode: KtxPublicIngestInputMode;
};
export interface KloPublicIngestPlanTarget {
export interface KtxPublicIngestPlanTarget {
connectionId: string;
driver: string;
operation: 'scan' | 'source-ingest';
adapter?: string;
sourceDir?: string;
debugCommand: string;
steps: KloPublicIngestStepName[];
steps: KtxPublicIngestStepName[];
}
export interface KloPublicIngestPlan {
export interface KtxPublicIngestPlan {
projectDir: string;
targets: KloPublicIngestPlanTarget[];
targets: KtxPublicIngestPlanTarget[];
}
export interface KloPublicIngestTargetResult {
export interface KtxPublicIngestTargetResult {
connectionId: string;
driver: string;
steps: Array<{
operation: KloPublicIngestStepName;
status: KloPublicIngestStepStatus;
operation: KtxPublicIngestStepName;
status: KtxPublicIngestStepStatus;
detail?: string;
debugCommand?: string;
}>;
}
export type KloPublicIngestProject = Pick<KloLocalProject, 'projectDir' | 'config'>;
export type KtxPublicIngestProject = Pick<KtxLocalProject, 'projectDir' | 'config'>;
export interface KloPublicIngestDeps {
loadProject?: (options: Parameters<typeof loadKloProject>[0]) => Promise<KloPublicIngestProject>;
runScan?: (args: KloScanArgs, io: KloCliIo) => Promise<number>;
runIngest?: (args: KloIngestArgs, io: KloCliIo) => Promise<number>;
export interface KtxPublicIngestDeps {
loadProject?: (options: Parameters<typeof loadKtxProject>[0]) => Promise<KtxPublicIngestProject>;
runScan?: (args: KtxScanArgs, io: KtxCliIo) => Promise<number>;
runIngest?: (args: KtxIngestArgs, io: KtxCliIo) => Promise<number>;
}
const sourceAdapterByDriver = new Map<string, string>([
@ -85,18 +85,18 @@ const warehouseDrivers = new Set([
'snowflake',
]);
function normalizedDriver(connection: KloProjectConnectionConfig): string {
function normalizedDriver(connection: KtxProjectConnectionConfig): string {
return String(connection.driver ?? '')
.trim()
.toLowerCase();
}
function sourceDirForConnection(connection: KloProjectConnectionConfig): string | undefined {
function sourceDirForConnection(connection: KtxProjectConnectionConfig): string | undefined {
const value = connection.source_dir ?? connection.sourceDir;
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function targetForConnection(connectionId: string, connection: KloProjectConnectionConfig): KloPublicIngestPlanTarget {
function targetForConnection(connectionId: string, connection: KtxProjectConnectionConfig): KtxPublicIngestPlanTarget {
const driver = normalizedDriver(connection);
const adapter = sourceAdapterByDriver.get(driver);
const sourceDir = sourceDirForConnection(connection);
@ -107,7 +107,7 @@ function targetForConnection(connectionId: string, connection: KloProjectConnect
operation: 'source-ingest',
adapter,
...(sourceDir ? { sourceDir } : {}),
debugCommand: `klo dev ingest run --connection-id ${connectionId} --adapter ${adapter} --debug`,
debugCommand: `ktx dev ingest run --connection-id ${connectionId} --adapter ${adapter} --debug`,
steps: ['source-ingest', 'memory-update'],
};
}
@ -117,7 +117,7 @@ function targetForConnection(connectionId: string, connection: KloProjectConnect
connectionId,
driver,
operation: 'scan',
debugCommand: `klo scan ${connectionId} --debug`,
debugCommand: `ktx scan ${connectionId} --debug`,
steps: ['scan'],
};
}
@ -126,18 +126,18 @@ function targetForConnection(connectionId: string, connection: KloProjectConnect
}
export function buildPublicIngestPlan(
project: KloPublicIngestProject,
project: KtxPublicIngestProject,
args: { projectDir: string; targetConnectionId?: string; all: boolean },
): KloPublicIngestPlan {
): KtxPublicIngestPlan {
if (!args.all && !args.targetConnectionId) {
throw new Error('klo ingest requires <connectionId> or --all in this release');
throw new Error('ktx ingest requires <connectionId> or --all in this release');
}
const entries = Object.entries(project.config.connections).sort(([a], [b]) => a.localeCompare(b));
const selected = args.all ? entries : entries.filter(([connectionId]) => connectionId === args.targetConnectionId);
if (!args.all && selected.length === 0) {
throw new Error(`Connection "${args.targetConnectionId}" is not configured in klo.yaml`);
throw new Error(`Connection "${args.targetConnectionId}" is not configured in ktx.yaml`);
}
if (selected.length === 0) {
throw new Error('No configured connections are eligible for ingest');
@ -150,7 +150,7 @@ export function buildPublicIngestPlan(
};
}
function defaultSteps(target: KloPublicIngestPlanTarget): KloPublicIngestTargetResult['steps'] {
function defaultSteps(target: KtxPublicIngestPlanTarget): KtxPublicIngestTargetResult['steps'] {
return [
{
operation: 'scan',
@ -171,7 +171,7 @@ function defaultSteps(target: KloPublicIngestPlanTarget): KloPublicIngestTargetR
];
}
function markTargetResult(target: KloPublicIngestPlanTarget, status: 'done' | 'failed'): KloPublicIngestTargetResult {
function markTargetResult(target: KtxPublicIngestPlanTarget, status: 'done' | 'failed'): KtxPublicIngestTargetResult {
const failedOperation = target.operation === 'scan' ? 'scan' : 'source-ingest';
return {
connectionId: target.connectionId,
@ -191,15 +191,15 @@ function markTargetResult(target: KloPublicIngestPlanTarget, status: 'done' | 'f
};
}
function resultFailed(result: KloPublicIngestTargetResult): boolean {
function resultFailed(result: KtxPublicIngestTargetResult): boolean {
return result.steps.some((step) => step.status === 'failed');
}
function stepStatus(result: KloPublicIngestTargetResult, operation: KloPublicIngestStepName): string {
function stepStatus(result: KtxPublicIngestTargetResult, operation: KtxPublicIngestStepName): string {
return result.steps.find((step) => step.operation === operation)?.status ?? 'not-run';
}
function renderPlainResults(results: KloPublicIngestTargetResult[], io: KloCliIo): void {
function renderPlainResults(results: KtxPublicIngestTargetResult[], io: KtxCliIo): void {
const failures = results.filter(resultFailed);
io.stdout.write(failures.length > 0 ? 'Ingest finished with partial failures\n' : 'Ingest finished\n');
io.stdout.write('\n');
@ -230,24 +230,24 @@ function renderPlainResults(results: KloPublicIngestTargetResult[], io: KloCliIo
}
}
function hasInteractiveInput(io: KloCliIo): boolean {
function hasInteractiveInput(io: KtxCliIo): boolean {
const stdin = (io as { stdin?: { isTTY?: boolean; setRawMode?: (value: boolean) => void } }).stdin;
return stdin?.isTTY === true && typeof stdin.setRawMode === 'function';
}
function sourceIngestOutputMode(args: Extract<KloPublicIngestArgs, { command: 'run' }>, io: KloCliIo): 'plain' | 'viz' {
function sourceIngestOutputMode(args: Extract<KtxPublicIngestArgs, { command: 'run' }>, io: KtxCliIo): 'plain' | 'viz' {
return args.inputMode === 'auto' && io.stdout.isTTY === true && hasInteractiveInput(io) ? 'viz' : 'plain';
}
export async function executePublicIngestTarget(
target: KloPublicIngestPlanTarget,
args: Extract<KloPublicIngestArgs, { command: 'run' }>,
io: KloCliIo,
deps: KloPublicIngestDeps,
): Promise<KloPublicIngestTargetResult> {
target: KtxPublicIngestPlanTarget,
args: Extract<KtxPublicIngestArgs, { command: 'run' }>,
io: KtxCliIo,
deps: KtxPublicIngestDeps,
): Promise<KtxPublicIngestTargetResult> {
if (target.operation === 'scan') {
const { runKloScan } = await import('./scan.js');
const exitCode = await (deps.runScan ?? runKloScan)(
const { runKtxScan } = await import('./scan.js');
const exitCode = await (deps.runScan ?? runKtxScan)(
{
command: 'run',
projectDir: args.projectDir,
@ -261,8 +261,8 @@ export async function executePublicIngestTarget(
return markTargetResult(target, exitCode === 0 ? 'done' : 'failed');
}
const { runKloIngest } = await import('./ingest.js');
const exitCode = await (deps.runIngest ?? runKloIngest)(
const { runKtxIngest } = await import('./ingest.js');
const exitCode = await (deps.runIngest ?? runKtxIngest)(
{
command: 'run',
projectDir: args.projectDir,
@ -277,14 +277,14 @@ export async function executePublicIngestTarget(
return markTargetResult(target, exitCode === 0 ? 'done' : 'failed');
}
export async function runKloPublicIngest(
args: KloPublicIngestArgs,
io: KloCliIo,
deps: KloPublicIngestDeps = {},
export async function runKtxPublicIngest(
args: KtxPublicIngestArgs,
io: KtxCliIo,
deps: KtxPublicIngestDeps = {},
): Promise<number> {
if (args.command !== 'run') {
const { runKloIngest } = await import('./ingest.js');
return await (deps.runIngest ?? runKloIngest)(
const { runKtxIngest } = await import('./ingest.js');
return await (deps.runIngest ?? runKtxIngest)(
{
command: args.command,
projectDir: args.projectDir,
@ -296,10 +296,10 @@ export async function runKloPublicIngest(
);
}
const loadProject = deps.loadProject ?? loadKloProject;
const loadProject = deps.loadProject ?? loadKtxProject;
const project = await loadProject({ projectDir: args.projectDir });
const plan = buildPublicIngestPlan(project, args);
const results: KloPublicIngestTargetResult[] = [];
const results: KtxPublicIngestTargetResult[] = [];
for (const target of plan.targets) {
results.push(await executePublicIngestTarget(target, args, io, deps));

View file

@ -1,21 +1,21 @@
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initKloProject } from '@klo/context/project';
import { initKtxProject } from '@ktx/context/project';
import type {
ApplyLocalScanRelationshipReviewDecisionsResult,
ExportLocalRelationshipFeedbackLabelsResult,
KloRelationshipFeedbackCalibrationReport,
KloRelationshipThresholdAdviceReport,
KloScanReport,
KtxRelationshipFeedbackCalibrationReport,
KtxRelationshipThresholdAdviceReport,
KtxScanReport,
LocalScanRunResult,
LocalScanStatusResponse,
ReadLocalScanRelationshipArtifactsResult,
RunLocalScanOptions,
WriteLocalScanRelationshipReviewDecisionResult,
} from '@klo/context/scan';
} from '@ktx/context/scan';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createCliScanProgress, runKloScan } from './scan.js';
import { createCliScanProgress, runKtxScan } from './scan.js';
const sqlServerExtractSchema = vi.hoisted(() =>
vi.fn(async (connectionId: string) => ({
@ -36,10 +36,10 @@ const sqlServerExtractSchema = vi.hoisted(() =>
const createSqlServerLiveDatabaseIntrospection = vi.hoisted(() =>
vi.fn(() => ({ extractSchema: sqlServerExtractSchema })),
);
const isKloSqlServerConnectionConfig = vi.hoisted(() =>
const isKtxSqlServerConnectionConfig = vi.hoisted(() =>
vi.fn((connection: { driver?: string } | undefined) => connection?.driver === 'sqlserver'),
);
const KloSqlServerScanConnector = vi.hoisted(
const KtxSqlServerScanConnector = vi.hoisted(
() =>
class {
readonly id: string;
@ -69,10 +69,10 @@ const bigQueryExtractSchema = vi.hoisted(() =>
const createBigQueryLiveDatabaseIntrospection = vi.hoisted(() =>
vi.fn(() => ({ extractSchema: bigQueryExtractSchema })),
);
const isKloBigQueryConnectionConfig = vi.hoisted(() =>
const isKtxBigQueryConnectionConfig = vi.hoisted(() =>
vi.fn((connection: { driver?: string } | undefined) => connection?.driver === 'bigquery'),
);
const KloBigQueryScanConnector = vi.hoisted(
const KtxBigQueryScanConnector = vi.hoisted(
() =>
class {
readonly id: string;
@ -102,10 +102,10 @@ const snowflakeExtractSchema = vi.hoisted(() =>
const createSnowflakeLiveDatabaseIntrospection = vi.hoisted(() =>
vi.fn(() => ({ extractSchema: snowflakeExtractSchema })),
);
const isKloSnowflakeConnectionConfig = vi.hoisted(() =>
const isKtxSnowflakeConnectionConfig = vi.hoisted(() =>
vi.fn((connection: { driver?: string } | undefined) => connection?.driver === 'snowflake'),
);
const KloSnowflakeScanConnector = vi.hoisted(
const KtxSnowflakeScanConnector = vi.hoisted(
() =>
class {
readonly id: string;
@ -127,12 +127,12 @@ const postgresExtractSchema = vi.hoisted(() =>
const createPostgresLiveDatabaseIntrospection = vi.hoisted(() =>
vi.fn(() => ({ extractSchema: postgresExtractSchema })),
);
const isKloPostgresConnectionConfig = vi.hoisted(() =>
const isKtxPostgresConnectionConfig = vi.hoisted(() =>
vi.fn((connection: { driver?: string } | undefined) =>
['postgres', 'postgresql'].includes(String(connection?.driver ?? '').toLowerCase()),
),
);
const KloPostgresScanConnector = vi.hoisted(
const KtxPostgresScanConnector = vi.hoisted(
() =>
class {
readonly id: string;
@ -144,28 +144,28 @@ const KloPostgresScanConnector = vi.hoisted(
},
);
vi.mock('@klo/connector-sqlserver', () => ({
vi.mock('@ktx/connector-sqlserver', () => ({
createSqlServerLiveDatabaseIntrospection,
isKloSqlServerConnectionConfig,
KloSqlServerScanConnector,
isKtxSqlServerConnectionConfig,
KtxSqlServerScanConnector,
}));
vi.mock('@klo/connector-bigquery', () => ({
vi.mock('@ktx/connector-bigquery', () => ({
createBigQueryLiveDatabaseIntrospection,
isKloBigQueryConnectionConfig,
KloBigQueryScanConnector,
isKtxBigQueryConnectionConfig,
KtxBigQueryScanConnector,
}));
vi.mock('@klo/connector-snowflake', () => ({
vi.mock('@ktx/connector-snowflake', () => ({
createSnowflakeLiveDatabaseIntrospection,
isKloSnowflakeConnectionConfig,
KloSnowflakeScanConnector,
isKtxSnowflakeConnectionConfig,
KtxSnowflakeScanConnector,
}));
vi.mock('@klo/connector-postgres', () => ({
vi.mock('@ktx/connector-postgres', () => ({
createPostgresLiveDatabaseIntrospection,
isKloPostgresConnectionConfig,
KloPostgresScanConnector,
isKtxPostgresConnectionConfig,
KtxPostgresScanConnector,
}));
function makeIo(options: { isTTY?: boolean } = {}) {
@ -190,7 +190,7 @@ function makeIo(options: { isTTY?: boolean } = {}) {
};
}
const report: KloScanReport = {
const report: KtxScanReport = {
connectionId: 'warehouse',
driver: 'postgres',
syncId: 'sync-1',
@ -242,7 +242,7 @@ const report: KloScanReport = {
createdAt: '2026-04-29T09:00:00.000Z',
};
const reportWithAttention: KloScanReport = {
const reportWithAttention: KtxScanReport = {
...report,
mode: 'relationships',
diffSummary: {
@ -258,7 +258,7 @@ const reportWithAttention: KloScanReport = {
warnings: [
{
code: 'connector_capability_missing',
message: 'KLO scan connector is missing optional capability: columnStats',
message: 'KTX scan connector is missing optional capability: columnStats',
recoverable: true,
metadata: { capability: 'columnStats' },
},
@ -283,11 +283,11 @@ const reportWithAttention: KloScanReport = {
},
};
describe('runKloScan', () => {
describe('runKtxScan', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-scan-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-scan-'));
});
afterEach(async () => {
@ -295,7 +295,7 @@ describe('runKloScan', () => {
});
it('runs structural scans and prints a dev-friendly plain summary', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const runLocalScan = vi.fn(
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
runId: 'scan-run-1',
@ -311,7 +311,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempDir,
@ -334,7 +334,7 @@ describe('runKloScan', () => {
connector: undefined,
}),
);
expect(io.stdout()).toContain('KLO scan completed\n');
expect(io.stdout()).toContain('KTX scan completed\n');
expect(io.stdout()).toContain('Run: scan-run-1');
expect(io.stdout()).toContain('Mode: structural');
expect(io.stdout()).toContain('What changed\n');
@ -346,9 +346,9 @@ describe('runKloScan', () => {
expect(io.stdout()).toContain('Artifacts\n');
expect(io.stdout()).toContain('Report: raw-sources/warehouse/live-database/sync-1/scan-report.json');
expect(io.stdout()).toContain('Next:\n');
expect(io.stdout()).toContain('klo dev scan status --project-dir ');
expect(io.stdout()).toContain('ktx dev scan status --project-dir ');
expect(io.stdout()).toContain(' scan-run-1\n');
expect(io.stdout()).toContain('klo dev scan report --project-dir ');
expect(io.stdout()).toContain('ktx dev scan report --project-dir ');
expect(io.stdout()).toContain(' scan-run-1\n');
expect(io.stdout()).not.toContain('\u001b[');
expect(io.stdout()).not.toContain('✓');
@ -357,7 +357,7 @@ describe('runKloScan', () => {
});
it('explains warnings, capability gaps, and relationships in human scan summaries', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const runLocalScan = vi.fn(
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
runId: 'scan-run-1',
@ -373,7 +373,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempDir,
@ -408,14 +408,14 @@ describe('runKloScan', () => {
});
it('prints review-only relationship summaries and validation capability warnings', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
const reviewOnlyReport: KloScanReport = {
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const reviewOnlyReport: KtxScanReport = {
...reportWithAttention,
capabilityGaps: [],
warnings: [
{
code: 'connector_capability_missing',
message: 'KLO scan connector cannot run read-only SQL relationship validation',
message: 'KTX scan connector cannot run read-only SQL relationship validation',
recoverable: true,
metadata: { capability: 'readOnlySql' },
},
@ -437,7 +437,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempDir,
@ -456,12 +456,12 @@ describe('runKloScan', () => {
expect(io.stdout()).toContain('Review: 12');
expect(io.stdout()).toContain('Rejected: 44');
expect(io.stdout()).toContain(
'connector_capability_missing: KLO scan connector cannot run read-only SQL relationship validation',
'connector_capability_missing: KTX scan connector cannot run read-only SQL relationship validation',
);
});
it('passes a scan progress port and prints TTY progress messages', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const runLocalScan = vi.fn(async (input: RunLocalScanOptions): Promise<LocalScanRunResult> => {
await input.progress?.update(0.15, 'Inspecting database schema');
await input.progress?.update(0.55, 'Semantic layer comparison found 5 changes across 18 tables');
@ -481,7 +481,7 @@ describe('runKloScan', () => {
delete process.env.CI;
try {
const exitCode = await runKloScan(
const exitCode = await runKtxScan(
{
command: 'run',
projectDir: tempDir,
@ -531,7 +531,7 @@ describe('runKloScan', () => {
});
it('flushes transient TTY progress messages before printing scan failures', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const runLocalScan = vi.fn(async (input: RunLocalScanOptions): Promise<LocalScanRunResult> => {
await input.progress?.update(0.42, 'Generating descriptions 3/35 tables', { transient: true });
throw new Error('scan failed');
@ -542,7 +542,7 @@ describe('runKloScan', () => {
try {
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempDir,
@ -568,7 +568,7 @@ describe('runKloScan', () => {
});
it('does not print live progress messages for non-TTY output', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const runLocalScan = vi.fn(async (input: RunLocalScanOptions): Promise<LocalScanRunResult> => {
await input.progress?.update(0.15, 'Inspecting database schema');
return {
@ -585,7 +585,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempDir,
@ -604,7 +604,7 @@ describe('runKloScan', () => {
});
it('uses terminal-aware visual styling only for TTY output', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const runLocalScan = vi.fn(
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
runId: 'scan-run-1',
@ -627,7 +627,7 @@ describe('runKloScan', () => {
try {
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempDir,
@ -659,12 +659,12 @@ describe('runKloScan', () => {
}
expect(io.stdout()).toContain('✓');
expect(io.stdout()).toContain('KLO scan completed');
expect(io.stdout()).toContain('KTX scan completed');
expect(io.stdout()).toContain('\u001b[');
});
it('honors NO_COLOR for TTY scan summaries', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const runLocalScan = vi.fn(
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
runId: 'scan-run-1',
@ -683,7 +683,7 @@ describe('runKloScan', () => {
try {
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempDir,
@ -704,12 +704,12 @@ describe('runKloScan', () => {
}
}
expect(io.stdout()).toContain('KLO scan completed');
expect(io.stdout()).toContain('KTX scan completed');
expect(io.stdout()).not.toContain('\u001b[');
});
it('prints status and human report output by default', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const status: LocalScanStatusResponse = {
runId: 'scan-run-1',
status: 'done',
@ -727,7 +727,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan({ command: 'status', projectDir: tempDir, runId: 'scan-run-1' }, io.io, {
runKtxScan({ command: 'status', projectDir: tempDir, runId: 'scan-run-1' }, io.io, {
getLocalScanStatus: vi.fn().mockResolvedValue(status),
}),
).resolves.toBe(0);
@ -736,22 +736,22 @@ describe('runKloScan', () => {
const reportIo = makeIo();
await expect(
runKloScan({ command: 'report', projectDir: tempDir, runId: 'scan-run-1', json: false }, reportIo.io, {
runKtxScan({ command: 'report', projectDir: tempDir, runId: 'scan-run-1', json: false }, reportIo.io, {
getLocalScanReport: vi.fn().mockResolvedValue(report),
}),
).resolves.toBe(0);
expect(reportIo.stdout()).toContain('KLO scan report\n');
expect(reportIo.stdout()).toContain('KTX scan report\n');
expect(reportIo.stdout()).toContain('Run: scan-run-1');
expect(reportIo.stdout()).toContain('What changed\n');
expect(() => JSON.parse(reportIo.stdout())).toThrow();
});
it('prints raw report JSON when requested', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const reportIo = makeIo();
await expect(
runKloScan({ command: 'report', projectDir: tempDir, runId: 'scan-run-1', json: true }, reportIo.io, {
runKtxScan({ command: 'report', projectDir: tempDir, runId: 'scan-run-1', json: true }, reportIo.io, {
getLocalScanReport: vi.fn().mockResolvedValue(report),
}),
).resolves.toBe(0);
@ -760,8 +760,8 @@ describe('runKloScan', () => {
});
it('prints review relationship artifacts in human form', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
const reviewReport: KloScanReport = {
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const reviewReport: KtxScanReport = {
...reportWithAttention,
runId: 'scan-run-review',
syncId: 'sync-review',
@ -866,7 +866,7 @@ describe('runKloScan', () => {
tables: [],
columns: {},
queryCount: 0,
warnings: ['KLO scan connector cannot run read-only SQL relationship validation'],
warnings: ['KTX scan connector cannot run read-only SQL relationship validation'],
},
paths: {
relationships: 'raw-sources/warehouse/live-database/sync-review/enrichment/relationships.json',
@ -878,7 +878,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationships',
projectDir: tempDir,
@ -897,7 +897,7 @@ describe('runKloScan', () => {
'scan-run-review',
);
expect(io.stdout()).toContain('KLO relationship artifacts');
expect(io.stdout()).toContain('KTX relationship artifacts');
expect(io.stdout()).toContain('Run: scan-run-review');
expect(io.stdout()).toContain('Summary: accepted=0 review=1 rejected=1 skipped=0');
expect(io.stdout()).toContain('Reason: relationship candidates require review before manifest writes');
@ -911,8 +911,8 @@ describe('runKloScan', () => {
});
it('prints filtered relationship artifacts as JSON', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
const jsonReport: KloScanReport = {
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const jsonReport: KtxScanReport = {
...reportWithAttention,
runId: 'scan-run-json',
syncId: 'sync-json',
@ -946,7 +946,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationships',
projectDir: tempDir,
@ -974,7 +974,7 @@ describe('runKloScan', () => {
});
it('records an accepted relationship review decision in human form', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const decisionResult: WriteLocalScanRelationshipReviewDecisionResult = {
path: 'raw-sources/warehouse/live-database/sync-review/enrichment/relationship-review-decisions.json',
decision: {
@ -1019,7 +1019,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipDecision',
projectDir: tempDir,
@ -1055,7 +1055,7 @@ describe('runKloScan', () => {
});
it('records a rejected relationship review decision as JSON', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const decisionResult: WriteLocalScanRelationshipReviewDecisionResult = {
path: 'raw-sources/warehouse/live-database/sync-review/enrichment/relationship-review-decisions.json',
decision: {
@ -1100,14 +1100,14 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipDecision',
projectDir: tempDir,
runId: 'scan-run-review',
candidateId: 'orders:orders.customer_id->customers:customers.id',
decision: 'rejected',
reviewer: 'klo',
reviewer: 'ktx',
note: null,
json: true,
},
@ -1127,19 +1127,19 @@ describe('runKloScan', () => {
});
it('reports missing scan runs when recording relationship decisions', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const writeLocalScanRelationshipReviewDecision = vi.fn(async () => null);
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipDecision',
projectDir: tempDir,
runId: 'missing-run',
candidateId: 'orders:orders.customer_id->customers:customers.id',
decision: 'accepted',
reviewer: 'klo',
reviewer: 'ktx',
note: null,
json: false,
},
@ -1152,7 +1152,7 @@ describe('runKloScan', () => {
});
it('applies accepted relationship review decisions with human output', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const applyLocalScanRelationshipReviewDecisions = vi.fn(
async (): Promise<ApplyLocalScanRelationshipReviewDecisionsResult> => ({
runId: 'scan-run-a',
@ -1190,7 +1190,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipApply',
projectDir: tempDir,
@ -1222,7 +1222,7 @@ describe('runKloScan', () => {
});
it('prints relationship review apply JSON', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const applyResult: ApplyLocalScanRelationshipReviewDecisionsResult = {
runId: 'scan-run-a',
connectionId: 'warehouse',
@ -1239,7 +1239,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipApply',
projectDir: tempDir,
@ -1267,7 +1267,7 @@ describe('runKloScan', () => {
});
it('prints relationship feedback export summary in human form', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const feedback: ExportLocalRelationshipFeedbackLabelsResult = {
generatedAt: '2026-05-07T13:00:00.000Z',
filters: { connectionId: null, decision: 'all' },
@ -1328,7 +1328,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipFeedback',
projectDir: tempDir,
@ -1349,7 +1349,7 @@ describe('runKloScan', () => {
decision: 'all',
},
);
expect(io.stdout()).toContain('KLO relationship feedback labels');
expect(io.stdout()).toContain('KTX relationship feedback labels');
expect(io.stdout()).toContain('Total: 2');
expect(io.stdout()).toContain('Accepted: 1');
expect(io.stdout()).toContain('Rejected: 1');
@ -1358,7 +1358,7 @@ describe('runKloScan', () => {
});
it('prints relationship feedback labels as JSONL', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const feedback: ExportLocalRelationshipFeedbackLabelsResult = {
generatedAt: '2026-05-07T13:00:00.000Z',
filters: { connectionId: 'warehouse', decision: 'accepted' },
@ -1373,7 +1373,7 @@ describe('runKloScan', () => {
runId: 'scan-run-review',
syncId: 'sync-review',
decidedAt: '2026-05-07T12:00:00.000Z',
reviewer: 'klo',
reviewer: 'ktx',
note: null,
relationshipType: 'many_to_one',
source: 'deterministic_name',
@ -1392,13 +1392,13 @@ describe('runKloScan', () => {
warnings: [],
};
const exportLocalRelationshipFeedbackLabels = vi.fn(async () => feedback);
const formatKloRelationshipFeedbackLabelsJsonl = vi.fn(
const formatKtxRelationshipFeedbackLabelsJsonl = vi.fn(
() => '{"candidateId":"orders:orders.customer_id->customers:customers.id"}\n',
);
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipFeedback',
projectDir: tempDir,
@ -1408,7 +1408,7 @@ describe('runKloScan', () => {
jsonl: true,
},
io.io,
{ exportLocalRelationshipFeedbackLabels, formatKloRelationshipFeedbackLabelsJsonl },
{ exportLocalRelationshipFeedbackLabels, formatKtxRelationshipFeedbackLabelsJsonl },
),
).resolves.toBe(0);
@ -1419,12 +1419,12 @@ describe('runKloScan', () => {
decision: 'accepted',
},
);
expect(formatKloRelationshipFeedbackLabelsJsonl).toHaveBeenCalledWith(feedback);
expect(formatKtxRelationshipFeedbackLabelsJsonl).toHaveBeenCalledWith(feedback);
expect(JSON.parse(io.stdout())).toEqual({ candidateId: 'orders:orders.customer_id->customers:customers.id' });
});
it('prints relationship feedback export as JSON', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const feedback: ExportLocalRelationshipFeedbackLabelsResult = {
generatedAt: '2026-05-07T13:00:00.000Z',
filters: { connectionId: null, decision: 'rejected' },
@ -1436,7 +1436,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipFeedback',
projectDir: tempDir,
@ -1458,8 +1458,8 @@ describe('runKloScan', () => {
});
it('prints relationship feedback calibration as human output', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
const calibration: KloRelationshipFeedbackCalibrationReport = {
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const calibration: KtxRelationshipFeedbackCalibrationReport = {
generatedAt: '2026-05-07T13:00:00.000Z',
filters: { connectionId: null, decision: 'all' },
thresholds: { accept: 0.85, review: 0.55 },
@ -1520,13 +1520,13 @@ describe('runKloScan', () => {
warnings: [],
};
const calibrateLocalRelationshipFeedbackLabels = vi.fn(async () => calibration);
const formatKloRelationshipFeedbackCalibrationMarkdown = vi.fn(
() => 'KLO relationship feedback calibration\nTotal labels: 2\n',
const formatKtxRelationshipFeedbackCalibrationMarkdown = vi.fn(
() => 'KTX relationship feedback calibration\nTotal labels: 2\n',
);
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipCalibration',
projectDir: tempDir,
@ -1537,7 +1537,7 @@ describe('runKloScan', () => {
json: false,
},
io.io,
{ calibrateLocalRelationshipFeedbackLabels, formatKloRelationshipFeedbackCalibrationMarkdown },
{ calibrateLocalRelationshipFeedbackLabels, formatKtxRelationshipFeedbackCalibrationMarkdown },
),
).resolves.toBe(0);
@ -1550,13 +1550,13 @@ describe('runKloScan', () => {
reviewThreshold: 0.55,
},
);
expect(formatKloRelationshipFeedbackCalibrationMarkdown).toHaveBeenCalledWith(calibration);
expect(io.stdout()).toBe('KLO relationship feedback calibration\nTotal labels: 2\n');
expect(formatKtxRelationshipFeedbackCalibrationMarkdown).toHaveBeenCalledWith(calibration);
expect(io.stdout()).toBe('KTX relationship feedback calibration\nTotal labels: 2\n');
});
it('prints relationship feedback calibration as JSON', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
const calibration: KloRelationshipFeedbackCalibrationReport = {
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const calibration: KtxRelationshipFeedbackCalibrationReport = {
generatedAt: '2026-05-07T13:00:00.000Z',
filters: { connectionId: 'warehouse', decision: 'rejected' },
thresholds: { accept: 0.9, review: 0.5 },
@ -1583,7 +1583,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipCalibration',
projectDir: tempDir,
@ -1606,8 +1606,8 @@ describe('runKloScan', () => {
});
it('prints relationship threshold advice as human output', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
const advice: KloRelationshipThresholdAdviceReport = {
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const advice: KtxRelationshipThresholdAdviceReport = {
generatedAt: '2026-05-07T14:00:00.000Z',
filters: { connectionId: null, decision: 'all' },
status: 'ready',
@ -1648,13 +1648,13 @@ describe('runKloScan', () => {
warnings: [],
};
const adviseLocalRelationshipFeedbackThresholds = vi.fn(async () => advice);
const formatKloRelationshipThresholdAdviceMarkdown = vi.fn(
() => 'KLO relationship threshold advice\nRecommended: accept=0.90 review=0.55\n',
const formatKtxRelationshipThresholdAdviceMarkdown = vi.fn(
() => 'KTX relationship threshold advice\nRecommended: accept=0.90 review=0.55\n',
);
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipThresholds',
projectDir: tempDir,
@ -1665,7 +1665,7 @@ describe('runKloScan', () => {
json: false,
},
io.io,
{ adviseLocalRelationshipFeedbackThresholds, formatKloRelationshipThresholdAdviceMarkdown },
{ adviseLocalRelationshipFeedbackThresholds, formatKtxRelationshipThresholdAdviceMarkdown },
),
).resolves.toBe(0);
@ -1678,13 +1678,13 @@ describe('runKloScan', () => {
minRejectedLabels: 2,
},
);
expect(formatKloRelationshipThresholdAdviceMarkdown).toHaveBeenCalledWith(advice);
expect(io.stdout()).toBe('KLO relationship threshold advice\nRecommended: accept=0.90 review=0.55\n');
expect(formatKtxRelationshipThresholdAdviceMarkdown).toHaveBeenCalledWith(advice);
expect(io.stdout()).toBe('KTX relationship threshold advice\nRecommended: accept=0.90 review=0.55\n');
});
it('prints relationship threshold advice as JSON', async () => {
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
const advice: KloRelationshipThresholdAdviceReport = {
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const advice: KtxRelationshipThresholdAdviceReport = {
generatedAt: '2026-05-07T14:00:00.000Z',
filters: { connectionId: 'warehouse', decision: 'all' },
status: 'insufficient_labels',
@ -1714,7 +1714,7 @@ describe('runKloScan', () => {
const io = makeIo();
await expect(
runKloScan(
runKtxScan(
{
command: 'relationshipThresholds',
projectDir: tempDir,
@ -1737,10 +1737,10 @@ describe('runKloScan', () => {
});
it('passes native CLI adapters into local scan runs for mysql configs', async () => {
const tempProject = await mkdtemp(join(tmpdir(), 'klo-scan-cli-native-'));
await initKloProject({ projectDir: tempProject, projectName: 'warehouse' });
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-'));
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
await writeFile(
join(tempProject, 'klo.yaml'),
join(tempProject, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -1767,7 +1767,7 @@ describe('runKloScan', () => {
);
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempProject,
@ -1786,10 +1786,10 @@ describe('runKloScan', () => {
});
it('creates a native connector for standalone relationship scans', async () => {
const tempProject = await mkdtemp(join(tmpdir(), 'klo-scan-cli-relationships-'));
await initKloProject({ projectDir: tempProject, projectName: 'warehouse' });
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-relationships-'));
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
await writeFile(
join(tempProject, 'klo.yaml'),
join(tempProject, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -1816,7 +1816,7 @@ describe('runKloScan', () => {
);
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempProject,
@ -1841,10 +1841,10 @@ describe('runKloScan', () => {
});
it('routes standalone postgres scans through the native connector before daemon fallback', async () => {
const tempProject = await mkdtemp(join(tmpdir(), 'klo-scan-cli-native-postgres-'));
await initKloProject({ projectDir: tempProject, projectName: 'warehouse' });
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-postgres-'));
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
await writeFile(
join(tempProject, 'klo.yaml'),
join(tempProject, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -1874,7 +1874,7 @@ describe('runKloScan', () => {
);
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempProject,
@ -1905,10 +1905,10 @@ describe('runKloScan', () => {
});
it('passes native CLI adapters into local scan runs for clickhouse configs', async () => {
const tempProject = await mkdtemp(join(tmpdir(), 'klo-scan-cli-native-clickhouse-'));
await initKloProject({ projectDir: tempProject, projectName: 'warehouse' });
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-clickhouse-'));
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
await writeFile(
join(tempProject, 'klo.yaml'),
join(tempProject, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -1938,7 +1938,7 @@ describe('runKloScan', () => {
);
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempProject,
@ -1957,10 +1957,10 @@ describe('runKloScan', () => {
});
it('passes native CLI adapters into local scan runs for sqlserver configs', async () => {
const tempProject = await mkdtemp(join(tmpdir(), 'klo-scan-cli-native-sqlserver-'));
await initKloProject({ projectDir: tempProject, projectName: 'warehouse' });
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-sqlserver-'));
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
await writeFile(
join(tempProject, 'klo.yaml'),
join(tempProject, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -1990,7 +1990,7 @@ describe('runKloScan', () => {
);
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempProject,
@ -2021,10 +2021,10 @@ describe('runKloScan', () => {
});
it('passes native CLI adapters into local scan runs for bigquery configs', async () => {
const tempProject = await mkdtemp(join(tmpdir(), 'klo-scan-cli-native-bigquery-'));
await initKloProject({ projectDir: tempProject, projectName: 'warehouse' });
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-bigquery-'));
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
await writeFile(
join(tempProject, 'klo.yaml'),
join(tempProject, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -2053,7 +2053,7 @@ describe('runKloScan', () => {
);
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempProject,
@ -2084,10 +2084,10 @@ describe('runKloScan', () => {
});
it('passes native CLI adapters into local scan runs for snowflake configs', async () => {
const tempProject = await mkdtemp(join(tmpdir(), 'klo-scan-cli-native-snowflake-'));
await initKloProject({ projectDir: tempProject, projectName: 'warehouse' });
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-snowflake-'));
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
await writeFile(
join(tempProject, 'klo.yaml'),
join(tempProject, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
@ -2119,7 +2119,7 @@ describe('runKloScan', () => {
);
await expect(
runKloScan(
runKtxScan(
{
command: 'run',
projectDir: tempProject,

View file

@ -1,4 +1,4 @@
import { loadKloProject } from '@klo/context/project';
import { loadKtxProject } from '@ktx/context/project';
import {
type ApplyLocalScanRelationshipReviewDecisionsResult,
adviseLocalRelationshipFeedbackThresholds,
@ -6,43 +6,43 @@ import {
calibrateLocalRelationshipFeedbackLabels,
type ExportLocalRelationshipFeedbackLabelsResult,
exportLocalRelationshipFeedbackLabels,
formatKloRelationshipFeedbackCalibrationMarkdown,
formatKloRelationshipFeedbackLabelsJsonl,
formatKloRelationshipThresholdAdviceMarkdown,
formatKtxRelationshipFeedbackCalibrationMarkdown,
formatKtxRelationshipFeedbackLabelsJsonl,
formatKtxRelationshipThresholdAdviceMarkdown,
getLocalScanReport,
getLocalScanStatus,
type KloProgressPort,
type KloRelationshipArtifact,
type KloRelationshipArtifactEdge,
type KloRelationshipArtifactStatus,
type KloRelationshipDiagnosticsArtifact,
type KloRelationshipFeedbackCalibrationReport,
type KloRelationshipFeedbackDecisionFilter,
type KloRelationshipFeedbackLabel,
type KloRelationshipReviewDecisionValue,
type KloRelationshipThresholdAdviceReport,
type KloScanMode,
type KloScanReport,
type KloScanWarning,
type KtxProgressPort,
type KtxRelationshipArtifact,
type KtxRelationshipArtifactEdge,
type KtxRelationshipArtifactStatus,
type KtxRelationshipDiagnosticsArtifact,
type KtxRelationshipFeedbackCalibrationReport,
type KtxRelationshipFeedbackDecisionFilter,
type KtxRelationshipFeedbackLabel,
type KtxRelationshipReviewDecisionValue,
type KtxRelationshipThresholdAdviceReport,
type KtxScanMode,
type KtxScanReport,
type KtxScanWarning,
type LocalScanStatusResponse,
readLocalScanRelationshipArtifacts,
runLocalScan,
type WriteLocalScanRelationshipReviewDecisionResult,
writeLocalScanRelationshipReviewDecision,
} from '@klo/context/scan';
import type { KloCliIo } from './index.js';
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
import { createKloCliScanConnector } from './local-scan-connectors.js';
} from '@ktx/context/scan';
import type { KtxCliIo } from './index.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js';
import { profileMark } from './startup-profile.js';
profileMark('module:scan');
export type KloScanArgs =
export type KtxScanArgs =
| {
command: 'run';
projectDir: string;
connectionId: string;
mode: KloScanMode;
mode: KtxScanMode;
detectRelationships: boolean;
dryRun: boolean;
databaseIntrospectionUrl?: string;
@ -53,7 +53,7 @@ export type KloScanArgs =
command: 'relationships';
projectDir: string;
runId: string;
status: KloRelationshipArtifactStatus;
status: KtxRelationshipArtifactStatus;
json: boolean;
limit: number;
}
@ -62,7 +62,7 @@ export type KloScanArgs =
projectDir: string;
runId: string;
candidateId: string;
decision: KloRelationshipReviewDecisionValue;
decision: KtxRelationshipReviewDecisionValue;
reviewer: string;
note: string | null;
json: boolean;
@ -80,7 +80,7 @@ export type KloScanArgs =
command: 'relationshipFeedback';
projectDir: string;
connectionId: string | null;
decision: KloRelationshipFeedbackDecisionFilter;
decision: KtxRelationshipFeedbackDecisionFilter;
json: boolean;
jsonl: boolean;
}
@ -88,7 +88,7 @@ export type KloScanArgs =
command: 'relationshipCalibration';
projectDir: string;
connectionId: string | null;
decision: KloRelationshipFeedbackDecisionFilter;
decision: KtxRelationshipFeedbackDecisionFilter;
acceptThreshold: number;
reviewThreshold: number;
json: boolean;
@ -103,23 +103,23 @@ export type KloScanArgs =
json: boolean;
};
interface KloScanDeps {
interface KtxScanDeps {
runLocalScan?: typeof runLocalScan;
createLocalIngestAdapters?: typeof createKloCliLocalIngestAdapters;
createLocalIngestAdapters?: typeof createKtxCliLocalIngestAdapters;
getLocalScanStatus?: typeof getLocalScanStatus;
getLocalScanReport?: typeof getLocalScanReport;
readLocalScanRelationshipArtifacts?: typeof readLocalScanRelationshipArtifacts;
writeLocalScanRelationshipReviewDecision?: typeof writeLocalScanRelationshipReviewDecision;
applyLocalScanRelationshipReviewDecisions?: typeof applyLocalScanRelationshipReviewDecisions;
exportLocalRelationshipFeedbackLabels?: typeof exportLocalRelationshipFeedbackLabels;
formatKloRelationshipFeedbackLabelsJsonl?: typeof formatKloRelationshipFeedbackLabelsJsonl;
formatKtxRelationshipFeedbackLabelsJsonl?: typeof formatKtxRelationshipFeedbackLabelsJsonl;
calibrateLocalRelationshipFeedbackLabels?: typeof calibrateLocalRelationshipFeedbackLabels;
formatKloRelationshipFeedbackCalibrationMarkdown?: typeof formatKloRelationshipFeedbackCalibrationMarkdown;
formatKtxRelationshipFeedbackCalibrationMarkdown?: typeof formatKtxRelationshipFeedbackCalibrationMarkdown;
adviseLocalRelationshipFeedbackThresholds?: typeof adviseLocalRelationshipFeedbackThresholds;
formatKloRelationshipThresholdAdviceMarkdown?: typeof formatKloRelationshipThresholdAdviceMarkdown;
formatKtxRelationshipThresholdAdviceMarkdown?: typeof formatKtxRelationshipThresholdAdviceMarkdown;
}
function shouldUseStyledOutput(io: KloCliIo): boolean {
function shouldUseStyledOutput(io: KtxCliIo): boolean {
return io.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== 'dumb' && !process.env.CI;
}
@ -142,15 +142,15 @@ function plural(count: number, singular: string, pluralValue = `${singular}s`):
return count === 1 ? singular : pluralValue;
}
function tableChangeCount(report: KloScanReport): number {
function tableChangeCount(report: KtxScanReport): number {
return report.diffSummary.tablesAdded + report.diffSummary.tablesModified + report.diffSummary.tablesDeleted;
}
function totalTableCount(report: KloScanReport): number {
function totalTableCount(report: KtxScanReport): number {
return tableChangeCount(report) + report.diffSummary.tablesUnchanged;
}
function writeScanIdentity(report: KloScanReport, io: KloCliIo): void {
function writeScanIdentity(report: KtxScanReport, io: KtxCliIo): void {
io.stdout.write(`Run: ${report.runId}\n`);
io.stdout.write(`Connection: ${report.connectionId}\n`);
io.stdout.write(`Mode: ${report.mode}\n`);
@ -158,7 +158,7 @@ function writeScanIdentity(report: KloScanReport, io: KloCliIo): void {
io.stdout.write(`Dry run: ${report.dryRun ? 'yes' : 'no'}\n`);
}
function writeWhatChanged(report: KloScanReport, io: KloCliIo): void {
function writeWhatChanged(report: KtxScanReport, io: KtxCliIo): void {
const changedTables = tableChangeCount(report);
const totalTables = totalTableCount(report);
io.stdout.write('\nWhat changed\n');
@ -182,7 +182,7 @@ function writeWhatChanged(report: KloScanReport, io: KloCliIo): void {
}
}
function hasRelationshipResults(report: KloScanReport): boolean {
function hasRelationshipResults(report: KtxScanReport): boolean {
return (
report.relationships.accepted > 0 ||
report.relationships.review > 0 ||
@ -191,7 +191,7 @@ function hasRelationshipResults(report: KloScanReport): boolean {
);
}
function writeRelationships(report: KloScanReport, io: KloCliIo): void {
function writeRelationships(report: KtxScanReport, io: KtxCliIo): void {
if (!hasRelationshipResults(report)) {
return;
}
@ -215,12 +215,12 @@ function capabilityGapMessage(gap: string): string {
return `${gap} is unavailable; scan results may be less complete.`;
}
function warningLine(warning: KloScanWarning): string {
function warningLine(warning: KtxScanWarning): string {
const location = warning.table ? `${warning.table}${warning.column ? `.${warning.column}` : ''}: ` : '';
return `${warning.code}: ${location}${warning.message}`;
}
function writeNeedsAttention(report: KloScanReport, io: KloCliIo): void {
function writeNeedsAttention(report: KtxScanReport, io: KtxCliIo): void {
io.stdout.write('\nNeeds attention\n');
if (report.warnings.length === 0 && report.capabilityGaps.length === 0) {
io.stdout.write(' None\n');
@ -243,7 +243,7 @@ function writeNeedsAttention(report: KloScanReport, io: KloCliIo): void {
}
}
function writeArtifacts(report: KloScanReport, io: KloCliIo): void {
function writeArtifacts(report: KtxScanReport, io: KtxCliIo): void {
io.stdout.write('\nArtifacts\n');
io.stdout.write(` Report: ${report.artifactPaths.reportPath ?? 'none'}\n`);
io.stdout.write(` Raw sources: ${report.artifactPaths.rawSourcesDir ?? 'none'}\n`);
@ -255,7 +255,7 @@ function writeArtifacts(report: KloScanReport, io: KloCliIo): void {
}
}
function writeHumanReportBody(report: KloScanReport, io: KloCliIo): void {
function writeHumanReportBody(report: KtxScanReport, io: KtxCliIo): void {
writeScanIdentity(report, io);
writeWhatChanged(report, io);
writeRelationships(report, io);
@ -263,25 +263,25 @@ function writeHumanReportBody(report: KloScanReport, io: KloCliIo): void {
writeArtifacts(report, io);
}
function writeRunSummary(report: KloScanReport, projectDir: string, io: KloCliIo): void {
function writeRunSummary(report: KtxScanReport, projectDir: string, io: KtxCliIo): void {
const styled = shouldUseStyledOutput(io);
io.stdout.write(`${styled ? green('✓') : ''}${styled ? ' ' : ''}KLO scan completed\n`);
io.stdout.write(`${styled ? green('✓') : ''}${styled ? ' ' : ''}KTX scan completed\n`);
io.stdout.write('Status: done\n');
writeHumanReportBody(report, io);
const projectDirArg = quoteCliArg(projectDir);
io.stdout.write('\nNext:\n');
const statusCommand = styled ? dim('klo dev scan status') : 'klo dev scan status';
const reportCommand = styled ? dim('klo dev scan report') : 'klo dev scan report';
const statusCommand = styled ? dim('ktx dev scan status') : 'ktx dev scan status';
const reportCommand = styled ? dim('ktx dev scan report') : 'ktx dev scan report';
io.stdout.write(` ${statusCommand} --project-dir ${projectDirArg} ${report.runId}\n`);
io.stdout.write(` ${reportCommand} --project-dir ${projectDirArg} ${report.runId}\n`);
}
function writeReport(report: KloScanReport, io: KloCliIo): void {
io.stdout.write('KLO scan report\n');
function writeReport(report: KtxScanReport, io: KtxCliIo): void {
io.stdout.write('KTX scan report\n');
writeHumanReportBody(report, io);
}
function formatRelationshipEndpoint(edge: KloRelationshipArtifactEdge, side: 'from' | 'to'): string {
function formatRelationshipEndpoint(edge: KtxRelationshipArtifactEdge, side: 'from' | 'to'): string {
const endpoint = edge[side];
if (endpoint.columns.length === 1) {
return `${endpoint.table.name}.${endpoint.columns[0]}`;
@ -293,7 +293,7 @@ function formatRelationshipScore(value: number | null): string {
return value === null ? 'n/a' : value.toFixed(2);
}
function relationshipStatusTitle(status: Exclude<KloRelationshipArtifactStatus, 'all'>): string {
function relationshipStatusTitle(status: Exclude<KtxRelationshipArtifactStatus, 'all'>): string {
if (status === 'accepted') {
return 'Accepted relationships';
}
@ -307,9 +307,9 @@ function relationshipStatusTitle(status: Exclude<KloRelationshipArtifactStatus,
}
function filteredRelationshipArtifact(
relationships: KloRelationshipArtifact,
status: KloRelationshipArtifactStatus,
): KloRelationshipArtifact {
relationships: KtxRelationshipArtifact,
status: KtxRelationshipArtifactStatus,
): KtxRelationshipArtifact {
if (status === 'all') {
return relationships;
}
@ -322,7 +322,7 @@ function filteredRelationshipArtifact(
};
}
function writeRelationshipEdge(edge: KloRelationshipArtifactEdge, index: number, io: KloCliIo): void {
function writeRelationshipEdge(edge: KtxRelationshipArtifactEdge, index: number, io: KtxCliIo): void {
io.stdout.write(
` ${index + 1}. ${formatRelationshipEndpoint(edge, 'from')} -> ${formatRelationshipEndpoint(edge, 'to')}\n`,
);
@ -333,10 +333,10 @@ function writeRelationshipEdge(edge: KloRelationshipArtifactEdge, index: number,
}
function writeRelationshipGroup(
status: Exclude<KloRelationshipArtifactStatus, 'all'>,
relationships: KloRelationshipArtifact,
status: Exclude<KtxRelationshipArtifactStatus, 'all'>,
relationships: KtxRelationshipArtifact,
limit: number,
io: KloCliIo,
io: KtxCliIo,
): void {
if (status === 'skipped') {
io.stdout.write(`\n${relationshipStatusTitle(status)} (${relationships.skipped.length})\n`);
@ -366,15 +366,15 @@ function writeRelationshipArtifactSummary(input: {
runId: string;
connectionId: string;
syncId: string;
status: KloRelationshipArtifactStatus;
status: KtxRelationshipArtifactStatus;
limit: number;
summary: KloRelationshipArtifact;
relationships: KloRelationshipArtifact;
diagnostics: KloRelationshipDiagnosticsArtifact | null;
summary: KtxRelationshipArtifact;
relationships: KtxRelationshipArtifact;
diagnostics: KtxRelationshipDiagnosticsArtifact | null;
relationshipsPath: string;
io: KloCliIo;
io: KtxCliIo;
}): void {
input.io.stdout.write('KLO relationship artifacts\n');
input.io.stdout.write('KTX relationship artifacts\n');
input.io.stdout.write(`Run: ${input.runId}\n`);
input.io.stdout.write(`Connection: ${input.connectionId}\n`);
input.io.stdout.write(`Sync: ${input.syncId}\n`);
@ -386,14 +386,14 @@ function writeRelationshipArtifactSummary(input: {
}
input.io.stdout.write(`Artifacts: ${input.relationshipsPath}\n`);
const statuses: Array<Exclude<KloRelationshipArtifactStatus, 'all'>> =
const statuses: Array<Exclude<KtxRelationshipArtifactStatus, 'all'>> =
input.status === 'all' ? ['accepted', 'review', 'rejected', 'skipped'] : [input.status];
for (const status of statuses) {
writeRelationshipGroup(status, input.relationships, input.limit, input.io);
}
}
function writeRelationshipDecisionResult(result: WriteLocalScanRelationshipReviewDecisionResult, io: KloCliIo): void {
function writeRelationshipDecisionResult(result: WriteLocalScanRelationshipReviewDecisionResult, io: KtxCliIo): void {
io.stdout.write('Recorded relationship decision\n');
io.stdout.write(`Decision: ${result.decision.decision}\n`);
io.stdout.write(`Candidate: ${result.decision.candidateId}\n`);
@ -405,7 +405,7 @@ function writeRelationshipDecisionResult(result: WriteLocalScanRelationshipRevie
io.stdout.write(`Path: ${result.path}\n`);
}
function writeRelationshipApplyResult(result: ApplyLocalScanRelationshipReviewDecisionsResult, io: KloCliIo): void {
function writeRelationshipApplyResult(result: ApplyLocalScanRelationshipReviewDecisionsResult, io: KtxCliIo): void {
io.stdout.write('Relationship review apply\n');
io.stdout.write(`Run: ${result.runId}\n`);
io.stdout.write(`Connection: ${result.connectionId}\n`);
@ -433,15 +433,15 @@ function feedbackTableShortName(value: string): string {
return value.split('.').at(-1) ?? value;
}
function feedbackEndpoint(label: KloRelationshipFeedbackLabel, side: 'from' | 'to'): string {
function feedbackEndpoint(label: KtxRelationshipFeedbackLabel, side: 'from' | 'to'): string {
if (side === 'from') {
return `${feedbackTableShortName(label.fromTable)}.${formatFeedbackColumns(label.fromColumns)}`;
}
return `${feedbackTableShortName(label.toTable)}.${formatFeedbackColumns(label.toColumns)}`;
}
function writeRelationshipFeedbackSummary(result: ExportLocalRelationshipFeedbackLabelsResult, io: KloCliIo): void {
io.stdout.write('KLO relationship feedback labels\n');
function writeRelationshipFeedbackSummary(result: ExportLocalRelationshipFeedbackLabelsResult, io: KtxCliIo): void {
io.stdout.write('KTX relationship feedback labels\n');
io.stdout.write(`Generated: ${result.generatedAt}\n`);
io.stdout.write(`Filter connection: ${result.filters.connectionId ?? 'all'}\n`);
io.stdout.write(`Filter decision: ${result.filters.decision}\n`);
@ -474,29 +474,29 @@ function writeRelationshipFeedbackSummary(result: ExportLocalRelationshipFeedbac
}
}
interface KloCliScanProgressState {
interface KtxCliScanProgressState {
progress: number;
hasPendingTransient: boolean;
}
interface KloCliScanProgressUpdateOptions {
interface KtxCliScanProgressUpdateOptions {
transient?: boolean;
}
interface KloCliScanProgress extends Omit<KloProgressPort, 'update'> {
update(progress: number, message?: string, options?: KloCliScanProgressUpdateOptions): Promise<void>;
interface KtxCliScanProgress extends Omit<KtxProgressPort, 'update'> {
update(progress: number, message?: string, options?: KtxCliScanProgressUpdateOptions): Promise<void>;
flush(): void;
}
export function createCliScanProgress(
io: KloCliIo,
state: KloCliScanProgressState = { progress: 0, hasPendingTransient: false },
io: KtxCliIo,
state: KtxCliScanProgressState = { progress: 0, hasPendingTransient: false },
start = 0,
weight = 1,
): KloCliScanProgress {
): KtxCliScanProgress {
const shouldWrite = io.stdout.isTTY === true && !process.env.CI;
const progress: KloCliScanProgress = {
async update(value: number, message?: string, options?: KloCliScanProgressUpdateOptions) {
const progress: KtxCliScanProgress = {
async update(value: number, message?: string, options?: KtxCliScanProgressUpdateOptions) {
const absoluteValue = start + Math.max(0, Math.min(1, value)) * weight;
state.progress = Math.max(state.progress, Math.min(1, absoluteValue));
if (!shouldWrite || !message) {
@ -526,7 +526,7 @@ export function createCliScanProgress(
return progress;
}
function writeStatus(status: LocalScanStatusResponse, io: KloCliIo): void {
function writeStatus(status: LocalScanStatusResponse, io: KtxCliIo): void {
io.stdout.write(`Run: ${status.runId}\n`);
io.stdout.write(`Status: ${status.status}\n`);
io.stdout.write(`Connection: ${status.connectionId}\n`);
@ -536,9 +536,9 @@ function writeStatus(status: LocalScanStatusResponse, io: KloCliIo): void {
io.stdout.write(`Report: ${status.reportPath ?? 'none'}\n`);
}
export async function runKloScan(args: KloScanArgs, io: KloCliIo = process, deps: KloScanDeps = {}): Promise<number> {
export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps: KtxScanDeps = {}): Promise<number> {
try {
const project = await loadKloProject({ projectDir: args.projectDir });
const project = await loadKtxProject({ projectDir: args.projectDir });
if (args.command === 'status') {
const status = await (deps.getLocalScanStatus ?? getLocalScanStatus)(project, args.runId);
if (!status) {
@ -655,7 +655,7 @@ export async function runKloScan(args: KloScanArgs, io: KloCliIo = process, deps
);
if (args.jsonl) {
io.stdout.write(
(deps.formatKloRelationshipFeedbackLabelsJsonl ?? formatKloRelationshipFeedbackLabelsJsonl)(result),
(deps.formatKtxRelationshipFeedbackLabelsJsonl ?? formatKtxRelationshipFeedbackLabelsJsonl)(result),
);
} else if (args.json) {
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@ -675,10 +675,10 @@ export async function runKloScan(args: KloScanArgs, io: KloCliIo = process, deps
},
);
if (args.json) {
io.stdout.write(`${JSON.stringify(result satisfies KloRelationshipFeedbackCalibrationReport, null, 2)}\n`);
io.stdout.write(`${JSON.stringify(result satisfies KtxRelationshipFeedbackCalibrationReport, null, 2)}\n`);
} else {
io.stdout.write(
(deps.formatKloRelationshipFeedbackCalibrationMarkdown ?? formatKloRelationshipFeedbackCalibrationMarkdown)(
(deps.formatKtxRelationshipFeedbackCalibrationMarkdown ?? formatKtxRelationshipFeedbackCalibrationMarkdown)(
result,
),
);
@ -695,10 +695,10 @@ export async function runKloScan(args: KloScanArgs, io: KloCliIo = process, deps
minRejectedLabels: args.minRejectedLabels,
});
if (args.json) {
io.stdout.write(`${JSON.stringify(result satisfies KloRelationshipThresholdAdviceReport, null, 2)}\n`);
io.stdout.write(`${JSON.stringify(result satisfies KtxRelationshipThresholdAdviceReport, null, 2)}\n`);
} else {
io.stdout.write(
(deps.formatKloRelationshipThresholdAdviceMarkdown ?? formatKloRelationshipThresholdAdviceMarkdown)(result),
(deps.formatKtxRelationshipThresholdAdviceMarkdown ?? formatKtxRelationshipThresholdAdviceMarkdown)(result),
);
}
return 0;
@ -706,7 +706,7 @@ export async function runKloScan(args: KloScanArgs, io: KloCliIo = process, deps
const connector =
args.mode !== 'structural' || args.detectRelationships
? await createKloCliScanConnector(project, args.connectionId)
? await createKtxCliScanConnector(project, args.connectionId)
: undefined;
const progress = createCliScanProgress(io);
try {
@ -719,7 +719,7 @@ export async function runKloScan(args: KloScanArgs, io: KloCliIo = process, deps
trigger: 'cli',
databaseIntrospectionUrl: args.databaseIntrospectionUrl,
connector,
adapters: (deps.createLocalIngestAdapters ?? createKloCliLocalIngestAdapters)(project, {
adapters: (deps.createLocalIngestAdapters ?? createKtxCliLocalIngestAdapters)(project, {
databaseIntrospectionUrl: args.databaseIntrospectionUrl,
}),
progress,

View file

@ -1,16 +1,16 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { SourceAdapter } from '@klo/context/ingest';
import { initKloProject } from '@klo/context/project';
import type { SourceAdapter } from '@ktx/context/ingest';
import { initKtxProject } from '@ktx/context/project';
import { describe, expect, it, vi } from 'vitest';
import { runKloServeStdio } from './serve.js';
import { runKtxServeStdio } from './serve.js';
describe('runKloServeStdio', () => {
describe('runKtxServeStdio', () => {
it('loads the project, creates local ports, and connects the server to stdio', async () => {
const connect = vi.fn().mockResolvedValue(undefined);
const project = {
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
config: {
connections: {},
llm: {
@ -27,10 +27,10 @@ describe('runKloServeStdio', () => {
let stderr = '';
await expect(
runKloServeStdio(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
@ -49,7 +49,7 @@ describe('runKloServeStdio', () => {
),
).resolves.toBe(0);
expect(loadProject).toHaveBeenCalledWith({ projectDir: '/tmp/klo-project' });
expect(loadProject).toHaveBeenCalledWith({ projectDir: '/tmp/ktx-project' });
expect(createContextTools).toHaveBeenCalledWith(
project,
expect.objectContaining({
@ -62,26 +62,26 @@ describe('runKloServeStdio', () => {
}),
);
expect(createServer).toHaveBeenCalledWith({
name: 'klo',
name: 'ktx',
version: '0.0.0-private',
userContext: { userId: 'agent' },
contextTools,
memoryCapture: undefined,
});
expect(connect).toHaveBeenCalledWith({ kind: 'stdio' });
expect(stderr).toContain('klo MCP server running on stdio for /tmp/klo-project');
expect(stderr).toContain('ktx MCP server running on stdio for /tmp/ktx-project');
});
it('enables local ingest ports by default when serving stdio', async () => {
const project = { projectDir: '/tmp/klo-project', config: { connections: {} } } as never;
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const connect = vi.fn().mockResolvedValue(undefined);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
await expect(
runKloServeStdio(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
@ -114,17 +114,17 @@ describe('runKloServeStdio', () => {
});
it('passes daemon database introspection URL to MCP local ingest adapters', async () => {
const project = { projectDir: '/tmp/klo-project', config: { connections: {} } } as never;
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const connect = vi.fn().mockResolvedValue(undefined);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
const createdAdapters: SourceAdapter[] = [];
const createIngestAdapters = vi.fn(() => createdAdapters);
await expect(
runKloServeStdio(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
@ -162,13 +162,13 @@ describe('runKloServeStdio', () => {
});
it('uses CLI-native local ingest adapters for standalone scan tools', async () => {
const project = { projectDir: '/tmp/klo-project', config: { connections: {} } } as never;
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const createContextTools = vi.fn(() => ({}) as never);
await runKloServeStdio(
await runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
userId: 'local',
semanticCompute: false,
executeQueries: false,
@ -193,14 +193,14 @@ describe('runKloServeStdio', () => {
});
it('passes semantic compute to local project ports when enabled', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-serve-'));
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-serve-'));
try {
const project = await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
await expect(
runKloServeStdio(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: project.projectDir,
@ -242,16 +242,16 @@ describe('runKloServeStdio', () => {
});
it('uses the HTTP semantic compute port when a daemon URL is provided', async () => {
const project = { projectDir: '/tmp/klo-project', config: { connections: {} } } as never;
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
const createHttpSemanticLayerCompute = vi.fn(() => semanticLayerCompute);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
await expect(
runKloServeStdio(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: true,
semanticComputeUrl: 'http://127.0.0.1:8765',
@ -281,17 +281,17 @@ describe('runKloServeStdio', () => {
});
it('passes a query executor to local project ports only when query execution is enabled', async () => {
const project = { projectDir: '/tmp/klo-project', config: { connections: {} } } as never;
const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
const connect = vi.fn().mockResolvedValue(undefined);
const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
const semanticLayerCompute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
const queryExecutor = { execute: vi.fn() };
await expect(
runKloServeStdio(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: true,
semanticComputeUrl: undefined,
@ -331,7 +331,7 @@ describe('runKloServeStdio', () => {
it('creates a local memory capture port when memory capture is enabled', async () => {
const project = {
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
config: {
connections: {},
llm: {
@ -348,10 +348,10 @@ describe('runKloServeStdio', () => {
const createServer = vi.fn().mockReturnValue({ connect });
await expect(
runKloServeStdio(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: false,
semanticComputeUrl: undefined,
@ -376,7 +376,7 @@ describe('runKloServeStdio', () => {
semanticLayerCompute: undefined,
});
expect(createServer).toHaveBeenCalledWith({
name: 'klo',
name: 'ktx',
version: '0.0.0-private',
userContext: { userId: 'agent' },
contextTools,
@ -386,7 +386,7 @@ describe('runKloServeStdio', () => {
it('reuses semantic compute for local memory capture when enabled', async () => {
const project = {
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
config: {
connections: {},
llm: {
@ -399,10 +399,10 @@ describe('runKloServeStdio', () => {
const createMemoryCapture = vi.fn().mockReturnValue({ capture: vi.fn(), status: vi.fn() });
await expect(
runKloServeStdio(
runKtxServeStdio(
{
mcp: 'stdio',
projectDir: '/tmp/klo-project',
projectDir: '/tmp/ktx-project',
userId: 'agent',
semanticCompute: true,
semanticComputeUrl: undefined,

View file

@ -1,27 +1,27 @@
import { createLocalKloLlmProviderFromConfig } from '@klo/context';
import { createDefaultLocalQueryExecutor, type KloSqlQueryExecutorPort } from '@klo/context/connections';
import { createLocalKtxLlmProviderFromConfig } from '@ktx/context';
import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
import {
createHttpSemanticLayerComputePort,
createPythonSemanticLayerComputePort,
type KloSemanticLayerComputePort,
} from '@klo/context/daemon';
import { createDefaultLocalIngestAdapters, type LocalIngestMcpOptions } from '@klo/context/ingest';
type KtxSemanticLayerComputePort,
} from '@ktx/context/daemon';
import { createDefaultLocalIngestAdapters, type LocalIngestMcpOptions } from '@ktx/context/ingest';
import {
createDefaultKloMcpServer,
createDefaultKtxMcpServer,
createLocalProjectMcpContextPorts,
type KloMcpContextPorts,
} from '@klo/context/mcp';
import { createLocalProjectMemoryCapture, type MemoryCaptureService } from '@klo/context/memory';
import { type KloLocalProject, loadKloProject } from '@klo/context/project';
import type { LocalScanMcpOptions } from '@klo/context/scan';
type KtxMcpContextPorts,
} from '@ktx/context/mcp';
import { createLocalProjectMemoryCapture, type MemoryCaptureService } from '@ktx/context/memory';
import { type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
import type { LocalScanMcpOptions } from '@ktx/context/scan';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
import { createKloCliScanConnector } from './local-scan-connectors.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js';
import { profileMark } from './startup-profile.js';
profileMark('module:serve');
export interface KloServeArgs {
export interface KtxServeArgs {
mcp: 'stdio';
projectDir: string;
userId: string;
@ -33,34 +33,34 @@ export interface KloServeArgs {
memoryModel?: string;
}
interface KloServeIo {
interface KtxServeIo {
stderr: { write(chunk: string): void };
}
interface LocalProjectContextToolOptions {
semanticLayerCompute?: KloSemanticLayerComputePort;
queryExecutor?: KloSqlQueryExecutorPort;
semanticLayerCompute?: KtxSemanticLayerComputePort;
queryExecutor?: KtxSqlQueryExecutorPort;
localIngest?: LocalIngestMcpOptions;
localScan?: LocalScanMcpOptions;
}
interface KloServeDeps {
loadProject?: typeof loadKloProject;
createContextTools?: (project: KloLocalProject, options?: LocalProjectContextToolOptions) => KloMcpContextPorts;
createSemanticLayerCompute?: () => KloSemanticLayerComputePort;
createHttpSemanticLayerCompute?: (baseUrl: string) => KloSemanticLayerComputePort;
interface KtxServeDeps {
loadProject?: typeof loadKtxProject;
createContextTools?: (project: KtxLocalProject, options?: LocalProjectContextToolOptions) => KtxMcpContextPorts;
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
createHttpSemanticLayerCompute?: (baseUrl: string) => KtxSemanticLayerComputePort;
createIngestAdapters?: typeof createDefaultLocalIngestAdapters;
createQueryExecutor?: () => KloSqlQueryExecutorPort;
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
createMemoryCapture?: typeof createLocalProjectMemoryCapture;
createServer?: typeof createDefaultKloMcpServer;
createServer?: typeof createDefaultKtxMcpServer;
createTransport?: () => StdioServerTransport;
stderr?: KloServeIo['stderr'];
stderr?: KtxServeIo['stderr'];
}
export async function runKloServeStdio(args: KloServeArgs, deps: KloServeDeps = {}): Promise<number> {
const loadProjectFn = deps.loadProject ?? loadKloProject;
export async function runKtxServeStdio(args: KtxServeArgs, deps: KtxServeDeps = {}): Promise<number> {
const loadProjectFn = deps.loadProject ?? loadKtxProject;
const createContextToolsFn = deps.createContextTools ?? createLocalProjectMcpContextPorts;
const createServerFn = deps.createServer ?? createDefaultKloMcpServer;
const createServerFn = deps.createServer ?? createDefaultKtxMcpServer;
const createTransportFn = deps.createTransport ?? (() => new StdioServerTransport());
const stderr = deps.stderr ?? process.stderr;
@ -75,12 +75,12 @@ export async function runKloServeStdio(args: KloServeArgs, deps: KloServeDeps =
const queryExecutor = args.executeQueries
? (deps.createQueryExecutor ?? createDefaultLocalQueryExecutor)()
: undefined;
const createIngestAdapters = deps.createIngestAdapters ?? createKloCliLocalIngestAdapters;
const createIngestAdapters = deps.createIngestAdapters ?? createKtxCliLocalIngestAdapters;
const localAdapters = createIngestAdapters(project, {
databaseIntrospectionUrl: args.databaseIntrospectionUrl,
});
const llmProvider = args.memoryCapture
? (createLocalKloLlmProviderFromConfig(project.config.llm) ?? undefined)
? (createLocalKtxLlmProviderFromConfig(project.config.llm) ?? undefined)
: undefined;
const memoryCapture: MemoryCaptureService | undefined = args.memoryCapture
? (deps.createMemoryCapture ?? createLocalProjectMemoryCapture)(project, {
@ -96,7 +96,7 @@ export async function runKloServeStdio(args: KloServeArgs, deps: KloServeDeps =
const localScan: LocalScanMcpOptions = {
adapters: localAdapters,
databaseIntrospectionUrl: args.databaseIntrospectionUrl,
createConnector: (connectionId) => createKloCliScanConnector(project, connectionId),
createConnector: (connectionId) => createKtxCliScanConnector(project, connectionId),
};
const contextToolOptions: LocalProjectContextToolOptions = {
localIngest,
@ -106,7 +106,7 @@ export async function runKloServeStdio(args: KloServeArgs, deps: KloServeDeps =
};
const contextTools = createContextToolsFn(project, contextToolOptions);
const server = createServerFn({
name: 'klo',
name: 'ktx',
version: '0.0.0-private',
userContext: { userId: args.userId },
contextTools,
@ -114,6 +114,6 @@ export async function runKloServeStdio(args: KloServeArgs, deps: KloServeDeps =
});
const transport = createTransportFn();
await server.connect(transport);
stderr.write(`klo MCP server running on stdio for ${project.projectDir}\n`);
stderr.write(`ktx MCP server running on stdio for ${project.projectDir}\n`);
return 0;
}

View file

@ -3,10 +3,10 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
plannedKloAgentFiles,
readKloAgentInstallManifest,
removeKloAgentInstall,
runKloSetupAgentsStep,
plannedKtxAgentFiles,
readKtxAgentInstallManifest,
removeKtxAgentInstall,
runKtxSetupAgentsStep,
} from './setup-agents.js';
function makeIo() {
@ -26,9 +26,9 @@ describe('setup agents', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-setup-agents-'));
await mkdir(join(tempDir, '.klo', 'agents'), { recursive: true });
await writeFile(join(tempDir, 'klo.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-'));
await mkdir(join(tempDir, '.ktx', 'agents'), { recursive: true });
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
});
afterEach(async () => {
@ -36,22 +36,22 @@ describe('setup agents', () => {
});
it('plans project-scoped CLI and MCP files for every target', () => {
expect(plannedKloAgentFiles({ projectDir: tempDir, target: 'claude-code', scope: 'project', mode: 'both' })).toEqual([
{ kind: 'file', path: join(tempDir, '.claude/skills/klo/SKILL.md') },
{ kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'klo'] },
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-code', scope: 'project', mode: 'both' })).toEqual([
{ kind: 'file', path: join(tempDir, '.claude/skills/ktx/SKILL.md') },
{ kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
]);
expect(plannedKloAgentFiles({ projectDir: tempDir, target: 'codex', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.agents/skills/klo/SKILL.md') },
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'codex', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.agents/skills/ktx/SKILL.md') },
]);
expect(plannedKloAgentFiles({ projectDir: tempDir, target: 'cursor', scope: 'project', mode: 'mcp' })).toEqual([
{ kind: 'json-key', path: join(tempDir, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'klo'] },
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'cursor', scope: 'project', mode: 'mcp' })).toEqual([
{ kind: 'json-key', path: join(tempDir, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
]);
expect(plannedKloAgentFiles({ projectDir: tempDir, target: 'opencode', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.opencode/commands/klo.md') },
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'opencode', scope: 'project', mode: 'cli' })).toEqual([
{ kind: 'file', path: join(tempDir, '.opencode/commands/ktx.md') },
]);
expect(plannedKloAgentFiles({ projectDir: tempDir, target: 'universal', scope: 'project', mode: 'both' })).toEqual([
{ kind: 'file', path: join(tempDir, '.agents/skills/klo/SKILL.md') },
{ kind: 'json-key', path: join(tempDir, '.agents/mcp/klo.json'), jsonPath: ['mcpServers', 'klo'] },
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'universal', scope: 'project', mode: 'both' })).toEqual([
{ kind: 'file', path: join(tempDir, '.agents/skills/ktx/SKILL.md') },
{ kind: 'json-key', path: join(tempDir, '.agents/mcp/ktx.json'), jsonPath: ['mcpServers', 'ktx'] },
]);
});
@ -59,7 +59,7 @@ describe('setup agents', () => {
const io = makeIo();
await expect(
runKloSetupAgentsStep(
runKtxSetupAgentsStep(
{
projectDir: tempDir,
inputMode: 'disabled',
@ -78,24 +78,24 @@ describe('setup agents', () => {
installs: [{ target: 'universal', scope: 'project', mode: 'both' }],
});
await expect(stat(join(tempDir, '.agents/skills/klo/SKILL.md'))).resolves.toBeDefined();
await expect(stat(join(tempDir, '.agents/mcp/klo.json'))).resolves.toBeDefined();
const skill = await readFile(join(tempDir, '.agents/skills/klo/SKILL.md'), 'utf-8');
await expect(stat(join(tempDir, '.agents/skills/ktx/SKILL.md'))).resolves.toBeDefined();
await expect(stat(join(tempDir, '.agents/mcp/ktx.json'))).resolves.toBeDefined();
const skill = await readFile(join(tempDir, '.agents/skills/ktx/SKILL.md'), 'utf-8');
expect(skill).toContain(`--project-dir ${tempDir}`);
expect(skill).toContain('must not print secrets');
expect(skill).toContain('klo agent sql execute');
expect(await readKloAgentInstallManifest(tempDir)).toMatchObject({
expect(skill).toContain('ktx agent sql execute');
expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
version: 1,
projectDir: tempDir,
installs: [{ target: 'universal', scope: 'project', mode: 'both' }],
});
expect(await readFile(join(tempDir, 'klo.yaml'), 'utf-8')).toContain('agents');
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain('agents');
expect(io.stderr()).toBe('');
});
it('removes only manifest-listed files and JSON keys', async () => {
const io = makeIo();
await runKloSetupAgentsStep(
await runKtxSetupAgentsStep(
{
projectDir: tempDir,
inputMode: 'disabled',
@ -108,13 +108,13 @@ describe('setup agents', () => {
},
io.io,
);
await writeFile(join(tempDir, '.claude/skills/klo/keep.txt'), 'user file', 'utf-8');
await writeFile(join(tempDir, '.claude/skills/ktx/keep.txt'), 'user file', 'utf-8');
await expect(removeKloAgentInstall(tempDir, io.io)).resolves.toBe(0);
await expect(removeKtxAgentInstall(tempDir, io.io)).resolves.toBe(0);
await expect(stat(join(tempDir, '.claude/skills/klo/SKILL.md'))).rejects.toThrow();
await expect(stat(join(tempDir, '.claude/skills/klo/keep.txt'))).resolves.toBeDefined();
await expect(readKloAgentInstallManifest(tempDir)).resolves.toEqual(null);
await expect(stat(join(tempDir, '.claude/skills/ktx/SKILL.md'))).rejects.toThrow();
await expect(stat(join(tempDir, '.claude/skills/ktx/keep.txt'))).resolves.toBeDefined();
await expect(readKtxAgentInstallManifest(tempDir)).resolves.toEqual(null);
});
it('uses prompts in interactive mode and supports Back', async () => {
@ -126,7 +126,7 @@ describe('setup agents', () => {
};
await expect(
runKloSetupAgentsStep(
runKtxSetupAgentsStep(
{
projectDir: tempDir,
inputMode: 'auto',
@ -151,7 +151,7 @@ describe('setup agents', () => {
};
await expect(
runKloSetupAgentsStep(
runKtxSetupAgentsStep(
{
projectDir: tempDir,
inputMode: 'auto',
@ -169,7 +169,7 @@ describe('setup agents', () => {
expect(prompts.multiselect).toHaveBeenCalledWith(
expect.objectContaining({
message:
'Which agent targets should KLO install?\nUse Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
'Which agent targets should KTX install?\nUse Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
}),
);
});

View file

@ -1,83 +1,83 @@
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { cancel, isCancel, multiselect, select } from '@clack/prompts';
import { loadKloProject, markKloSetupStepComplete, serializeKloProjectConfig } from '@klo/context/project';
import type { KloCliIo } from './cli-runtime.js';
import { loadKtxProject, markKtxSetupStepComplete, serializeKtxProjectConfig } from '@ktx/context/project';
import type { KtxCliIo } from './cli-runtime.js';
import { withMenuOptionsSpacing, withMultiselectNavigation } from './prompt-navigation.js';
import { withSetupInterruptConfirmation } from './setup-interrupt.js';
export type KloAgentTarget = 'claude-code' | 'codex' | 'cursor' | 'opencode' | 'universal';
export type KloAgentScope = 'project' | 'global';
export type KloAgentInstallMode = 'cli' | 'mcp' | 'both';
export type KtxAgentTarget = 'claude-code' | 'codex' | 'cursor' | 'opencode' | 'universal';
export type KtxAgentScope = 'project' | 'global';
export type KtxAgentInstallMode = 'cli' | 'mcp' | 'both';
export interface KloSetupAgentsArgs {
export interface KtxSetupAgentsArgs {
projectDir: string;
inputMode: 'auto' | 'disabled';
yes: boolean;
agents: boolean;
target?: KloAgentTarget;
scope: KloAgentScope;
mode: KloAgentInstallMode;
target?: KtxAgentTarget;
scope: KtxAgentScope;
mode: KtxAgentInstallMode;
skipAgents: boolean;
}
export type KloSetupAgentsResult =
export type KtxSetupAgentsResult =
| {
status: 'ready';
projectDir: string;
installs: Array<{ target: KloAgentTarget; scope: KloAgentScope; mode: KloAgentInstallMode }>;
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>;
}
| { status: 'skipped'; projectDir: string }
| { status: 'back'; projectDir: string }
| { status: 'missing-input'; projectDir: string }
| { status: 'failed'; projectDir: string };
export interface KloAgentInstallManifest {
export interface KtxAgentInstallManifest {
version: 1;
projectDir: string;
installedAt: string;
installs: Array<{ target: KloAgentTarget; scope: KloAgentScope; mode: KloAgentInstallMode }>;
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>;
entries: Array<{ kind: 'file'; path: string } | { kind: 'json-key'; path: string; jsonPath: string[] }>;
}
type InstallEntry = KloAgentInstallManifest['entries'][number];
type InstallEntry = KtxAgentInstallManifest['entries'][number];
export function agentInstallManifestPath(projectDir: string): string {
return join(resolve(projectDir), '.klo/agents/install-manifest.json');
return join(resolve(projectDir), '.ktx/agents/install-manifest.json');
}
export function plannedKloAgentFiles(input: {
export function plannedKtxAgentFiles(input: {
projectDir: string;
target: KloAgentTarget;
scope: KloAgentScope;
mode: KloAgentInstallMode;
target: KtxAgentTarget;
scope: KtxAgentScope;
mode: KtxAgentInstallMode;
}): InstallEntry[] {
if (input.scope === 'global') {
if (input.target === 'claude-code') {
return [{ kind: 'file', path: join(process.env.HOME ?? '', '.claude/skills/klo/SKILL.md') }];
return [{ kind: 'file', path: join(process.env.HOME ?? '', '.claude/skills/ktx/SKILL.md') }];
}
if (input.target === 'codex') {
return [
{ kind: 'file', path: join(process.env.CODEX_HOME ?? join(process.env.HOME ?? '', '.codex'), 'skills/klo/SKILL.md') },
{ kind: 'file', path: join(process.env.CODEX_HOME ?? join(process.env.HOME ?? '', '.codex'), 'skills/ktx/SKILL.md') },
];
}
throw new Error(`Global ${input.target} installation is not supported; use --project.`);
}
const root = resolve(input.projectDir);
const cliEntries: Partial<Record<KloAgentTarget, InstallEntry>> = {
'claude-code': { kind: 'file', path: join(root, '.claude/skills/klo/SKILL.md') },
codex: { kind: 'file', path: join(root, '.agents/skills/klo/SKILL.md') },
cursor: { kind: 'file', path: join(root, '.cursor/rules/klo.mdc') },
opencode: { kind: 'file', path: join(root, '.opencode/commands/klo.md') },
universal: { kind: 'file', path: join(root, '.agents/skills/klo/SKILL.md') },
const cliEntries: Partial<Record<KtxAgentTarget, InstallEntry>> = {
'claude-code': { kind: 'file', path: join(root, '.claude/skills/ktx/SKILL.md') },
codex: { kind: 'file', path: join(root, '.agents/skills/ktx/SKILL.md') },
cursor: { kind: 'file', path: join(root, '.cursor/rules/ktx.mdc') },
opencode: { kind: 'file', path: join(root, '.opencode/commands/ktx.md') },
universal: { kind: 'file', path: join(root, '.agents/skills/ktx/SKILL.md') },
};
const mcpEntries: Record<KloAgentTarget, InstallEntry> = {
'claude-code': { kind: 'json-key', path: join(root, '.mcp.json'), jsonPath: ['mcpServers', 'klo'] },
codex: { kind: 'json-key', path: join(root, '.agents/mcp/klo.json'), jsonPath: ['mcpServers', 'klo'] },
cursor: { kind: 'json-key', path: join(root, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'klo'] },
opencode: { kind: 'json-key', path: join(root, '.opencode/mcp.json'), jsonPath: ['mcpServers', 'klo'] },
universal: { kind: 'json-key', path: join(root, '.agents/mcp/klo.json'), jsonPath: ['mcpServers', 'klo'] },
const mcpEntries: Record<KtxAgentTarget, InstallEntry> = {
'claude-code': { kind: 'json-key', path: join(root, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
codex: { kind: 'json-key', path: join(root, '.agents/mcp/ktx.json'), jsonPath: ['mcpServers', 'ktx'] },
cursor: { kind: 'json-key', path: join(root, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
opencode: { kind: 'json-key', path: join(root, '.opencode/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
universal: { kind: 'json-key', path: join(root, '.agents/mcp/ktx.json'), jsonPath: ['mcpServers', 'ktx'] },
};
return [
...(input.mode === 'cli' || input.mode === 'both' ? [cliEntries[input.target]] : []),
@ -85,28 +85,28 @@ export function plannedKloAgentFiles(input: {
].filter((entry): entry is InstallEntry => entry !== undefined);
}
function cliInstructionContent(input: { projectDir: string; target: KloAgentTarget }): string {
function cliInstructionContent(input: { projectDir: string; target: KtxAgentTarget }): string {
return [
'---',
'name: klo',
'description: Use local KLO semantic context, wiki knowledge, and safe SQL execution for this project.',
'name: ktx',
'description: Use local KTX semantic context, wiki knowledge, and safe SQL execution for this project.',
'---',
'',
'# KLO Local Context',
'# KTX Local Context',
'',
`Use this project with \`--project-dir ${input.projectDir}\`.`,
'',
'Agents must not print secrets, credential references, environment variable values, or file contents from `.klo/secrets`.',
'Agents must not print secrets, credential references, environment variable values, or file contents from `.ktx/secrets`.',
'',
'Available commands:',
'',
`- \`klo agent context --json --project-dir ${input.projectDir}\``,
`- \`klo agent sl list --json --project-dir ${input.projectDir}\``,
`- \`klo agent sl read <sourceName> --json --project-dir ${input.projectDir}\``,
`- \`klo agent sl query --json --project-dir ${input.projectDir} --connection-id <id> --query-file <path> --execute --max-rows 100\``,
`- \`klo agent wiki search <query> --json --project-dir ${input.projectDir}\``,
`- \`klo agent wiki read <pageId> --json --project-dir ${input.projectDir}\``,
`- \`klo agent sql execute --json --project-dir ${input.projectDir} --connection-id <id> --sql-file <path> --max-rows 100\``,
`- \`ktx agent context --json --project-dir ${input.projectDir}\``,
`- \`ktx agent sl list --json --project-dir ${input.projectDir}\``,
`- \`ktx agent sl read <sourceName> --json --project-dir ${input.projectDir}\``,
`- \`ktx agent sl query --json --project-dir ${input.projectDir} --connection-id <id> --query-file <path> --execute --max-rows 100\``,
`- \`ktx agent wiki search <query> --json --project-dir ${input.projectDir}\``,
`- \`ktx agent wiki read <pageId> --json --project-dir ${input.projectDir}\``,
`- \`ktx agent sql execute --json --project-dir ${input.projectDir} --connection-id <id> --sql-file <path> --max-rows 100\``,
'',
'SQL execution is read-only, requires an explicit row limit, and should use the smallest useful limit.',
'',
@ -115,7 +115,7 @@ function cliInstructionContent(input: { projectDir: string; target: KloAgentTarg
function mcpConfig(projectDir: string): Record<string, unknown> {
return {
command: 'klo',
command: 'ktx',
args: ['--project-dir', projectDir, 'serve', '--mcp', 'stdio', '--semantic-compute', '--execute-queries'],
env: {},
};
@ -151,15 +151,15 @@ async function removeJsonKey(path: string, jsonPath: string[]): Promise<void> {
await writeFile(path, `${JSON.stringify(root, null, 2)}\n`, 'utf-8');
}
export async function readKloAgentInstallManifest(projectDir: string): Promise<KloAgentInstallManifest | null> {
export async function readKtxAgentInstallManifest(projectDir: string): Promise<KtxAgentInstallManifest | null> {
try {
return JSON.parse(await readFile(agentInstallManifestPath(projectDir), 'utf-8')) as KloAgentInstallManifest;
return JSON.parse(await readFile(agentInstallManifestPath(projectDir), 'utf-8')) as KtxAgentInstallManifest;
} catch {
return null;
}
}
async function writeManifest(projectDir: string, manifest: KloAgentInstallManifest): Promise<void> {
async function writeManifest(projectDir: string, manifest: KtxAgentInstallManifest): Promise<void> {
const path = agentInstallManifestPath(projectDir);
await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
@ -171,11 +171,11 @@ function entryKey(entry: InstallEntry): string {
function mergeManifest(
projectDir: string,
existing: KloAgentInstallManifest | null,
installs: KloAgentInstallManifest['installs'],
existing: KtxAgentInstallManifest | null,
installs: KtxAgentInstallManifest['installs'],
entries: InstallEntry[],
): KloAgentInstallManifest {
const installMap = new Map<string, KloAgentInstallManifest['installs'][number]>();
): KtxAgentInstallManifest {
const installMap = new Map<string, KtxAgentInstallManifest['installs'][number]>();
for (const install of [...(existing?.installs ?? []), ...installs]) {
installMap.set(`${install.target}:${install.scope}:${install.mode}`, install);
}
@ -192,10 +192,10 @@ function mergeManifest(
};
}
export async function removeKloAgentInstall(projectDir: string, io: KloCliIo): Promise<number> {
const manifest = await readKloAgentInstallManifest(projectDir);
export async function removeKtxAgentInstall(projectDir: string, io: KtxCliIo): Promise<number> {
const manifest = await readKtxAgentInstallManifest(projectDir);
if (!manifest) {
io.stdout.write('No KLO agent installation manifest found.\n');
io.stdout.write('No KTX agent installation manifest found.\n');
return 0;
}
for (const entry of manifest.entries) {
@ -203,11 +203,11 @@ export async function removeKloAgentInstall(projectDir: string, io: KloCliIo): P
if (entry.kind === 'json-key') await removeJsonKey(entry.path, entry.jsonPath).catch(() => undefined);
}
await rm(agentInstallManifestPath(projectDir), { force: true });
io.stdout.write('Removed KLO agent integration files from manifest.\n');
io.stdout.write('Removed KTX agent integration files from manifest.\n');
return 0;
}
export interface KloSetupAgentsPromptAdapter {
export interface KtxSetupAgentsPromptAdapter {
select(options: { message: string; options: Array<{ value: string; label: string }> }): Promise<string>;
multiselect(options: {
message: string;
@ -217,11 +217,11 @@ export interface KloSetupAgentsPromptAdapter {
cancel(message: string): void;
}
export interface KloSetupAgentsDeps {
prompts?: KloSetupAgentsPromptAdapter;
export interface KtxSetupAgentsDeps {
prompts?: KtxSetupAgentsPromptAdapter;
}
function createPromptAdapter(): KloSetupAgentsPromptAdapter {
function createPromptAdapter(): KtxSetupAgentsPromptAdapter {
return {
async select(options) {
const value = await withSetupInterruptConfirmation(() => select(withMenuOptionsSpacing(options)));
@ -247,11 +247,11 @@ function createPromptAdapter(): KloSetupAgentsPromptAdapter {
async function installTarget(input: {
projectDir: string;
target: KloAgentTarget;
scope: KloAgentScope;
mode: KloAgentInstallMode;
target: KtxAgentTarget;
scope: KtxAgentScope;
mode: KtxAgentInstallMode;
}): Promise<InstallEntry[]> {
const entries = plannedKloAgentFiles(input);
const entries = plannedKtxAgentFiles(input);
for (const entry of entries) {
if (entry.kind === 'file') {
await mkdir(dirname(entry.path), { recursive: true });
@ -264,15 +264,15 @@ async function installTarget(input: {
}
async function markAgentsComplete(projectDir: string): Promise<void> {
const project = await loadKloProject({ projectDir });
await writeFile(project.configPath, serializeKloProjectConfig(markKloSetupStepComplete(project.config, 'agents')), 'utf-8');
const project = await loadKtxProject({ projectDir });
await writeFile(project.configPath, serializeKtxProjectConfig(markKtxSetupStepComplete(project.config, 'agents')), 'utf-8');
}
export async function runKloSetupAgentsStep(
args: KloSetupAgentsArgs,
io: KloCliIo,
deps: KloSetupAgentsDeps = {},
): Promise<KloSetupAgentsResult> {
export async function runKtxSetupAgentsStep(
args: KtxSetupAgentsArgs,
io: KtxCliIo,
deps: KtxSetupAgentsDeps = {},
): Promise<KtxSetupAgentsResult> {
if (args.skipAgents) {
io.stdout.write('Agent integration skipped.\n');
return { status: 'skipped', projectDir: args.projectDir };
@ -286,7 +286,7 @@ export async function runKloSetupAgentsStep(
args.inputMode === 'disabled'
? args.mode
: ((await prompts.select({
message: 'How should agents use this KLO project?',
message: 'How should agents use this KTX project?',
options: [
{ value: 'cli', label: 'CLI tools and skills' },
{ value: 'mcp', label: 'MCP server config' },
@ -294,7 +294,7 @@ export async function runKloSetupAgentsStep(
{ value: 'skip', label: 'Skip' },
{ value: 'back', label: 'Back' },
],
})) as KloAgentInstallMode | 'skip' | 'back');
})) as KtxAgentInstallMode | 'skip' | 'back');
if (mode === 'back') return { status: 'back', projectDir: args.projectDir };
if (mode === 'skip') return { status: 'skipped', projectDir: args.projectDir };
@ -304,7 +304,7 @@ export async function runKloSetupAgentsStep(
: args.inputMode === 'disabled'
? []
: ((await prompts.multiselect({
message: withMultiselectNavigation('Which agent targets should KLO install?'),
message: withMultiselectNavigation('Which agent targets should KTX install?'),
options: [
{ value: 'claude-code', label: 'Claude Code' },
{ value: 'codex', label: 'Codex' },
@ -314,8 +314,8 @@ export async function runKloSetupAgentsStep(
{ value: 'back', label: 'Back' },
],
required: true,
})) as KloAgentTarget[]);
if (targets.includes('back' as KloAgentTarget)) return { status: 'back', projectDir: args.projectDir };
})) as KtxAgentTarget[]);
if (targets.includes('back' as KtxAgentTarget)) return { status: 'back', projectDir: args.projectDir };
if (targets.length === 0) {
io.stderr.write('Missing agent target: pass --target or use interactive setup.\n');
return { status: 'missing-input', projectDir: args.projectDir };
@ -325,7 +325,7 @@ export async function runKloSetupAgentsStep(
const entries: InstallEntry[] = [];
try {
for (const install of installs) entries.push(...(await installTarget({ projectDir: args.projectDir, ...install })));
await writeManifest(args.projectDir, mergeManifest(args.projectDir, await readKloAgentInstallManifest(args.projectDir), installs, entries));
await writeManifest(args.projectDir, mergeManifest(args.projectDir, await readKtxAgentInstallManifest(args.projectDir), installs, entries));
await markAgentsComplete(args.projectDir);
io.stdout.write(`Agent integration installed for ${installs.map((install) => install.target).join(', ')}.\n`);
return { status: 'ready', projectDir: args.projectDir, installs };

View file

@ -5,10 +5,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
contextBuildCommands,
readKloSetupContextState,
runKloSetupContextCommand,
runKloSetupContextStep,
writeKloSetupContextState,
readKtxSetupContextState,
runKtxSetupContextCommand,
runKtxSetupContextStep,
writeKtxSetupContextState,
} from './setup-context.js';
function makeIo() {
@ -34,7 +34,7 @@ function makeIo() {
async function writeReadyProject(projectDir: string) {
await writeFile(
join(projectDir, 'klo.yaml'),
join(projectDir, 'ktx.yaml'),
[
'project: revenue',
'setup:',
@ -124,7 +124,7 @@ describe('setup context build state', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'klo-setup-context-'));
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-context-'));
});
afterEach(async () => {
@ -132,9 +132,9 @@ describe('setup context build state', () => {
});
it('reads missing state as not started and writes durable command metadata without secrets', async () => {
await expect(readKloSetupContextState(tempDir)).resolves.toMatchObject({ status: 'not_started' });
await expect(readKtxSetupContextState(tempDir)).resolves.toMatchObject({ status: 'not_started' });
await writeKloSetupContextState(tempDir, {
await writeKtxSetupContextState(tempDir, {
runId: 'setup-context-local-abc123',
status: 'running',
startedAt: '2026-05-09T10:00:00.000Z',
@ -147,16 +147,16 @@ describe('setup context build state', () => {
commands: contextBuildCommands(tempDir, 'setup-context-local-abc123'),
});
const state = await readKloSetupContextState(tempDir);
const state = await readKtxSetupContextState(tempDir);
expect(state).toMatchObject({
runId: 'setup-context-local-abc123',
status: 'running',
primarySourceConnectionIds: ['warehouse'],
contextSourceConnectionIds: ['docs'],
commands: {
watch: `klo setup context watch setup-context-local-abc123 --project-dir ${tempDir}`,
status: `klo setup context status setup-context-local-abc123 --project-dir ${tempDir}`,
resume: `klo setup --project-dir ${tempDir}`,
watch: `ktx setup context watch setup-context-local-abc123 --project-dir ${tempDir}`,
status: `ktx setup context status setup-context-local-abc123 --project-dir ${tempDir}`,
resume: `ktx setup --project-dir ${tempDir}`,
},
});
expect(JSON.stringify(state)).not.toContain('DATABASE_URL');
@ -175,7 +175,7 @@ describe('setup context build state', () => {
}));
await expect(
runKloSetupContextStep(
runKtxSetupContextStep(
{ projectDir: tempDir, inputMode: 'disabled' },
io.io,
{
@ -199,13 +199,13 @@ describe('setup context build state', () => {
expect.objectContaining({ onDetach: expect.any(Function) }),
);
expect(verifyContextReady).toHaveBeenCalledWith(tempDir);
expect(await readFile(join(tempDir, 'klo.yaml'), 'utf-8')).toContain(' - context');
await expect(readKloSetupContextState(tempDir)).resolves.toMatchObject({
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain(' - context');
await expect(readKtxSetupContextState(tempDir)).resolves.toMatchObject({
runId: 'setup-context-local-abc123',
status: 'completed',
completedAt: '2026-05-09T10:00:00.000Z',
});
expect(io.stdout()).toContain('KLO context is ready for agents.');
expect(io.stdout()).toContain('KTX context is ready for agents.');
});
it('marks context complete without prompting when initial source ingest already made agent context', async () => {
@ -219,7 +219,7 @@ describe('setup context build state', () => {
const runContextBuildMock = vi.fn(async () => ({ exitCode: 0, detached: false }));
await expect(
runKloSetupContextStep(
runKtxSetupContextStep(
{ projectDir: tempDir, inputMode: 'auto' },
io.io,
{
@ -237,14 +237,14 @@ describe('setup context build state', () => {
).resolves.toEqual({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-existing' });
expect(runContextBuildMock).not.toHaveBeenCalled();
expect(await readFile(join(tempDir, 'klo.yaml'), 'utf-8')).toContain(' - context');
await expect(readKloSetupContextState(tempDir)).resolves.toMatchObject({
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain(' - context');
await expect(readKtxSetupContextState(tempDir)).resolves.toMatchObject({
runId: 'setup-context-local-existing',
status: 'completed',
completedAt: '2026-05-09T10:00:00.000Z',
contextSourceConnectionIds: ['docs'],
});
expect(io.stdout()).toContain('KLO context is ready for agents.');
expect(io.stdout()).toContain('KTX context is ready for agents.');
});
it('does not mark context ready until primary scans have completed description enrichment', async () => {
@ -264,7 +264,7 @@ describe('setup context build state', () => {
});
await expect(
runKloSetupContextStep(
runKtxSetupContextStep(
{ projectDir: tempDir, inputMode: 'disabled' },
io.io,
{
@ -292,7 +292,7 @@ describe('setup context build state', () => {
});
await expect(
runKloSetupContextStep(
runKtxSetupContextStep(
{ projectDir: tempDir, inputMode: 'disabled' },
io.io,
{
@ -309,7 +309,7 @@ describe('setup context build state', () => {
it('refuses empty setup context builds', async () => {
await writeFile(
join(tempDir, 'klo.yaml'),
join(tempDir, 'ktx.yaml'),
[
'project: revenue',
'connections: {}',
@ -330,19 +330,19 @@ describe('setup context build state', () => {
const io = makeIo();
await expect(
runKloSetupContextStep(
runKtxSetupContextStep(
{ projectDir: tempDir, inputMode: 'disabled' },
io.io,
{ runIdFactory: () => 'setup-context-local-empty' },
),
).resolves.toEqual({ status: 'failed', projectDir: tempDir });
expect(io.stderr()).toContain('No primary or context sources are configured for a KLO context build.');
expect(io.stderr()).toContain('No primary or context sources are configured for a KTX context build.');
});
it('prints JSON setup context command status with watch and resume commands', async () => {
await mkdir(join(tempDir, '.klo', 'setup'), { recursive: true });
await writeKloSetupContextState(tempDir, {
await mkdir(join(tempDir, '.ktx', 'setup'), { recursive: true });
await writeKtxSetupContextState(tempDir, {
runId: 'setup-context-local-abc123',
status: 'detached',
startedAt: '2026-05-09T10:00:00.000Z',
@ -357,7 +357,7 @@ describe('setup context build state', () => {
const io = makeIo();
await expect(
runKloSetupContextCommand(
runKtxSetupContextCommand(
{ command: 'status', projectDir: tempDir, runId: 'setup-context-local-abc123', json: true },
io.io,
),
@ -367,8 +367,8 @@ describe('setup context build state', () => {
ready: false,
status: 'detached',
runId: 'setup-context-local-abc123',
watchCommand: `klo setup context watch setup-context-local-abc123 --project-dir ${tempDir}`,
statusCommand: `klo setup context status setup-context-local-abc123 --project-dir ${tempDir}`,
watchCommand: `ktx setup context watch setup-context-local-abc123 --project-dir ${tempDir}`,
statusCommand: `ktx setup context status setup-context-local-abc123 --project-dir ${tempDir}`,
});
});
@ -378,7 +378,7 @@ describe('setup context build state', () => {
const runContextBuildMock = vi.fn(async () => ({ exitCode: 0, detached: false }));
await expect(
runKloSetupContextCommand(
runKtxSetupContextCommand(
{ command: 'build', projectDir: tempDir, inputMode: 'auto' },
io.io,
{

View file

@ -3,18 +3,18 @@ import { access, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { cancel, isCancel, select } from '@clack/prompts';
import {
type KloLocalProject,
loadKloProject,
markKloSetupStepComplete,
serializeKloProjectConfig,
} from '@klo/context/project';
import type { KloCliIo } from './cli-runtime.js';
type KtxLocalProject,
loadKtxProject,
markKtxSetupStepComplete,
serializeKtxProjectConfig,
} from '@ktx/context/project';
import type { KtxCliIo } from './cli-runtime.js';
import { buildPublicIngestPlan } from './public-ingest.js';
import { runContextBuild } from './context-build-view.js';
import { withMenuOptionsSpacing } from './prompt-navigation.js';
import { withSetupInterruptConfirmation } from './setup-interrupt.js';
export type KloSetupContextBuildStatus =
export type KtxSetupContextBuildStatus =
| 'not_started'
| 'running'
| 'detached'
@ -24,7 +24,7 @@ export type KloSetupContextBuildStatus =
| 'interrupted'
| 'stale';
export interface KloSetupContextCommands {
export interface KtxSetupContextCommands {
build: string;
watch: string;
status: string;
@ -32,9 +32,9 @@ export interface KloSetupContextCommands {
resume: string;
}
export interface KloSetupContextState {
export interface KtxSetupContextState {
runId?: string;
status: KloSetupContextBuildStatus;
status: KtxSetupContextBuildStatus;
startedAt?: string;
updatedAt?: string;
completedAt?: string;
@ -43,13 +43,13 @@ export interface KloSetupContextState {
reportIds: string[];
artifactPaths: string[];
retryableFailedTargets: string[];
commands: KloSetupContextCommands;
commands: KtxSetupContextCommands;
failureReason?: string;
}
export interface KloSetupContextStatusSummary {
export interface KtxSetupContextStatusSummary {
ready: boolean;
status: KloSetupContextBuildStatus;
status: KtxSetupContextBuildStatus;
runId?: string;
watchCommand?: string;
statusCommand?: string;
@ -57,7 +57,7 @@ export interface KloSetupContextStatusSummary {
detail?: string;
}
export interface KloSetupContextReadiness {
export interface KtxSetupContextReadiness {
ready: boolean;
agentContextReady: boolean;
semanticSearchReady: boolean;
@ -65,7 +65,7 @@ export interface KloSetupContextReadiness {
failedTargets?: string[];
}
export type KloSetupContextResult =
export type KtxSetupContextResult =
| { status: 'ready'; projectDir: string; runId: string }
| { status: 'skipped'; projectDir: string }
| { status: 'detached'; projectDir: string; runId: string }
@ -74,7 +74,7 @@ export type KloSetupContextResult =
| { status: 'missing-input'; projectDir: string }
| { status: 'failed'; projectDir: string };
export interface KloSetupContextStepArgs {
export interface KtxSetupContextStepArgs {
projectDir: string;
inputMode: 'auto' | 'disabled';
forcePrompt?: boolean;
@ -82,35 +82,35 @@ export interface KloSetupContextStepArgs {
prompt?: boolean;
}
export type KloSetupContextCommandArgs =
export type KtxSetupContextCommandArgs =
| { command: 'build'; projectDir: string; inputMode: 'auto' | 'disabled' }
| { command: 'watch'; projectDir: string; runId?: string; inputMode: 'auto' | 'disabled' }
| { command: 'status'; projectDir: string; runId?: string; json: boolean }
| { command: 'stop'; projectDir: string; runId?: string };
export interface KloSetupContextPromptAdapter {
export interface KtxSetupContextPromptAdapter {
select(options: { message: string; options: Array<{ value: string; label: string }> }): Promise<string>;
cancel(message: string): void;
}
export interface KloSetupContextDeps {
prompts?: KloSetupContextPromptAdapter;
export interface KtxSetupContextDeps {
prompts?: KtxSetupContextPromptAdapter;
runIdFactory?: () => string;
now?: () => Date;
runContextBuild?: typeof runContextBuild;
verifyContextReady?: (projectDir: string) => Promise<KloSetupContextReadiness>;
verifyContextReady?: (projectDir: string) => Promise<KtxSetupContextReadiness>;
}
interface KloSetupContextTargets {
interface KtxSetupContextTargets {
primarySourceConnectionIds: string[];
contextSourceConnectionIds: string[];
}
const SETUP_CONTEXT_STATE_PATH = ['.klo', 'setup', 'context-build.json'] as const;
const SETUP_CONTEXT_STATE_PATH = ['.ktx', 'setup', 'context-build.json'] as const;
const LIVE_DATABASE_ADAPTER = 'live-database';
const SCAN_REPORT_FILE = 'scan-report.json';
function createPromptAdapter(): KloSetupContextPromptAdapter {
function createPromptAdapter(): KtxSetupContextPromptAdapter {
return {
async select(options) {
const value = await withSetupInterruptConfirmation(() => select(withMenuOptionsSpacing(options)));
@ -139,19 +139,19 @@ async function pathExists(path: string): Promise<boolean> {
}
}
export function contextBuildCommands(projectDir: string, runId?: string): KloSetupContextCommands {
export function contextBuildCommands(projectDir: string, runId?: string): KtxSetupContextCommands {
const resolvedProjectDir = resolve(projectDir);
const runIdArg = runId ? ` ${runId}` : '';
return {
build: `klo setup context build --project-dir ${resolvedProjectDir}`,
watch: `klo setup context watch${runIdArg} --project-dir ${resolvedProjectDir}`,
status: `klo setup context status${runIdArg} --project-dir ${resolvedProjectDir}`,
stop: `klo setup context stop${runIdArg} --project-dir ${resolvedProjectDir}`,
resume: `klo setup --project-dir ${resolvedProjectDir}`,
build: `ktx setup context build --project-dir ${resolvedProjectDir}`,
watch: `ktx setup context watch${runIdArg} --project-dir ${resolvedProjectDir}`,
status: `ktx setup context status${runIdArg} --project-dir ${resolvedProjectDir}`,
stop: `ktx setup context stop${runIdArg} --project-dir ${resolvedProjectDir}`,
resume: `ktx setup --project-dir ${resolvedProjectDir}`,
};
}
function notStartedState(projectDir: string): KloSetupContextState {
function notStartedState(projectDir: string): KtxSetupContextState {
return {
status: 'not_started',
primarySourceConnectionIds: [],
@ -163,11 +163,11 @@ function notStartedState(projectDir: string): KloSetupContextState {
};
}
function normalizeState(projectDir: string, value: unknown): KloSetupContextState {
function normalizeState(projectDir: string, value: unknown): KtxSetupContextState {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return notStartedState(projectDir);
}
const record = value as Partial<KloSetupContextState>;
const record = value as Partial<KtxSetupContextState>;
const status = record.status ?? 'not_started';
const runId = typeof record.runId === 'string' && record.runId.length > 0 ? record.runId : undefined;
return {
@ -196,7 +196,7 @@ function normalizeState(projectDir: string, value: unknown): KloSetupContextStat
};
}
export async function readKloSetupContextState(projectDir: string): Promise<KloSetupContextState> {
export async function readKtxSetupContextState(projectDir: string): Promise<KtxSetupContextState> {
const filePath = statePath(projectDir);
if (!(await pathExists(filePath))) {
return notStartedState(projectDir);
@ -204,9 +204,9 @@ export async function readKloSetupContextState(projectDir: string): Promise<KloS
return normalizeState(projectDir, JSON.parse(await readFile(filePath, 'utf-8')) as unknown);
}
export async function writeKloSetupContextState(projectDir: string, state: KloSetupContextState): Promise<void> {
export async function writeKtxSetupContextState(projectDir: string, state: KtxSetupContextState): Promise<void> {
const resolvedProjectDir = resolve(projectDir);
await mkdir(join(resolvedProjectDir, '.klo', 'setup'), { recursive: true });
await mkdir(join(resolvedProjectDir, '.ktx', 'setup'), { recursive: true });
const normalized = normalizeState(resolvedProjectDir, {
...state,
commands: contextBuildCommands(resolvedProjectDir, state.runId),
@ -215,9 +215,9 @@ export async function writeKloSetupContextState(projectDir: string, state: KloSe
}
export function setupContextStatusFromState(
state: KloSetupContextState,
state: KtxSetupContextState,
options: { completedStep: boolean } = { completedStep: false },
): KloSetupContextStatusSummary {
): KtxSetupContextStatusSummary {
const status = options.completedStep && state.status === 'not_started' ? 'completed' : state.status;
const ready = options.completedStep && status === 'completed';
return {
@ -234,7 +234,7 @@ function runIdFactory(): string {
return `setup-context-local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
function listContextTargets(project: KloLocalProject): KloSetupContextTargets {
function listContextTargets(project: KtxLocalProject): KtxSetupContextTargets {
if (Object.keys(project.config.connections).length === 0) {
return { primarySourceConnectionIds: [], contextSourceConnectionIds: [] };
}
@ -249,7 +249,7 @@ function listContextTargets(project: KloLocalProject): KloSetupContextTargets {
};
}
function missingCapabilities(project: KloLocalProject): string[] {
function missingCapabilities(project: KtxLocalProject): string[] {
const missing: string[] = [];
const llm = project.config.llm;
if (llm.provider.backend === 'none' || !llm.models.default) {
@ -374,8 +374,8 @@ async function verifyPrimarySourceScans(
return { ready: details.length === 0, details };
}
async function defaultVerifyContextReady(projectDir: string): Promise<KloSetupContextReadiness> {
const project = await loadKloProject({ projectDir });
async function defaultVerifyContextReady(projectDir: string): Promise<KtxSetupContextReadiness> {
const project = await loadKtxProject({ projectDir });
const targets = listContextTargets(project);
const primarySourceScans = await verifyPrimarySourceScans(projectDir, targets.primarySourceConnectionIds);
const semanticLayerContextReady = await hasFileWithExtension(
@ -411,17 +411,17 @@ async function defaultVerifyContextReady(projectDir: string): Promise<KloSetupCo
}
async function markContextComplete(projectDir: string): Promise<void> {
const project = await loadKloProject({ projectDir });
const project = await loadKtxProject({ projectDir });
await writeFile(
project.configPath,
serializeKloProjectConfig(markKloSetupStepComplete(project.config, 'context')),
serializeKtxProjectConfig(markKtxSetupStepComplete(project.config, 'context')),
'utf-8',
);
}
function writeBuildHeader(projectDir: string, runId: string, io: KloCliIo): void {
function writeBuildHeader(projectDir: string, runId: string, io: KtxCliIo): void {
const commands = contextBuildCommands(projectDir, runId);
io.stdout.write('\nKLO context build\n');
io.stdout.write('\nKTX context build\n');
io.stdout.write(`Run: ${runId}\n`);
io.stdout.write(`Project: ${resolve(projectDir)}\n\n`);
io.stdout.write('Detach: press d to leave this running.\n');
@ -429,8 +429,8 @@ function writeBuildHeader(projectDir: string, runId: string, io: KloCliIo): void
io.stdout.write(`Status: ${commands.status}\n\n`);
}
function writeMissingCapabilities(missing: string[], io: KloCliIo): void {
io.stderr.write('KLO cannot build agent-ready context yet.\n\n');
function writeMissingCapabilities(missing: string[], io: KtxCliIo): void {
io.stderr.write('KTX cannot build agent-ready context yet.\n\n');
io.stderr.write('Missing:\n');
for (const item of missing) {
io.stderr.write(` ${item}\n`);
@ -438,16 +438,16 @@ function writeMissingCapabilities(missing: string[], io: KloCliIo): void {
io.stderr.write('\nFix this in setup before building context.\n');
}
function writeSkippedContext(projectDir: string, io: KloCliIo): void {
io.stdout.write('\nKLO is configured, but context has not been built yet.\n\n');
io.stdout.write('Agents were not connected because KLO has not prepared searchable context for them.\n\n');
io.stdout.write(`Resume setup:\n klo setup --project-dir ${resolve(projectDir)}\n\n`);
io.stdout.write(`Build context directly:\n klo setup context build --project-dir ${resolve(projectDir)}\n\n`);
io.stdout.write(`Check status:\n klo status --project-dir ${resolve(projectDir)}\n`);
function writeSkippedContext(projectDir: string, io: KtxCliIo): void {
io.stdout.write('\nKTX is configured, but context has not been built yet.\n\n');
io.stdout.write('Agents were not connected because KTX has not prepared searchable context for them.\n\n');
io.stdout.write(`Resume setup:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
io.stdout.write(`Build context directly:\n ktx setup context build --project-dir ${resolve(projectDir)}\n\n`);
io.stdout.write(`Check status:\n ktx status --project-dir ${resolve(projectDir)}\n`);
}
function writeSuccess(readiness: KloSetupContextReadiness, targets: KloSetupContextTargets, io: KloCliIo): void {
io.stdout.write('\nKLO context is ready for agents.\n\n');
function writeSuccess(readiness: KtxSetupContextReadiness, targets: KtxSetupContextTargets, io: KtxCliIo): void {
io.stdout.write('\nKTX context is ready for agents.\n\n');
io.stdout.write('Primary sources:\n');
if (targets.primarySourceConnectionIds.length === 0) {
io.stdout.write(' none\n');
@ -469,19 +469,19 @@ function writeSuccess(readiness: KloSetupContextReadiness, targets: KloSetupCont
io.stdout.write(` Semantic search: ${readiness.semanticSearchReady ? 'ready' : 'not ready'}\n`);
}
function writeExistingContextSuccess(readiness: KloSetupContextReadiness, io: KloCliIo): void {
io.stdout.write('\nKLO context is ready for agents.\n\n');
function writeExistingContextSuccess(readiness: KtxSetupContextReadiness, io: KtxCliIo): void {
io.stdout.write('\nKTX context is ready for agents.\n\n');
io.stdout.write('Existing context artifacts were found from setup ingest.\n\n');
io.stdout.write('Verification:\n');
io.stdout.write(` Agent context: ${readiness.agentContextReady ? 'ready' : 'not ready'}\n`);
io.stdout.write(` Semantic search: ${readiness.semanticSearchReady ? 'ready' : 'not ready'}\n`);
}
async function promptForBuild(prompts: KloSetupContextPromptAdapter): Promise<'build' | 'skip' | 'back'> {
async function promptForBuild(prompts: KtxSetupContextPromptAdapter): Promise<'build' | 'skip' | 'back'> {
return (await prompts.select({
message:
'Build KLO context for agents?\n\n' +
'KLO is fully configured and ready to build context. This may take a few minutes to a few hours.',
'Build KTX context for agents?\n\n' +
'KTX is fully configured and ready to build context. This may take a few minutes to a few hours.',
options: [
{ value: 'build', label: 'Build context now (recommended)' },
{ value: 'skip', label: 'Leave context unbuilt and exit setup' },
@ -491,16 +491,16 @@ async function promptForBuild(prompts: KloSetupContextPromptAdapter): Promise<'b
}
async function runBuild(
args: KloSetupContextStepArgs,
io: KloCliIo,
deps: KloSetupContextDeps,
project: KloLocalProject,
targets: KloSetupContextTargets,
): Promise<KloSetupContextResult> {
args: KtxSetupContextStepArgs,
io: KtxCliIo,
deps: KtxSetupContextDeps,
project: KtxLocalProject,
targets: KtxSetupContextTargets,
): Promise<KtxSetupContextResult> {
const now = deps.now ?? (() => new Date());
const runId = deps.runIdFactory?.() ?? runIdFactory();
const startedAt = now().toISOString();
const runningState: KloSetupContextState = {
const runningState: KtxSetupContextState = {
runId,
status: 'running',
startedAt,
@ -512,7 +512,7 @@ async function runBuild(
retryableFailedTargets: [],
commands: contextBuildCommands(args.projectDir, runId),
};
await writeKloSetupContextState(args.projectDir, runningState);
await writeKtxSetupContextState(args.projectDir, runningState);
const contextBuild = deps.runContextBuild ?? runContextBuild;
const buildResult = await contextBuild(
@ -527,7 +527,7 @@ async function runBuild(
{
onDetach: () => {
const resolvedDir = resolve(args.projectDir);
mkdirSync(join(resolvedDir, '.klo', 'setup'), { recursive: true });
mkdirSync(join(resolvedDir, '.ktx', 'setup'), { recursive: true });
const detachedState = normalizeState(resolvedDir, {
...runningState,
status: 'detached',
@ -539,12 +539,12 @@ async function runBuild(
);
if (buildResult.detached) {
const updatedAt = now().toISOString();
await writeKloSetupContextState(args.projectDir, { ...runningState, status: 'detached', updatedAt });
await writeKtxSetupContextState(args.projectDir, { ...runningState, status: 'detached', updatedAt });
return { status: 'detached', projectDir: args.projectDir, runId };
}
if (buildResult.exitCode !== 0) {
const updatedAt = now().toISOString();
await writeKloSetupContextState(args.projectDir, {
await writeKtxSetupContextState(args.projectDir, {
...runningState,
status: 'failed',
updatedAt,
@ -557,14 +557,14 @@ async function runBuild(
const readiness = await (deps.verifyContextReady ?? defaultVerifyContextReady)(args.projectDir);
if (!readiness.ready) {
const updatedAt = now().toISOString();
await writeKloSetupContextState(args.projectDir, {
await writeKtxSetupContextState(args.projectDir, {
...runningState,
status: 'failed',
updatedAt,
retryableFailedTargets: readiness.failedTargets ?? [],
failureReason: readiness.details.join(' '),
});
io.stderr.write('KLO context build did not pass agent-readiness verification.\n');
io.stderr.write('KTX context build did not pass agent-readiness verification.\n');
for (const detail of readiness.details) {
io.stderr.write(` ${detail}\n`);
}
@ -573,7 +573,7 @@ async function runBuild(
await markContextComplete(project.projectDir);
const completedAt = now().toISOString();
await writeKloSetupContextState(args.projectDir, {
await writeKtxSetupContextState(args.projectDir, {
...runningState,
status: 'completed',
updatedAt: completedAt,
@ -585,11 +585,11 @@ async function runBuild(
}
async function completeExistingContext(
args: KloSetupContextStepArgs,
io: KloCliIo,
deps: KloSetupContextDeps,
targets: KloSetupContextTargets,
): Promise<KloSetupContextResult | null> {
args: KtxSetupContextStepArgs,
io: KtxCliIo,
deps: KtxSetupContextDeps,
targets: KtxSetupContextTargets,
): Promise<KtxSetupContextResult | null> {
const readiness = await (deps.verifyContextReady ?? defaultVerifyContextReady)(args.projectDir);
if (!readiness.ready) {
return null;
@ -599,7 +599,7 @@ async function completeExistingContext(
const completedAt = now().toISOString();
const runId = deps.runIdFactory?.() ?? runIdFactory();
await markContextComplete(args.projectDir);
await writeKloSetupContextState(args.projectDir, {
await writeKtxSetupContextState(args.projectDir, {
runId,
status: 'completed',
startedAt: completedAt,
@ -616,14 +616,14 @@ async function completeExistingContext(
return { status: 'ready', projectDir: args.projectDir, runId };
}
export async function runKloSetupContextStep(
args: KloSetupContextStepArgs,
io: KloCliIo,
deps: KloSetupContextDeps = {},
): Promise<KloSetupContextResult> {
export async function runKtxSetupContextStep(
args: KtxSetupContextStepArgs,
io: KtxCliIo,
deps: KtxSetupContextDeps = {},
): Promise<KtxSetupContextResult> {
try {
const project = await loadKloProject({ projectDir: args.projectDir });
const existingState = await readKloSetupContextState(args.projectDir);
const project = await loadKtxProject({ projectDir: args.projectDir });
const existingState = await readKtxSetupContextState(args.projectDir);
if (project.config.setup?.completed_steps.includes('context') === true && existingState.status === 'completed') {
return { status: 'ready', projectDir: args.projectDir, runId: existingState.runId ?? 'setup-context-completed' };
}
@ -646,7 +646,7 @@ export async function runKloSetupContextStep(
if (choice === 'status') {
const commands = contextBuildCommands(args.projectDir, existingState.runId);
io.stdout.write(`\nRun: ${commands.status}\n`);
io.stdout.write(`Log: ${join(resolve(args.projectDir), '.klo', 'setup', 'context-build.log')}\n`);
io.stdout.write(`Log: ${join(resolve(args.projectDir), '.ktx', 'setup', 'context-build.log')}\n`);
return { status: 'detached', projectDir: args.projectDir, runId: existingState.runId ?? '' };
}
if (choice === 'back') {
@ -659,7 +659,7 @@ export async function runKloSetupContextStep(
if (args.allowEmpty === true) {
return { status: 'skipped', projectDir: args.projectDir };
}
io.stderr.write('No primary or context sources are configured for a KLO context build.\n');
io.stderr.write('No primary or context sources are configured for a KTX context build.\n');
return { status: 'failed', projectDir: args.projectDir };
}
@ -694,16 +694,16 @@ export async function runKloSetupContextStep(
}
}
function stateMatchesRunId(state: KloSetupContextState, runId: string | undefined): boolean {
function stateMatchesRunId(state: KtxSetupContextState, runId: string | undefined): boolean {
return !runId || state.runId === runId;
}
function statusPayload(state: KloSetupContextState): KloSetupContextStatusSummary {
function statusPayload(state: KtxSetupContextState): KtxSetupContextStatusSummary {
return setupContextStatusFromState(state, { completedStep: state.status === 'completed' });
}
function writeContextStatus(state: KloSetupContextState, io: KloCliIo): void {
io.stdout.write(`KLO context built: ${state.status === 'completed' ? 'yes' : state.status.replaceAll('_', ' ')}\n`);
function writeContextStatus(state: KtxSetupContextState, io: KtxCliIo): void {
io.stdout.write(`KTX context built: ${state.status === 'completed' ? 'yes' : state.status.replaceAll('_', ' ')}\n`);
if (state.runId) {
io.stdout.write(`Run: ${state.runId}\n`);
io.stdout.write(`Watch: ${state.commands.watch}\n`);
@ -714,13 +714,13 @@ function writeContextStatus(state: KloSetupContextState, io: KloCliIo): void {
}
}
export async function runKloSetupContextCommand(
args: KloSetupContextCommandArgs,
io: KloCliIo,
deps: KloSetupContextDeps = {},
export async function runKtxSetupContextCommand(
args: KtxSetupContextCommandArgs,
io: KtxCliIo,
deps: KtxSetupContextDeps = {},
): Promise<number> {
if (args.command === 'build') {
const result = await runKloSetupContextStep(
const result = await runKtxSetupContextStep(
{ projectDir: args.projectDir, inputMode: args.inputMode, prompt: false },
io,
deps,
@ -728,9 +728,9 @@ export async function runKloSetupContextCommand(
return result.status === 'ready' || result.status === 'skipped' ? 0 : 1;
}
const state = await readKloSetupContextState(args.projectDir);
const state = await readKtxSetupContextState(args.projectDir);
if (!stateMatchesRunId(state, args.runId)) {
io.stderr.write(`KLO setup context run "${args.runId}" was not found.\n`);
io.stderr.write(`KTX setup context run "${args.runId}" was not found.\n`);
return 1;
}
@ -744,22 +744,22 @@ export async function runKloSetupContextCommand(
}
if (args.command === 'watch') {
io.stdout.write('KLO context build\n');
io.stdout.write('KTX context build\n');
writeContextStatus(state, io);
return 0;
}
const updatedAt = new Date().toISOString();
const nextState: KloSetupContextState = {
const nextState: KtxSetupContextState = {
...state,
status: state.status === 'completed' ? 'completed' : 'paused',
updatedAt,
};
await writeKloSetupContextState(args.projectDir, nextState);
await writeKtxSetupContextState(args.projectDir, nextState);
io.stdout.write(
state.status === 'completed'
? 'KLO context build already completed.\n'
: 'KLO context build pause requested. Resume with setup when ready.\n',
? 'KTX context build already completed.\n'
: 'KTX context build pause requested. Resume with setup when ready.\n',
);
return 0;
}

Some files were not shown because too many files have changed in this diff Show more