mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the `pnpm dead-code` script, document the `/** @internal */` convention for test-only exports in AGENTS.md, annotate test-only exports across the CLI with that JSDoc, and drop dead exports/wrappers the new gate surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`, `createLocalScanEnrichmentProvidersFromConfig`, `PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports). Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit production entries so cross-package barrel leaks are caught.
This commit is contained in:
parent
ac3885b652
commit
b690e6988b
71 changed files with 211 additions and 279 deletions
|
|
@ -41,8 +41,13 @@ repos:
|
|||
language: system
|
||||
pass_filenames: false
|
||||
- id: knip-dead-code
|
||||
name: knip dead-code check
|
||||
entry: pnpm exec knip --reporter compact
|
||||
name: knip dead-code (auto-fix)
|
||||
entry: pnpm exec knip --fix --reporter compact
|
||||
language: system
|
||||
pass_filenames: false
|
||||
- id: knip-dead-code-production
|
||||
name: knip dead-code (production mode)
|
||||
entry: pnpm exec knip --production --reporter compact
|
||||
language: system
|
||||
pass_filenames: false
|
||||
|
||||
|
|
|
|||
31
AGENTS.md
31
AGENTS.md
|
|
@ -166,7 +166,16 @@ pnpm run test 2>&1 | tee /tmp/ktx-test-output.log
|
|||
analysis. These checks are intentionally part of CI and pre-commit because the
|
||||
normal development workflow is agent-based.
|
||||
|
||||
- Run `pnpm run dead-code` after TypeScript changes.
|
||||
- `pnpm run dead-code` runs three checks: Biome (`dead-code:biome`), Knip
|
||||
default-mode (`dead-code:knip`), and Knip production-mode
|
||||
(`dead-code:knip:production`). All three must pass.
|
||||
- Default-mode Knip catches dead code reachable from no entry at all (broken
|
||||
graph). Production-mode Knip catches code reachable only via tests —
|
||||
i.e. code that's tested but doesn't ship.
|
||||
- Pre-commit runs `knip --fix` (auto-removes the `export` keyword from
|
||||
symbols that are exported but unused) plus `knip --production` (alerts on
|
||||
test-only paths). CI runs the same checks without `--fix` and fails on any
|
||||
finding.
|
||||
- Treat Knip findings as investigation prompts, not automatic deletion orders.
|
||||
- Remove private dead code when you confirm there are no imports, dynamic
|
||||
references, generated references, or tests that still need it.
|
||||
|
|
@ -177,6 +186,26 @@ normal development workflow is agent-based.
|
|||
- Update `knip.json` when adding dynamic entrypoints, generated files, package
|
||||
exports, CLI bins, or framework files that Knip cannot infer.
|
||||
|
||||
#### Internal exports for testability
|
||||
|
||||
When a function, type, or constant must be exported solely so a unit test can
|
||||
import it (i.e. it has no production cross-file consumer), annotate the
|
||||
declaration with `/** @internal */` JSDoc. Knip's production-mode check
|
||||
ignores `@internal` exports, so the convention keeps the gate clean without
|
||||
silencing the rest of the file.
|
||||
|
||||
```typescript
|
||||
/** @internal */
|
||||
export function reindexHasErrors(result: ReindexResult): boolean { ... }
|
||||
```
|
||||
|
||||
Do NOT use Vitest in-source testing (`if (import.meta.vitest)` blocks). Keep
|
||||
tests in separate `*.test.ts(x)` files.
|
||||
|
||||
If the only consumer of an export is its own test and the underlying behavior
|
||||
isn't used in production, delete both the export AND the test — testing dead
|
||||
code is still dead code.
|
||||
|
||||
### CLI Standards
|
||||
|
||||
- Use Commander for CLI command trees, arguments, options, help text, custom
|
||||
|
|
|
|||
72
knip.json
72
knip.json
|
|
@ -14,33 +14,25 @@
|
|||
},
|
||||
"packages/cli": {
|
||||
"entry": [
|
||||
"src/index.ts",
|
||||
"src/bin.ts",
|
||||
"src/llm/index.ts",
|
||||
"src/context/index.ts",
|
||||
"src/context/**/index.ts",
|
||||
"src/connectors/*/index.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"scripts/**/*.mjs"
|
||||
"src/llm/index.ts!",
|
||||
"src/context/**/index.ts!",
|
||||
"src/connectors/*/index.ts!",
|
||||
"src/print-command-tree.ts!",
|
||||
"src/**/*.test-utils.ts",
|
||||
"src/**/acceptance-fixtures.ts"
|
||||
],
|
||||
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.mjs", "vitest.config.ts"]
|
||||
"project": ["src/**/*.{ts,tsx}!", "scripts/**/*.mjs", "vitest.config.ts"]
|
||||
},
|
||||
"docs-site": {
|
||||
"entry": [
|
||||
"app/**/*.{ts,tsx}",
|
||||
"components/**/*.{ts,tsx}",
|
||||
"lib/**/*.{ts,tsx}",
|
||||
"middleware.ts",
|
||||
"next.config.mjs",
|
||||
"source.config.ts",
|
||||
"tests/**/*.mjs"
|
||||
"components/**/*.{ts,tsx}!",
|
||||
"source.config.ts!"
|
||||
],
|
||||
"project": [
|
||||
"app/**/*.{ts,tsx}",
|
||||
"components/**/*.{ts,tsx}",
|
||||
"lib/**/*.{ts,tsx}",
|
||||
"*.ts",
|
||||
"app/**/*.{ts,tsx}!",
|
||||
"components/**/*.{ts,tsx}!",
|
||||
"lib/**/*.{ts,tsx}!",
|
||||
"*.ts!",
|
||||
"*.mjs",
|
||||
"tests/**/*.mjs"
|
||||
],
|
||||
|
|
@ -51,41 +43,7 @@
|
|||
"**/dist/**",
|
||||
"**/coverage/**",
|
||||
"**/.next/**",
|
||||
"**/node_modules/**",
|
||||
"**/*.gen.ts",
|
||||
"**/*.generated.ts"
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"ignoreBinaries": ["uv", "lsof"],
|
||||
"ignoreIssues": {
|
||||
"packages/cli/src/clack.ts": ["exports"],
|
||||
"packages/cli/src/commands/connection-metabase-setup.ts": ["exports", "types"],
|
||||
"packages/cli/src/ingest.test-utils.ts": ["exports"],
|
||||
"packages/cli/src/io/symbols.ts": ["exports"],
|
||||
"packages/cli/src/managed-python-command.ts": ["types"],
|
||||
"packages/cli/src/managed-python-daemon.ts": ["types"],
|
||||
"packages/cli/src/managed-python-http.ts": ["exports", "types"],
|
||||
"packages/cli/src/managed-python-runtime.ts": ["types"],
|
||||
"packages/cli/src/memory-flow-tui.tsx": ["types"],
|
||||
"packages/cli/src/next-steps.ts": ["exports"],
|
||||
"packages/cli/src/print-command-tree.ts": ["exports"],
|
||||
"packages/cli/src/setup-agents.ts": ["exports", "types"],
|
||||
"packages/cli/src/setup-context.ts": ["types"],
|
||||
"packages/cli/src/setup-demo-tour.ts": ["exports"],
|
||||
"packages/cli/src/setup-models.ts": ["exports"],
|
||||
"packages/cli/src/setup-project.ts": ["types"],
|
||||
"packages/cli/src/setup-ready-menu.ts": ["types"],
|
||||
"packages/cli/src/setup-sources.ts": ["types"],
|
||||
"packages/cli/src/context/ingest/adapters/historic-sql/pattern-inputs.ts": ["exports", "types"],
|
||||
"packages/cli/src/context/ingest/adapters/lookml/pull-config.ts": ["exports"],
|
||||
"packages/cli/src/context/ingest/adapters/metabase/serialize-card.ts": ["types"],
|
||||
"packages/cli/src/context/ingest/adapters/metabase/types.ts": ["exports"],
|
||||
"packages/cli/src/context/ingest/adapters/metricflow/parse.ts": ["types"],
|
||||
"packages/cli/src/context/ingest/ports.ts": ["types"],
|
||||
"packages/cli/src/context/ingest/stages/stage-3-work-units.ts": ["types"],
|
||||
"packages/cli/src/context/ingest/stages/stage-index.types.ts": ["types"],
|
||||
"packages/cli/src/context/project/config.ts": ["types"],
|
||||
"packages/cli/src/context/scan/relationship-candidates.ts": ["types"],
|
||||
"packages/cli/src/context/scan/relationship-diagnostics.ts": ["types"],
|
||||
"packages/cli/src/context/tools/context-evidence-tool-store.ts": ["types"]
|
||||
}
|
||||
"ignoreBinaries": ["uv", "lsof"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,11 @@
|
|||
"artifacts:verify-manifest": "node scripts/package-artifacts.mjs verify-manifest",
|
||||
"build": "pnpm --filter './packages/*' run build",
|
||||
"check": "node scripts/check-boundaries.mjs && node --test scripts/*.test.mjs && pnpm --filter './packages/*' run build && pnpm --filter './packages/*' run test",
|
||||
"dead-code": "pnpm run dead-code:biome && pnpm run dead-code:knip",
|
||||
"dead-code": "pnpm run dead-code:biome && pnpm run dead-code:knip && pnpm run dead-code:knip:production",
|
||||
"dead-code:biome": "biome ci . --formatter-enabled=false --assist-enabled=false",
|
||||
"dead-code:fix": "biome check . --formatter-enabled=false --assist-enabled=false --write && knip --fix --format",
|
||||
"dead-code:knip": "knip --reporter compact",
|
||||
"dead-code:knip:production": "knip --production --reporter compact",
|
||||
"docs": "kill $(lsof -ti:3000) 2>/dev/null; pnpm --filter ktx-docs run dev",
|
||||
"ktx": "node scripts/run-ktx.mjs",
|
||||
"link:dev": "node scripts/link-dev-cli.mjs",
|
||||
|
|
|
|||
|
|
@ -55,10 +55,12 @@ function quotePlainValue(value: string): string {
|
|||
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function reindexHasErrors(summary: ReindexSummary): boolean {
|
||||
return summary.scopes.some((scope) => scope.error);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function renderReindexPlain(summary: ReindexSummary, io: KtxCliIo): void {
|
||||
const updateKey = summary.force ? 'rebuilt' : 'updated';
|
||||
for (const scope of summary.scopes) {
|
||||
|
|
@ -88,6 +90,7 @@ export function renderReindexPlain(summary: ReindexSummary, io: KtxCliIo): void
|
|||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function renderReindexJson(summary: ReindexSummary, io: KtxCliIo): void {
|
||||
io.stdout.write(`${JSON.stringify({ kind: 'reindex', data: summary, meta: { command: 'admin reindex' } }, null, 2)}\n`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export interface KtxCliPromptAdapter {
|
|||
spinner(): KtxCliSpinner;
|
||||
}
|
||||
|
||||
export class KtxCliPromptCancelledError extends Error {
|
||||
class KtxCliPromptCancelledError extends Error {
|
||||
constructor(message = 'Operation cancelled.') {
|
||||
super(message);
|
||||
this.name = 'KtxCliPromptCancelledError';
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { buildDefaultKtxProjectConfig, type KtxLocalProject, type KtxProjectConfig } from './context/project/index.js';
|
||||
import { loadKtxCliProject } from './cli-project.js';
|
||||
|
||||
function projectWithConfig(config: KtxProjectConfig): KtxLocalProject {
|
||||
return {
|
||||
projectDir: '/work/proj',
|
||||
configPath: '/work/proj/ktx.yaml',
|
||||
config,
|
||||
coreConfig: {} as KtxLocalProject['coreConfig'],
|
||||
git: {} as KtxLocalProject['git'],
|
||||
fileStore: {} as KtxLocalProject['fileStore'],
|
||||
};
|
||||
}
|
||||
|
||||
describe('loadKtxCliProject', () => {
|
||||
it('delegates to loadKtxProject and returns the project unchanged', async () => {
|
||||
const project = projectWithConfig(buildDefaultKtxProjectConfig());
|
||||
const loadProject = vi.fn(async () => project);
|
||||
|
||||
const result = await loadKtxCliProject({ projectDir: '/work/proj' }, { loadProject });
|
||||
|
||||
expect(result).toBe(project);
|
||||
expect(loadProject).toHaveBeenCalledWith({ projectDir: '/work/proj' });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { loadKtxProject, type KtxLocalProject } from './context/project/index.js';
|
||||
|
||||
export interface LoadKtxCliProjectOptions {
|
||||
projectDir: string;
|
||||
}
|
||||
|
||||
export interface LoadKtxCliProjectDeps {
|
||||
loadProject?: typeof loadKtxProject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin wrapper around `loadKtxProject`. Kept as a single entrypoint so the CLI can grow shared
|
||||
* pre-load behavior later (telemetry, project lock, etc.). Today it does no extra work.
|
||||
*/
|
||||
export async function loadKtxCliProject(
|
||||
options: LoadKtxCliProjectOptions,
|
||||
deps: LoadKtxCliProjectDeps = {},
|
||||
): Promise<KtxLocalProject> {
|
||||
return (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
|
||||
}
|
||||
|
|
@ -444,17 +444,20 @@ export function renderContextBuildView(
|
|||
const ESC_K_RE = new RegExp(`${ESC.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[K`, 'g');
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
/** @internal */
|
||||
export function extractProgressMessage(chunk: string): string | null {
|
||||
const cleaned = chunk.replace(/^\r/, '').replace(ESC_K_RE, '').replace(/\n$/, '').trim();
|
||||
const match = cleaned.match(/^\[(\d+)%\]\s*(.+)$/);
|
||||
return match ? `[${match[1]}%] ${match[2]}` : null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function parseScanSummary(output: string): string | null {
|
||||
const match = output.match(/(\d+) changes? across (\d+) tables?/);
|
||||
return match ? `${match[2]} tables` : null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function parseIngestSummary(output: string): string | null {
|
||||
const savedMemory = output.match(/Saved memory: (.+)/);
|
||||
if (savedMemory) return savedMemory[1];
|
||||
|
|
@ -560,6 +563,7 @@ function collectSourceProgress(targets: ContextBuildTargetState[]): ContextBuild
|
|||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function viewStateFromSourceProgress(
|
||||
sources: ContextBuildSourceProgressUpdate[],
|
||||
now: number,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { join, relative } from 'node:path';
|
|||
|
||||
const YAML_EXT_RE = /\.(ya?ml)$/i;
|
||||
|
||||
/** @internal */
|
||||
export function normalizeDbtPath(path: string): string {
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Buffer } from 'node:buffer';
|
||||
import type { StagedPatternsInput } from './types.js';
|
||||
|
||||
export const HISTORIC_SQL_PATTERN_WORKUNIT_DIR = 'patterns-input';
|
||||
const HISTORIC_SQL_PATTERN_WORKUNIT_DIR = 'patterns-input';
|
||||
/** @internal */
|
||||
export const HISTORIC_SQL_PATTERN_WORKUNIT_MAX_BYTES = 110_000;
|
||||
export const HISTORIC_SQL_PATTERN_WORKUNIT_PATH_RE = /^patterns-input\/part-\d{4}\.json$/;
|
||||
const HISTORIC_SQL_PATTERN_WORKUNIT_PATH_RE = /^patterns-input\/part-\d{4}\.json$/;
|
||||
|
||||
type PatternTemplate = StagedPatternsInput['templates'][number];
|
||||
|
||||
export interface HistoricSqlPatternInputShard {
|
||||
interface HistoricSqlPatternInputShard {
|
||||
path: string;
|
||||
input: StagedPatternsInput;
|
||||
byteLength: number;
|
||||
|
|
@ -27,10 +28,11 @@ export function isHistoricSqlPatternInputShardPath(path: string): boolean {
|
|||
return HISTORIC_SQL_PATTERN_WORKUNIT_PATH_RE.test(path);
|
||||
}
|
||||
|
||||
export function serializeStagedPatternsInput(input: StagedPatternsInput): string {
|
||||
function serializeStagedPatternsInput(input: StagedPatternsInput): string {
|
||||
return `${JSON.stringify(input, null, 2)}\n`;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function serializedStagedPatternsInputByteLength(input: StagedPatternsInput): number {
|
||||
return Buffer.byteLength(serializeStagedPatternsInput(input), 'utf-8');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ function tableSortKey(table: KtxTableRef): string {
|
|||
return `${table.catalog ?? ''}\u0000${table.db ?? ''}\u0000${table.name}`;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function liveDatabaseTablePath(table: KtxTableRef): string {
|
||||
return `${LIVE_DATABASE_TABLES_DIR}/${encodePathPart(table.catalog)}.${encodePathPart(table.db)}.${encodePathPart(
|
||||
table.name,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import type { ParsedTargetTable } from '../../parsed-target-table.js';
|
||||
import type { FetchContext } from '../../types.js';
|
||||
import { writeLookerEvidenceDocuments } from './evidence-documents.js';
|
||||
import { writeLookerFetchReport } from './fetch-report.js';
|
||||
import {
|
||||
type LookerPullConfig,
|
||||
type ParsedTargetTable,
|
||||
parseLookerPullConfig,
|
||||
STAGED_FILES,
|
||||
type StagedDashboardFile,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import type { ParsedTargetTable } from '../../parsed-target-table.js';
|
||||
import type { LookerWarehouseConnectionInfo } from './client.js';
|
||||
import type {
|
||||
LookerPullConfig,
|
||||
LookerRuntimeCursors,
|
||||
ParsedTargetTable,
|
||||
StagedExploreFile,
|
||||
StagedLookmlModelsFile,
|
||||
} from './types.js';
|
||||
import type { LookerPullConfig, LookerRuntimeCursors, StagedExploreFile, StagedLookmlModelsFile } from './types.js';
|
||||
|
||||
export const LOOKER_DIALECT_TO_CONNECTION_TYPE = {
|
||||
bigquery: 'BIGQUERY',
|
||||
|
|
|
|||
|
|
@ -1,17 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { buildLookerReconcileNotes, lookerRuntimeSourceToFileAdapterSource } from './reconcile.js';
|
||||
|
||||
describe('lookerRuntimeSourceToFileAdapterSource', () => {
|
||||
it('maps API-derived Looker source names to file-adapter source names', () => {
|
||||
expect(lookerRuntimeSourceToFileAdapterSource('looker__b2b__sales_pipeline')).toBe('b2b__sales_pipeline');
|
||||
expect(lookerRuntimeSourceToFileAdapterSource('looker__finance__orders')).toBe('finance__orders');
|
||||
});
|
||||
|
||||
it('ignores non-Looker and malformed source names', () => {
|
||||
expect(lookerRuntimeSourceToFileAdapterSource('b2b__sales_pipeline')).toBeNull();
|
||||
expect(lookerRuntimeSourceToFileAdapterSource('looker__missing_explore')).toBeNull();
|
||||
});
|
||||
});
|
||||
import { buildLookerReconcileNotes } from './reconcile.js';
|
||||
|
||||
describe('buildLookerReconcileNotes', () => {
|
||||
it('instructs reconciliation to record subsumed provenance', () => {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,3 @@
|
|||
export function lookerRuntimeSourceToFileAdapterSource(sourceName: string): string | null {
|
||||
if (!sourceName.startsWith('looker__')) {
|
||||
return null;
|
||||
}
|
||||
const stripped = sourceName.slice('looker__'.length);
|
||||
const parts = stripped.split('__');
|
||||
if (parts.length < 2 || parts.some((part) => part.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
const [model, ...exploreParts] = parts;
|
||||
return `${model}__${exploreParts.join('__')}`;
|
||||
}
|
||||
|
||||
export function buildLookerReconcileNotes(): string[] {
|
||||
return [
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import type { ToolOutput } from '../../../../tools/index.js';
|
||||
import { type ParsedTargetTable, stagedLookerQuerySchema } from '../types.js';
|
||||
import type { ParsedTargetTable } from '../../../parsed-target-table.js';
|
||||
import { stagedLookerQuerySchema } from '../types.js';
|
||||
|
||||
const lookerUsageInputSchema = z.object({
|
||||
queryCount30d: z.number().int().nonnegative().default(0),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { parsedTargetTableSchema } from '../../parsed-target-table.js';
|
||||
import {
|
||||
lookerPullConfigSchema,
|
||||
parseLookerPullConfig,
|
||||
parsedTargetTableSchema,
|
||||
stagedDashboardFileSchema,
|
||||
stagedExploreFileSchema,
|
||||
stagedLookerFetchIssueSchema,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ const nullableLookerIdSchema = z.union([lookerIdSchema, z.null()]).default(null)
|
|||
|
||||
export const lookerConnectionIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/);
|
||||
|
||||
export { parsedTargetTableSchema, type ParsedTargetTable } from '../../parsed-target-table.js';
|
||||
|
||||
export const lookerRuntimeCursorsSchema = z.object({
|
||||
dashboardsLastSyncedAt: z.iso.datetime().nullable().default(null),
|
||||
looksLastSyncedAt: z.iso.datetime().nullable().default(null),
|
||||
|
|
@ -16,6 +14,7 @@ export const lookerRuntimeCursorsSchema = z.object({
|
|||
|
||||
export type LookerRuntimeCursors = z.infer<typeof lookerRuntimeCursorsSchema>;
|
||||
|
||||
/** @internal */
|
||||
export const lookerPullConfigSchema = z.object({
|
||||
lookerConnectionId: lookerConnectionIdSchema.optional(),
|
||||
instanceBaseUrl: z.url().optional(),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import * as z from 'zod';
|
|||
import type { SourceFetchReport } from '../../types.js';
|
||||
import type { ParsedLookmlProject } from './parse.js';
|
||||
|
||||
/** @internal */
|
||||
export const LOOKML_FETCH_REPORT_FILE = 'lookml-fetch-report.json';
|
||||
/** @internal */
|
||||
export const LOOKML_MISMATCHED_MODELS_FILE = 'lookml-mismatched-models.json';
|
||||
|
||||
const fetchIssueKindSchema = z.enum([
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as z from 'zod';
|
||||
import { parsedTargetTableSchema } from '../../parsed-target-table.js';
|
||||
|
||||
export const lookmlPullConfigSchema = z.object({
|
||||
const lookmlPullConfigSchema = z.object({
|
||||
repoUrl: z.string().url(),
|
||||
branch: z.string().default('main'),
|
||||
path: z.string().nullable().default(null),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const CARD_REF_RE = /\{\{#(\d+)\}\}/g;
|
|||
* Input TemplateTag shape mirrors `MetabaseClient.getTemplateTags` output. We keep the
|
||||
* shape loose — only `name`, `type`, and optional `cardReference`/`default` are needed here.
|
||||
*/
|
||||
/** @internal */
|
||||
export interface InputTemplateTag {
|
||||
name: string;
|
||||
type: string;
|
||||
|
|
@ -13,6 +14,7 @@ export interface InputTemplateTag {
|
|||
defaultValue?: string | null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function extractReferencedCardIds(templateTags: InputTemplateTag[], sql: string): number[] {
|
||||
const ids = new Set<number>();
|
||||
for (const tag of templateTags) {
|
||||
|
|
@ -34,7 +36,7 @@ export function extractReferencedCardIds(templateTags: InputTemplateTag[], sql:
|
|||
* care about. The adapter reads whatever the client returns; this helper stays
|
||||
* duck-typed so the client's type can evolve without churn here.
|
||||
*/
|
||||
export interface InputCard {
|
||||
interface InputCard {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const metabaseSyncModeSchema = z.enum(['ALL', 'ONLY', 'EXCEPT']);
|
||||
const metabaseSyncModeSchema = z.enum(['ALL', 'ONLY', 'EXCEPT']);
|
||||
export type MetabaseSyncMode = z.infer<typeof metabaseSyncModeSchema>;
|
||||
|
||||
export const metabaseLocalConnectionIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/);
|
||||
|
|
@ -25,7 +25,7 @@ export function parseMetabasePullConfig(raw: unknown): MetabasePullConfig {
|
|||
}
|
||||
|
||||
/** A Metabase column from `card.result_metadata`. Mirrors what the LLM consumes today. */
|
||||
export const stagedResultColumnSchema = z.object({
|
||||
const stagedResultColumnSchema = z.object({
|
||||
name: z.string(),
|
||||
display_name: z.string().optional().nullable(),
|
||||
base_type: z.string(),
|
||||
|
|
@ -37,7 +37,7 @@ export const stagedResultColumnSchema = z.object({
|
|||
|
||||
export type StagedResultColumn = z.infer<typeof stagedResultColumnSchema>;
|
||||
|
||||
export const stagedParameterSchema = z.object({
|
||||
const stagedParameterSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
|
|
@ -49,7 +49,7 @@ export const stagedParameterSchema = z.object({
|
|||
export type StagedParameter = z.infer<typeof stagedParameterSchema>;
|
||||
|
||||
/** A template tag pulled from an MBQL card's `dataset_query.stages[0].template-tags`. */
|
||||
export const stagedTemplateTagSchema = z.object({
|
||||
const stagedTemplateTagSchema = z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
defaultValue: z.string().optional().nullable(),
|
||||
|
|
@ -87,7 +87,7 @@ export const stagedCardFileSchema = z.object({
|
|||
export type StagedCardFile = z.infer<typeof stagedCardFileSchema>;
|
||||
|
||||
/** A serialized collection file, `collections/<id>.json`. Minimal — path lives on the card. */
|
||||
export const stagedCollectionFileSchema = z.object({
|
||||
const stagedCollectionFileSchema = z.object({
|
||||
metabaseId: z.union([z.number().int(), z.literal('root')]),
|
||||
name: z.string(),
|
||||
parentId: z.union([z.number().int(), z.literal('root')]).nullable(),
|
||||
|
|
@ -96,7 +96,7 @@ export const stagedCollectionFileSchema = z.object({
|
|||
export type StagedCollectionFile = z.infer<typeof stagedCollectionFileSchema>;
|
||||
|
||||
/** A serialized database-mapping snapshot, `databases/<id>.json`. */
|
||||
export const stagedDatabaseFileSchema = z.object({
|
||||
const stagedDatabaseFileSchema = z.object({
|
||||
metabaseDatabaseId: z.number().int().positive(),
|
||||
metabaseDatabaseName: z.string(),
|
||||
metabaseEngine: z.string().nullable(),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { readdir, readFile } from 'node:fs/promises';
|
|||
import { join, relative } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
|
||||
export interface ParsedMetricFlowSemanticModel {
|
||||
interface ParsedMetricFlowSemanticModel {
|
||||
/** Path relative to stagedDir, e.g. "models/orders.yml". */
|
||||
path: string;
|
||||
/** `name:` on the semantic_model. */
|
||||
|
|
@ -24,9 +24,9 @@ export interface ParsedMetricFlowSemanticModel {
|
|||
defaultTimeDimension: string | null;
|
||||
}
|
||||
|
||||
export type MetricFlowMetricType = 'simple' | 'derived' | 'cumulative' | 'ratio' | 'conversion';
|
||||
type MetricFlowMetricType = 'simple' | 'derived' | 'cumulative' | 'ratio' | 'conversion';
|
||||
|
||||
export interface ParsedMetricFlowMetric {
|
||||
interface ParsedMetricFlowMetric {
|
||||
path: string;
|
||||
name: string;
|
||||
type: MetricFlowMetricType;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { kmeans, pickK } from '../../clustering/kmeans.js';
|
|||
import type { WorkUnit } from '../../types.js';
|
||||
import { notionMetadataSchema } from './types.js';
|
||||
|
||||
/** @internal */
|
||||
export const MIN_PAGES_TO_CLUSTER = 5;
|
||||
const CLUSTER_TEXT_BODY_CHARS = 1024;
|
||||
const CLUSTER_SEED = 42;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ function richTextToMarkdown(value: unknown): string {
|
|||
.trim();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function propertyValueToText(value: unknown): string {
|
||||
if (!value || typeof value !== 'object' || !('type' in value)) {
|
||||
return '';
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ function shouldRetryNotionError(error: unknown): boolean {
|
|||
return code === 'rate_limited' || transientErrorCodes.has(code ?? '') || transientStatusCodes.has(status ?? 0);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function retryNotionRequest<T>(operation: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
||||
const maxAttempts = options.maxAttempts ?? 4;
|
||||
const sleep = options.sleep ?? defaultSleep;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { KtxModelRole } from '../../llm/index.js';
|
|||
import type { KtxEmbeddingPort } from '../core/embedding.js';
|
||||
import type { GitService, KtxFileStorePort, KtxLogger, SessionOutcome } from '../core/index.js';
|
||||
import type { AgentRunnerPort, KtxLlmRuntimePort, KtxRuntimeToolSet } from '../llm/index.js';
|
||||
import type { CaptureSession, MemoryAction, MemoryKnowledgeSlRefsPort } from '../memory/index.js';
|
||||
import type { MemoryAction, MemoryKnowledgeSlRefsPort } from '../memory/index.js';
|
||||
import type { PromptService } from '../prompts/index.js';
|
||||
import type { SkillsRegistryService } from '../skills/index.js';
|
||||
import type {
|
||||
|
|
@ -34,7 +34,7 @@ import type {
|
|||
SourceAdapter,
|
||||
} from './types.js';
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
type JsonPrimitive = string | number | boolean | null;
|
||||
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue | undefined };
|
||||
|
||||
export interface IngestRunRecord {
|
||||
|
|
@ -359,9 +359,4 @@ export interface IngestBundleRunnerDeps {
|
|||
logger?: KtxLogger;
|
||||
}
|
||||
|
||||
export interface IngestCaptureState {
|
||||
session: CaptureSession;
|
||||
actions: MemoryAction[];
|
||||
}
|
||||
|
||||
export type IngestRunnerJob = IngestBundleJob;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export interface SemanticLayerTargetPolicyInput {
|
|||
allowedConnectionIds: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface SemanticLayerTargetPolicyViolation {
|
||||
path: string;
|
||||
connectionId: string;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { WorkUnit } from '../types.js';
|
|||
|
||||
const MAX_WORK_UNIT_PROMPT_CHARS = 240_000;
|
||||
|
||||
export interface TouchedValidationResult {
|
||||
interface TouchedValidationResult {
|
||||
invalidSources: string[];
|
||||
validSources: string[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { MemoryAction } from '../../memory/index.js';
|
||||
import type { TouchedSlSource } from '../../tools/index.js';
|
||||
|
||||
export interface StageIndexWorkUnit {
|
||||
interface StageIndexWorkUnit {
|
||||
unitKey: string;
|
||||
rawFiles: string[];
|
||||
status: 'success' | 'failed';
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ export interface KtxSqlExecutionResponse {
|
|||
rowCount: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface KtxSqlExecutionMcpPort {
|
||||
execute(
|
||||
input: { connectionId: string; sql: string; maxRows: number },
|
||||
|
|
|
|||
|
|
@ -256,14 +256,10 @@ const ktxProjectConfigSchema = z
|
|||
|
||||
export type KtxProjectConfig = z.infer<typeof ktxProjectConfigSchema>;
|
||||
export type KtxProjectLlmConfig = z.infer<typeof llmSchema>;
|
||||
export type KtxProjectLlmProviderConfig = z.infer<typeof llmProviderSchema>;
|
||||
export type KtxProjectEmbeddingConfig = z.infer<typeof embeddingSchema>;
|
||||
export type KtxScanEnrichmentConfig = z.infer<typeof scanEnrichmentSchema>;
|
||||
export type KtxIngestWorkUnitsConfig = z.infer<typeof workUnitsSchema>;
|
||||
export type KtxScanRelationshipConfig = z.infer<typeof scanRelationshipsSchema>;
|
||||
export type KtxProjectScanConfig = z.infer<typeof scanSchema>;
|
||||
export type KtxProjectConnectionConfig = z.infer<typeof connectionSchema>;
|
||||
export type KtxProjectSetupConfig = z.infer<typeof setupSchema>;
|
||||
export type KtxStorageState = z.infer<typeof storageSchema>['state'];
|
||||
export type KtxSearchBackend = z.infer<typeof storageSchema>['search'];
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
runLocalScanEnrichment,
|
||||
snapshotToKtxEnrichedSchema,
|
||||
} from './local-enrichment.js';
|
||||
import { createLocalScanEnrichmentProvidersFromConfig } from './local-scan.js';
|
||||
import {
|
||||
createKtxConnectorCapabilities,
|
||||
type KtxQueryResult,
|
||||
|
|
@ -813,46 +812,4 @@ describe('local scan enrichment', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('resolves gateway LLM providers and passes injected embedding provider through to scan enrichment', () => {
|
||||
const createKtxLlmProvider = vi.fn(() => ({
|
||||
getModel: vi.fn().mockReturnValue({ modelId: 'provider/language-model', provider: 'gateway' }),
|
||||
}));
|
||||
const embeddingProvider = {
|
||||
dimensions: 1536,
|
||||
maxBatchSize: 8,
|
||||
embed: vi.fn(),
|
||||
[['embed', 'Many'].join('')]: vi.fn(),
|
||||
};
|
||||
|
||||
const providers = createLocalScanEnrichmentProvidersFromConfig(
|
||||
{
|
||||
mode: 'llm',
|
||||
embeddings: {
|
||||
backend: 'openai',
|
||||
model: 'provider/embedding-model',
|
||||
dimensions: 1536,
|
||||
batchSize: 8,
|
||||
openai: { api_key: 'env:OPENAI_API_KEY' }, // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: {
|
||||
backend: 'gateway',
|
||||
gateway: {},
|
||||
},
|
||||
models: { default: 'provider/language-model' },
|
||||
},
|
||||
{
|
||||
createKtxLlmProvider: createKtxLlmProvider as any,
|
||||
env: { OPENAI_API_KEY: 'openai-key' }, // pragma: allowlist secret
|
||||
embeddingProvider: embeddingProvider as any,
|
||||
},
|
||||
);
|
||||
|
||||
expect(providers?.embedding?.dimensions).toBe(1536);
|
||||
expect(providers?.embedding?.maxBatchSize).toBe(8);
|
||||
expect(createKtxLlmProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ backend: 'gateway', modelSlots: { default: 'provider/language-model' } }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -226,15 +226,6 @@ function resolveLocalScanEnrichmentProviders(
|
|||
};
|
||||
}
|
||||
|
||||
export function createLocalScanEnrichmentProvidersFromConfig(
|
||||
config: KtxScanEnrichmentConfig,
|
||||
llmConfig: KtxProjectLlmConfig,
|
||||
deps: LocalScanEnrichmentProviderDeps = {},
|
||||
): KtxLocalScanEnrichmentProviders | null {
|
||||
const resolved = resolveLocalScanEnrichmentProviders(config, llmConfig, deps);
|
||||
return resolved.status === 'ready' ? resolved.providers : null;
|
||||
}
|
||||
|
||||
function createLocalScanEnrichmentStateStore(options: RunLocalScanOptions): SqliteLocalScanEnrichmentStateStore | null {
|
||||
if (options.dryRun) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
pluralizeKtxRelationshipToken,
|
||||
singularizeKtxRelationshipToken,
|
||||
} from './relationship-name-similarity.js';
|
||||
export type { KtxRelationshipNormalizedName } from './relationship-name-similarity.js';
|
||||
;
|
||||
export { normalizeKtxRelationshipName } from './relationship-name-similarity.js';
|
||||
import type { KtxRelationshipProfileArtifact } from './relationship-profiling.js';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export interface KtxRelationshipDiagnosticsThresholds {
|
|||
reviewThreshold: number;
|
||||
}
|
||||
|
||||
export interface KtxRelationshipDiagnosticsPolicy {
|
||||
interface KtxRelationshipDiagnosticsPolicy {
|
||||
validationRequiredForManifest: boolean;
|
||||
maxCandidatesPerColumn: number;
|
||||
profileSampleRows: number;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { tmpdir } from 'node:os';
|
|||
import { join } from 'node:path';
|
||||
import { Client } from 'pg';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { assertSearchBackendCapabilities, assertSearchBackendConformanceCase } from './index.js';
|
||||
import { KtxPGliteOwnerProcess, PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES } from './pglite-owner-process.js';
|
||||
import { assertSearchBackendConformanceCase } from './index.js';
|
||||
import { KtxPGliteOwnerProcess } from './pglite-owner-process.js';
|
||||
|
||||
async function allocatePort(): Promise<number> {
|
||||
const server = createServer();
|
||||
|
|
@ -107,20 +107,6 @@ describe('KtxPGliteOwnerProcess', () => {
|
|||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('declares the advanced PGlite search capabilities observed by the spike', () => {
|
||||
assertSearchBackendCapabilities({
|
||||
backendName: 'pglite-owner-process',
|
||||
capabilities: PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES,
|
||||
expected: {
|
||||
fts: true,
|
||||
vector: true,
|
||||
fuzzy: true,
|
||||
jsonSearch: true,
|
||||
arraySearch: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('starts a socket owner process and serves PostgreSQL clients', async () => {
|
||||
const owner = await KtxPGliteOwnerProcess.start({
|
||||
dataDir,
|
||||
|
|
|
|||
|
|
@ -3,15 +3,6 @@ import { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm';
|
|||
import { vector } from '@electric-sql/pglite/vector';
|
||||
import { PGLiteSocketServer } from '@electric-sql/pglite-socket';
|
||||
import { Client, type ClientConfig, type QueryResult, type QueryResultRow } from 'pg';
|
||||
import type { SearchBackendCapabilities } from './types.js';
|
||||
|
||||
export const PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES = {
|
||||
fts: true,
|
||||
vector: true,
|
||||
fuzzy: true,
|
||||
jsonSearch: true,
|
||||
arraySearch: false,
|
||||
} satisfies SearchBackendCapabilities;
|
||||
|
||||
export interface KtxPGliteOwnerProcessOptions {
|
||||
dataDir: string;
|
||||
|
|
|
|||
|
|
@ -25,8 +25,11 @@ export interface LoadAllSourcesResult {
|
|||
loadErrors: string[];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class UnknownColumnOverrideError extends Error {}
|
||||
/** @internal */
|
||||
export class ColumnNameCollisionError extends Error {}
|
||||
/** @internal */
|
||||
export class ConflictingExcludeAndOverrideError extends Error {}
|
||||
class ComposeContractError extends Error {}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export interface ContextEvidenceSearchArgs {
|
|||
|
||||
export type ContextEvidenceSearchMatchReason = 'lexical' | 'semantic' | 'token' | (string & {});
|
||||
|
||||
export interface ContextEvidenceSearchLaneSummary {
|
||||
interface ContextEvidenceSearchLaneSummary {
|
||||
lane: string;
|
||||
status: 'available' | 'skipped' | 'failed';
|
||||
requestedCandidatePoolLimit: number;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ interface WikiSearchStructured {
|
|||
totalFound: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface WikiSearchAdapterPort {
|
||||
search(input: { userId: string; query: string; limit: number }): Promise<{
|
||||
results: Array<{
|
||||
|
|
|
|||
|
|
@ -17,8 +17,11 @@ interface EnsureDemoProjectOptions {
|
|||
force: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const DEMO_CONNECTION_ID = 'orbit_demo';
|
||||
/** @internal */
|
||||
export const DEMO_ADAPTER = 'live-database';
|
||||
/** @internal */
|
||||
export const DEMO_REPLAY_FILE = 'replay.memory-flow.v1.json';
|
||||
|
||||
const REQUIRED_PACKAGED_BASE_ASSET_PATHS = ['demo.db', 'manifest.json', DEMO_REPLAY_FILE] as const;
|
||||
|
|
@ -115,6 +118,7 @@ async function assertPackagedSeededAssetsPresent(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function ensureDemoProject(options: EnsureDemoProjectOptions): Promise<DemoProjectResult> {
|
||||
const projectDir = resolve(options.projectDir);
|
||||
const configPath = join(projectDir, 'ktx.yaml');
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ export function formatDuration(ms: number): string {
|
|||
return `${hr}h${(min % 60).toString().padStart(2, '0')}m`;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function formatEta(ms: number | null, status: MemoryFlowReplayInput['status']): string {
|
||||
if (status !== 'running') return 'done';
|
||||
if (ms === null) return 'estimating...';
|
||||
|
|
@ -153,6 +154,7 @@ export function formatCost(usd: number): string {
|
|||
return `$${usd.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function formatTokens(n: number): string {
|
||||
if (!Number.isFinite(n) || n <= 0) return '0';
|
||||
if (n < 1000) return `${Math.round(n)}`;
|
||||
|
|
@ -160,6 +162,7 @@ export function formatTokens(n: number): string {
|
|||
return `${(n / 1_000_000).toFixed(2)}M`;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function formatTokensPerSec(n: number): string {
|
||||
if (!Number.isFinite(n) || n <= 0) return '0/s';
|
||||
if (n < 1000) return `${Math.round(n)}/s`;
|
||||
|
|
@ -167,6 +170,7 @@ export function formatTokensPerSec(n: number): string {
|
|||
}
|
||||
|
||||
const PROGRESS_BAR_WIDTH = 12;
|
||||
/** @internal */
|
||||
export function progressBar(ratio: number, width: number = PROGRESS_BAR_WIDTH): string {
|
||||
const clamped = Math.max(0, Math.min(1, ratio));
|
||||
const filled = Math.round(clamped * width);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ function writeLine(io: KtxCliIo, message: string): void {
|
|||
io.stderr.write(message.endsWith('\n') ? message : `${message}\n`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createNoopOperationalLogger(): KtxOperationalLogger {
|
||||
return {
|
||||
log: () => undefined,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface KtxMcpDaemonState {
|
|||
logPath: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface KtxMcpDaemonChild {
|
||||
pid?: number;
|
||||
unref(): void;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export interface ManagedPythonCommandRuntime {
|
|||
manifest: InstalledKtxRuntimeManifest;
|
||||
}
|
||||
|
||||
export interface ManagedPythonCommandDeps {
|
||||
interface ManagedPythonCommandDeps {
|
||||
readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeStatus>;
|
||||
installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise<ManagedPythonRuntimeInstallResult>;
|
||||
confirmInstall?: (message: string, io: KtxCliIo) => Promise<boolean>;
|
||||
|
|
@ -51,6 +51,7 @@ export interface ManagedPythonSemanticLayerComputeOptions extends ManagedPythonC
|
|||
createPythonCompute?: typeof createPythonSemanticLayerComputePort;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function managedRuntimeInstallCommand(feature: KtxRuntimeFeature): string {
|
||||
return feature === 'local-embeddings'
|
||||
? 'ktx admin runtime install --feature local-embeddings --yes'
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export interface ManagedPythonDaemonProcessInfo {
|
|||
command: string;
|
||||
}
|
||||
|
||||
export type ManagedPythonDaemonStopAllSource = 'state' | 'process';
|
||||
type ManagedPythonDaemonStopAllSource = 'state' | 'process';
|
||||
|
||||
export interface ManagedPythonDaemonStopAllEntry {
|
||||
pid: number;
|
||||
|
|
@ -74,11 +74,13 @@ export interface ManagedPythonDaemonStopAllResult {
|
|||
scanErrors: string[];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface ManagedPythonDaemonChild {
|
||||
pid?: number;
|
||||
unref(): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type ManagedPythonDaemonSpawn = (
|
||||
command: string,
|
||||
args: string[],
|
||||
|
|
@ -89,6 +91,7 @@ export type ManagedPythonDaemonSpawn = (
|
|||
},
|
||||
) => ManagedPythonDaemonChild;
|
||||
|
||||
/** @internal */
|
||||
export type ManagedPythonDaemonFetch = (
|
||||
url: string,
|
||||
) => Promise<{
|
||||
|
|
@ -98,7 +101,7 @@ export type ManagedPythonDaemonFetch = (
|
|||
text(): Promise<string>;
|
||||
}>;
|
||||
|
||||
export type ManagedPythonDaemonKillProcess = (pid: number, signal?: NodeJS.Signals) => void;
|
||||
type ManagedPythonDaemonKillProcess = (pid: number, signal?: NodeJS.Signals) => void;
|
||||
|
||||
export interface ManagedPythonDaemonStartOptions extends ManagedPythonDaemonLayoutOptions {
|
||||
features: KtxRuntimeFeature[];
|
||||
|
|
|
|||
|
|
@ -21,12 +21,13 @@ import {
|
|||
} from './managed-python-command.js';
|
||||
import { startManagedPythonDaemon, type ManagedPythonDaemonStartResult } from './managed-python-daemon.js';
|
||||
|
||||
/** @internal */
|
||||
export type ManagedPythonHttpJsonRunner = (
|
||||
path: string,
|
||||
payload: Record<string, unknown>,
|
||||
) => Promise<Record<string, unknown>>;
|
||||
|
||||
export type ManagedPythonHttpPostJson = (
|
||||
type ManagedPythonHttpPostJson = (
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
payload: Record<string, unknown>,
|
||||
|
|
@ -75,7 +76,7 @@ function parseJsonObject(raw: string, path: string): Record<string, unknown> {
|
|||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export async function postManagedDaemonJson(
|
||||
async function postManagedDaemonJson(
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
payload: Record<string, unknown>,
|
||||
|
|
@ -117,6 +118,7 @@ export async function postManagedDaemonJson(
|
|||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createManagedPythonDaemonBaseUrlResolver(
|
||||
options: ManagedPythonCoreDaemonOptions,
|
||||
): () => Promise<string> {
|
||||
|
|
@ -158,6 +160,7 @@ function isResolveBaseUrlOnly(
|
|||
return 'resolveBaseUrl' in options;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createManagedDaemonHttpJsonRunner(
|
||||
options: ManagedPythonDaemonHttpOptions,
|
||||
): ManagedPythonHttpJsonRunner {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const runtimeAssetManifestSchema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
export type KtxRuntimeAssetManifest = z.infer<typeof runtimeAssetManifestSchema>;
|
||||
type KtxRuntimeAssetManifest = z.infer<typeof runtimeAssetManifestSchema>;
|
||||
|
||||
const installedRuntimeManifestSchema = z.object({
|
||||
schemaVersion: z.literal(1),
|
||||
|
|
@ -76,6 +76,7 @@ export interface ManagedPythonDaemonLayout extends ManagedPythonRuntimeLayout {
|
|||
daemonStderrPath: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface ManagedRuntimeAsset {
|
||||
manifest: KtxRuntimeAssetManifest;
|
||||
wheelPath: string;
|
||||
|
|
@ -104,7 +105,7 @@ export interface ManagedPythonRuntimeInstallResult {
|
|||
manifest: InstalledKtxRuntimeManifest;
|
||||
}
|
||||
|
||||
export type ManagedPythonRuntimeStatusKind = 'missing' | 'ready' | 'mismatched' | 'broken';
|
||||
type ManagedPythonRuntimeStatusKind = 'missing' | 'ready' | 'mismatched' | 'broken';
|
||||
|
||||
export interface ManagedPythonRuntimeStatus {
|
||||
kind: ManagedPythonRuntimeStatusKind;
|
||||
|
|
@ -121,6 +122,7 @@ export interface ManagedPythonRuntimeDoctorCheck {
|
|||
fix?: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const MISSING_UV_RUNTIME_INSTALL_MESSAGE =
|
||||
'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx admin runtime install --yes';
|
||||
|
||||
|
|
@ -142,6 +144,7 @@ function executablePath(venvDir: string, platform: NodeJS.Platform, name: string
|
|||
return join(venvDir, 'bin', name);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function managedPythonRuntimeLayout(options: ManagedPythonRuntimeLayoutOptions): ManagedPythonRuntimeLayout {
|
||||
const platform = options.platform ?? process.platform;
|
||||
const env = options.env ?? process.env;
|
||||
|
|
@ -235,6 +238,7 @@ function parseRequiresPythonFromWheel(input: { wheelPath: string; contents: Buff
|
|||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function verifyRuntimeAsset(input: { assetDir: string }): Promise<ManagedRuntimeAsset> {
|
||||
const manifestPath = join(input.assetDir, 'manifest.json');
|
||||
let manifestData: unknown;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export interface McpSecurityConfig {
|
|||
allowedOrigins: string[];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type McpAuthorizationResult =
|
||||
| { ok: true }
|
||||
| { ok: false; status: 401 | 403; message: string };
|
||||
|
|
@ -34,6 +35,7 @@ function isLoopbackHost(host: string): boolean {
|
|||
return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function normalizeHostHeader(value: string): string {
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
if (trimmed.startsWith('[')) {
|
||||
|
|
@ -85,6 +87,7 @@ function headerValue(headers: IncomingHttpHeaders | Record<string, string | unde
|
|||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isMcpRequestAuthorized(
|
||||
request: { path: string; headers: IncomingHttpHeaders | Record<string, string | undefined> },
|
||||
config: McpSecurityConfig,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ function defaultPrepareKeypressEvents(stdin: KtxMemoryFlowStdin): void {
|
|||
emitKeypressEvents(stdin as Parameters<typeof emitKeypressEvents>[0]);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function memoryFlowCommandForKey(
|
||||
chunk: string,
|
||||
search: MemoryFlowInteractionState['search'],
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export interface MemoryFlowTuiLiveSession {
|
|||
isClosed(): boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface MemoryFlowInkInstance {
|
||||
rerender(tree: ReactNode): void;
|
||||
unmount(): void;
|
||||
|
|
@ -73,7 +74,7 @@ export interface MemoryFlowInkInstance {
|
|||
clear?(): void;
|
||||
}
|
||||
|
||||
export interface MemoryFlowInkRenderOptions {
|
||||
interface MemoryFlowInkRenderOptions {
|
||||
stdin?: KtxMemoryFlowTuiIo['stdin'];
|
||||
stdout: KtxMemoryFlowTuiIo['stdout'];
|
||||
stderr: KtxMemoryFlowTuiIo['stderr'];
|
||||
|
|
@ -157,6 +158,7 @@ export function sanitizeMemoryFlowTuiError(error: unknown): string {
|
|||
.replace(/\b(api[_-]?key|password|token|secret)=\S+/gi, '[redacted]');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function memoryFlowCommandForInkInput(
|
||||
input: string,
|
||||
key: InkKey,
|
||||
|
|
@ -285,6 +287,7 @@ function TrustIssues(props: { view: MemoryFlowViewModel; theme: MemoryFlowTuiThe
|
|||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function MemoryFlowTuiApp(props: MemoryFlowTuiAppProps): ReactNode {
|
||||
const app = useApp();
|
||||
const totalEvents = props.input.events.length;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @internal */
|
||||
export const KTX_CONTEXT_BUILD_COMMANDS = [
|
||||
{
|
||||
command: 'ktx ingest',
|
||||
|
|
@ -24,9 +25,10 @@ export const KTX_NEXT_STEP_DIRECT_COMMANDS = [
|
|||
},
|
||||
] as const;
|
||||
|
||||
/** @internal */
|
||||
export const KTX_NEXT_STEP_COMMANDS = [...KTX_NEXT_STEP_DIRECT_COMMANDS] as const;
|
||||
|
||||
export const KTX_NEXT_STEP_COMMAND_WIDTH = Math.max(
|
||||
const KTX_NEXT_STEP_COMMAND_WIDTH = Math.max(
|
||||
...[...KTX_CONTEXT_BUILD_COMMANDS, ...KTX_NEXT_STEP_COMMANDS].map((step) => step.command.length),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface PickNotionRootPagesArgs {
|
|||
connection: KtxProjectConnectionConfig;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type NotionPickerApi = Pick<NotionApi, 'search' | 'retrieveBotUser'>;
|
||||
export type NotionRootPagePickResult =
|
||||
| { kind: 'selected'; rootPageIds: string[] }
|
||||
|
|
@ -50,6 +51,7 @@ function assertSafeNotionPickerConnectionId(connectionId: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function normalizeNotionPageId(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
const compact = trimmed.includes('-') ? trimmed.replace(/-/g, '') : trimmed;
|
||||
|
|
@ -106,6 +108,7 @@ function extractParentPageId(page: Record<string, unknown>): string | null {
|
|||
return normalizeNotionPageId(parent.page_id);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function notionPickerPageFromSearchResult(result: Record<string, unknown>): TreePickerNodeInput {
|
||||
const id = typeof result.id === 'string' ? normalizeNotionPageId(result.id) : '';
|
||||
if (!id) {
|
||||
|
|
@ -119,6 +122,7 @@ export function notionPickerPageFromSearchResult(result: Record<string, unknown>
|
|||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function discoverNotionPickerPages(
|
||||
api: NotionPickerApi,
|
||||
options: { cap?: number } = {},
|
||||
|
|
@ -161,6 +165,7 @@ export async function discoverNotionPickerPages(
|
|||
return { pages, cappedAtCount: cap, warnings };
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function resolveNotionWorkspaceLabel(api: NotionPickerApi, connectionId: string): Promise<string> {
|
||||
try {
|
||||
const bot = (await api.retrieveBotUser()) as NotionBotInfo;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ function withTextInputBodySpacing(message: string): string {
|
|||
return `${title}\n\n${bodyLines.join('\n')}`;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function withMenuOptionSpacing(message: string): string {
|
||||
if (!message.includes('\n') || message.endsWith('\n')) {
|
||||
return message;
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ interface KtxCliScanProgress extends Omit<KtxProgressPort, 'update'> {
|
|||
flush(): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createCliScanProgress(
|
||||
io: KtxCliIo,
|
||||
state: KtxCliScanProgressState = { progress: 0, hasPendingTransient: false },
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { readKtxMcpDaemonStatus } from './managed-mcp-daemon.js';
|
|||
|
||||
export type KtxAgentTarget = 'claude-code' | 'claude-desktop' | 'codex' | 'cursor' | 'opencode' | 'universal';
|
||||
export type KtxAgentScope = 'project' | 'global' | 'local';
|
||||
/** @internal */
|
||||
export type KtxAgentInstallMode = 'mcp' | 'mcp-cli';
|
||||
|
||||
export interface KtxSetupAgentsArgs {
|
||||
|
|
@ -118,6 +119,7 @@ function writeSetupOutro(io: KtxCliIo, message: string): void {
|
|||
const STEP_HEADING_RE = /^(\d+)\. (.+)$/;
|
||||
const ACTION_MARKER_RE = /^(RUN|PASTE|USE|OPEN):$/;
|
||||
|
||||
/** @internal */
|
||||
export function createAgentNextActionsLineFormatter(
|
||||
stdout: KtxCliIo['stdout'],
|
||||
): (line: string) => string {
|
||||
|
|
@ -301,7 +303,7 @@ function claudeDesktopConfigPath(): { path: string; jsonPath: string[] } {
|
|||
|
||||
const CLAUDE_DESKTOP_FORWARDED_ENV_KEYS = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY'] as const;
|
||||
|
||||
export function collectClaudeDesktopForwardedEnv(source: NodeJS.ProcessEnv): Record<string, string> {
|
||||
function collectClaudeDesktopForwardedEnv(source: NodeJS.ProcessEnv): Record<string, string> {
|
||||
const captured: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (value === undefined || value === '') continue;
|
||||
|
|
@ -394,7 +396,7 @@ function plannedMcpJsonEntries(input: {
|
|||
return [];
|
||||
}
|
||||
|
||||
export function agentInstallManifestPath(projectDir: string): string {
|
||||
function agentInstallManifestPath(projectDir: string): string {
|
||||
return join(resolve(projectDir), '.ktx/agents/install-manifest.json');
|
||||
}
|
||||
|
||||
|
|
@ -410,6 +412,7 @@ function claudeDesktopLauncherPath(projectDir: string): string {
|
|||
return join(resolve(projectDir), '.ktx/agents/claude/ktx-plugin-runner.sh');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function plannedKtxAgentFiles(input: {
|
||||
projectDir: string;
|
||||
target: KtxAgentTarget;
|
||||
|
|
@ -750,6 +753,7 @@ function mergeManifest(
|
|||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function removeKtxAgentInstall(projectDir: string, io: KtxCliIo): Promise<number> {
|
||||
const manifest = await readKtxAgentInstallManifest(projectDir);
|
||||
if (!manifest) {
|
||||
|
|
@ -765,7 +769,7 @@ export async function removeKtxAgentInstall(projectDir: string, io: KtxCliIo): P
|
|||
return 0;
|
||||
}
|
||||
|
||||
export interface KtxSetupAgentsPromptAdapter {
|
||||
interface KtxSetupAgentsPromptAdapter {
|
||||
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
||||
multiselect(options: {
|
||||
message: string;
|
||||
|
|
@ -853,6 +857,7 @@ function hasAdminCliEntries(entries: InstallEntry[]): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InstallSummaryEntry {
|
||||
title: string;
|
||||
lines: string[];
|
||||
|
|
@ -869,6 +874,7 @@ function formatInlinePath(path: string): string {
|
|||
return path;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function formatInstallSummaryLines(
|
||||
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>,
|
||||
entries: InstallEntry[],
|
||||
|
|
|
|||
|
|
@ -25,12 +25,13 @@ import {
|
|||
type KtxSetupPromptOption,
|
||||
} from './setup-prompts.js';
|
||||
|
||||
export type KtxSetupContextBuildStatus =
|
||||
type KtxSetupContextBuildStatus =
|
||||
| 'not_started'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'stale';
|
||||
|
||||
/** @internal */
|
||||
export interface KtxSetupContextCommands {
|
||||
build: string;
|
||||
status: string;
|
||||
|
|
@ -61,7 +62,7 @@ export interface KtxSetupContextStatusSummary {
|
|||
detail?: string;
|
||||
}
|
||||
|
||||
export interface KtxSetupContextReadiness {
|
||||
interface KtxSetupContextReadiness {
|
||||
ready: boolean;
|
||||
agentContextReady: boolean;
|
||||
semanticSearchReady: boolean;
|
||||
|
|
@ -86,7 +87,7 @@ export interface KtxSetupContextStepArgs {
|
|||
runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
|
||||
}
|
||||
|
||||
export interface KtxSetupContextPromptAdapter {
|
||||
interface KtxSetupContextPromptAdapter {
|
||||
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
||||
cancel(message: string): void;
|
||||
}
|
||||
|
|
@ -125,6 +126,7 @@ async function pathExists(path: string): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function contextBuildCommands(projectDir: string): KtxSetupContextCommands {
|
||||
const resolvedProjectDir = resolve(projectDir);
|
||||
return {
|
||||
|
|
@ -236,6 +238,7 @@ export async function readKtxSetupContextState(projectDir: string): Promise<KtxS
|
|||
return normalizeState(projectDir, JSON.parse(await readFile(filePath, 'utf-8')) as unknown);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function writeKtxSetupContextState(projectDir: string, state: KtxSetupContextState): Promise<void> {
|
||||
const resolvedProjectDir = resolve(projectDir);
|
||||
await mkdir(join(resolvedProjectDir, '.ktx', 'setup'), { recursive: true });
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export type KtxSetupDatabasesResult =
|
|||
| { status: 'missing-input'; projectDir: string }
|
||||
| { status: 'failed'; projectDir: string };
|
||||
|
||||
/** @internal */
|
||||
export interface KtxSetupDatabasesPromptAdapter {
|
||||
multiselect(options: {
|
||||
message: string;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ function createTargetState(target: KtxPublicIngestPlanTarget): ContextBuildTarge
|
|||
// Pure rendering functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** @internal */
|
||||
export function renderDemoBanner(projectDir?: string): string {
|
||||
const lines = [
|
||||
'',
|
||||
|
|
@ -76,6 +77,7 @@ export function renderDemoBanner(projectDir?: string): string {
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function renderDemoCardContent(title: string, selections: string[]): string {
|
||||
const lines = [
|
||||
`┌ ${title}`,
|
||||
|
|
@ -88,6 +90,7 @@ export function renderDemoCardContent(title: string, selections: string[]): stri
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function renderDemoAgentTransition(): string {
|
||||
const lines = [
|
||||
'┌ Demo project is ready — let\'s connect your agent',
|
||||
|
|
@ -99,6 +102,7 @@ export function renderDemoAgentTransition(): string {
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function renderDemoCompletionSummary(projectDir: string, agentInstalled: boolean): string {
|
||||
const lines: string[] = [
|
||||
'',
|
||||
|
|
@ -129,7 +133,7 @@ export function renderDemoCompletionSummary(projectDir: string, agentInstalled:
|
|||
// Keypress navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function waitForDemoNavigation(
|
||||
async function waitForDemoNavigation(
|
||||
stdin?: NodeJS.ReadStream,
|
||||
): Promise<'forward' | 'back'> {
|
||||
const input = stdin ?? process.stdin;
|
||||
|
|
@ -169,7 +173,7 @@ export async function waitForDemoNavigation(
|
|||
// Interactive card
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function renderDemoCard(
|
||||
async function renderDemoCard(
|
||||
title: string,
|
||||
selections: string[],
|
||||
io: KtxCliIo,
|
||||
|
|
@ -186,6 +190,7 @@ export async function renderDemoCard(
|
|||
// Context build replay
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** @internal */
|
||||
export interface DemoReplayEvent {
|
||||
delayMs: number;
|
||||
connectionId: string;
|
||||
|
|
@ -194,6 +199,7 @@ export interface DemoReplayEvent {
|
|||
summaryText: string | null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const DEMO_REPLAY_TARGETS = {
|
||||
primarySources: [
|
||||
createDemoTarget('postgres-warehouse', 'database-ingest', 'postgres'),
|
||||
|
|
@ -205,6 +211,7 @@ export const DEMO_REPLAY_TARGETS = {
|
|||
],
|
||||
} as const;
|
||||
|
||||
/** @internal */
|
||||
export function buildDemoReplayTimeline(): DemoReplayEvent[] {
|
||||
return [
|
||||
// postgres-warehouse: database schema context
|
||||
|
|
@ -239,7 +246,7 @@ function renderDemoContextCompletionSummary(): string {
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export async function runDemoContextReplay(
|
||||
async function runDemoContextReplay(
|
||||
io: KtxCliIo,
|
||||
stdin?: NodeJS.ReadStream,
|
||||
): Promise<'forward' | 'back'> {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export type KtxSetupEmbeddingsResult =
|
|||
| { status: 'missing-input'; projectDir: string }
|
||||
| { status: 'failed'; projectDir: string };
|
||||
|
||||
/** @internal */
|
||||
export interface KtxSetupEmbeddingsPromptAdapter {
|
||||
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
||||
password(options: { message: string }): Promise<string | undefined>;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export class KtxSetupExitError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface SetupInterruptTracker {
|
||||
track<T>(run: () => Promise<T>): Promise<T>;
|
||||
wasCtrlC(): boolean;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export type KtxSetupModelResult =
|
|||
| { status: 'missing-input'; projectDir: string }
|
||||
| { status: 'failed'; projectDir: string };
|
||||
|
||||
/** @internal */
|
||||
export interface AnthropicModelChoice {
|
||||
id: string;
|
||||
label: string;
|
||||
|
|
@ -59,6 +60,7 @@ export interface AnthropicModelChoice {
|
|||
|
||||
export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code';
|
||||
|
||||
/** @internal */
|
||||
export interface KtxSetupModelPromptAdapter {
|
||||
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
||||
text(options: { message: string; placeholder?: string }): Promise<string | undefined>;
|
||||
|
|
@ -82,8 +84,7 @@ export interface KtxSetupModelDeps {
|
|||
spinner?: () => KtxCliSpinner;
|
||||
}
|
||||
|
||||
export const BUNDLED_ANTHROPIC_MODEL_REGISTRY_VERSION = '2026-05-07';
|
||||
|
||||
/** @internal */
|
||||
export const BUNDLED_ANTHROPIC_MODELS: AnthropicModelChoice[] = [
|
||||
{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', recommended: true },
|
||||
{ id: 'claude-opus-4-6', label: 'Claude Opus 4.6', recommended: false },
|
||||
|
|
@ -131,7 +132,7 @@ const execFileAsync = promisify(execFile);
|
|||
|
||||
type AnthropicModelDiscoveryErrorReason = 'authentication' | 'http' | 'empty-response';
|
||||
|
||||
export class AnthropicModelDiscoveryError extends Error {
|
||||
class AnthropicModelDiscoveryError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly reason: AnthropicModelDiscoveryErrorReason,
|
||||
|
|
@ -212,6 +213,7 @@ async function defaultListGcloudProjects(): Promise<GcloudProjectChoice[]> {
|
|||
.filter((project): project is GcloudProjectChoice => Boolean(project));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function fetchAnthropicModels(
|
||||
apiKey: string,
|
||||
fetchFn: typeof fetch = fetch,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import {
|
|||
type KtxSetupPromptOption,
|
||||
} from './setup-prompts.js';
|
||||
|
||||
export type KtxSetupProjectMode = 'auto' | 'prompt-new';
|
||||
export type KtxSetupInputMode = 'auto' | 'disabled';
|
||||
type KtxSetupProjectMode = 'auto' | 'prompt-new';
|
||||
type KtxSetupInputMode = 'auto' | 'disabled';
|
||||
|
||||
export interface KtxSetupProjectArgs {
|
||||
projectDir: string;
|
||||
|
|
@ -45,6 +45,7 @@ export type KtxSetupProjectResult =
|
|||
| { status: 'cancelled'; projectDir: string }
|
||||
| { status: 'missing-input'; projectDir: string };
|
||||
|
||||
/** @internal */
|
||||
export interface KtxSetupProjectPromptAdapter {
|
||||
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
||||
text(options: { message: string; placeholder?: string }): Promise<string | undefined>;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export type KtxSetupReadyAction =
|
|||
| 'agents'
|
||||
| 'exit';
|
||||
|
||||
export interface KtxSetupReadyMenuPromptAdapter {
|
||||
interface KtxSetupReadyMenuPromptAdapter {
|
||||
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
||||
cancel(message: string): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export interface KtxSetupSourcesPromptAdapter {
|
|||
log?(message: string): void;
|
||||
}
|
||||
|
||||
export type SourceValidationResult = { ok: true; detail?: string } | { ok: false; message: string };
|
||||
type SourceValidationResult = { ok: true; detail?: string } | { ok: false; message: string };
|
||||
|
||||
export interface KtxSetupSourcesDeps {
|
||||
prompts?: KtxSetupSourcesPromptAdapter;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface KtxTextIngestArgs {
|
|||
failFast: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface TextMemoryIngestPort {
|
||||
ingest(input: MemoryAgentInput): Promise<{ runId: string }>;
|
||||
waitForRun(runId: string): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ function transientHint(text: string, now: number): PickerState['transientHint']
|
|||
return { text, expiresAt: now + TRANSIENT_HINT_DURATION_MS };
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function clearExpiredTransientHint(state: PickerState, now = Date.now()): PickerState {
|
||||
if (!state.transientHint || state.transientHint.expiresAt > now) {
|
||||
return state;
|
||||
|
|
@ -249,6 +250,7 @@ function checkedAncestor(nodeId: string, state: PickerState): TreePickerNode | n
|
|||
return null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function canToggle(nodeId: string, state: PickerState): { ok: true } | { ok: false; reason: string } {
|
||||
if (!state.byId.has(nodeId)) {
|
||||
return { ok: false, reason: 'Node not found' };
|
||||
|
|
@ -260,6 +262,7 @@ export function canToggle(nodeId: string, state: PickerState): { ok: true } | {
|
|||
return { ok: true };
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toggleChecked(state: PickerState, nodeId: string, now = Date.now()): PickerState {
|
||||
const toggle = canToggle(nodeId, state);
|
||||
if (!toggle.ok) {
|
||||
|
|
@ -335,6 +338,7 @@ export function visibleNodeIds(state: PickerState): string[] {
|
|||
return result;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function selectAllVisible(state: PickerState): PickerState {
|
||||
const candidates = state.search.query.trim().length > 0 ? matchingIds(state) : new Set(visibleNodeIds(state));
|
||||
const checked = new Set(state.checked);
|
||||
|
|
@ -358,6 +362,7 @@ export function selectAllVisible(state: PickerState): PickerState {
|
|||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function selectNone(state: PickerState): PickerState {
|
||||
return cloneState(state, { checked: new Set(), transientHint: null });
|
||||
}
|
||||
|
|
@ -383,6 +388,7 @@ function setExpanded(state: PickerState, nodeId: string, value: boolean | 'toggl
|
|||
return cloneState(state, { expanded });
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function moveCursor(state: PickerState, dir: 'up' | 'down' | 'left' | 'right'): PickerState {
|
||||
const node = state.byId.get(state.cursorId);
|
||||
if (!node) {
|
||||
|
|
|
|||
|
|
@ -77,12 +77,14 @@ interface TreePickerAppProps extends TreePickerRenderInput {
|
|||
onExit(result: TreePickerResult): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface TreePickerInkInstance {
|
||||
rerender(tree: ReactNode): void;
|
||||
unmount(): void;
|
||||
waitUntilExit(): Promise<void>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface TreePickerInkRenderOptions {
|
||||
stdin?: TreePickerTuiIo['stdin'];
|
||||
stdout: TreePickerTuiIo['stdout'];
|
||||
|
|
@ -97,6 +99,7 @@ function resolveTheme(env: NodeJS.ProcessEnv = process.env): TreePickerTheme {
|
|||
return env.NO_COLOR || env.TERM === 'dumb' ? NO_COLOR_THEME : COLOR_THEME;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function resolveTreePickerWidth(columns: number | undefined): number {
|
||||
const resolvedColumns = columns ?? 100;
|
||||
return Math.max(60, Math.min(120, resolvedColumns - 4));
|
||||
|
|
@ -114,6 +117,7 @@ function rowMatchesSearch(state: PickerState, nodeId: string): boolean {
|
|||
return node.title.toLocaleLowerCase().includes(query) || node.path.toLocaleLowerCase().includes(query);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function sanitizeTreePickerTuiError(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message
|
||||
|
|
@ -121,11 +125,13 @@ export function sanitizeTreePickerTuiError(error: unknown): string {
|
|||
.replace(/\b(api[_-]?key|password|token|secret)=\S+/gi, '[redacted]');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function windowOffset(count: number, selected: number, visible: number): number {
|
||||
if (count <= visible) return 0;
|
||||
return Math.max(0, Math.min(count - visible, selected - Math.floor(visible / 2)));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function windowItems<T>(items: T[], selected: number, visible: number): { items: T[]; offset: number } {
|
||||
const offset = windowOffset(items.length, selected, visible);
|
||||
return { items: items.slice(offset, offset + visible), offset };
|
||||
|
|
@ -137,6 +143,7 @@ function truncateText(value: string, width: number): string {
|
|||
return `${value.slice(0, width - 3)}...`;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function treePickerCommandForInkInput(
|
||||
input: string,
|
||||
key: InkKey,
|
||||
|
|
@ -205,6 +212,7 @@ function PickerRow(props: { state: PickerState; nodeId: string; width: number; t
|
|||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function TreePickerApp(props: TreePickerAppProps): ReactNode {
|
||||
const app = useApp();
|
||||
const [state, setState] = useState(props.initialState);
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export function warnVizFallbackOnce(io: KtxVizFallbackIo, decision: KtxVizFallba
|
|||
io.stderr.write(`Visualization requested but ${decision.message}; printing plain output.\n`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function resetVizFallbackWarningsForTest(): void {
|
||||
warnedFallbackReasons.clear();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue