@kaelio/ktx 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/python/{kaelio_ktx-0.7.0-py3-none-any.whl → kaelio_ktx-0.9.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli-program.js +7 -0
- package/dist/cli-runtime.js +50 -3
- package/dist/command-schemas.d.ts +1 -1
- package/dist/command-tree.js +5 -1
- package/dist/commands/completion-commands.d.ts +3 -0
- package/dist/commands/completion-commands.js +38 -0
- package/dist/commands/ingest-commands.js +0 -4
- package/dist/commands/knowledge-commands.js +15 -2
- package/dist/commands/setup-commands.js +3 -3
- package/dist/commands/sl-commands.js +19 -7
- package/dist/completion/complete-engine.d.ts +19 -0
- package/dist/completion/complete-engine.js +128 -0
- package/dist/completion/completion-scripts.d.ts +1 -0
- package/dist/completion/completion-scripts.js +36 -0
- package/dist/completion/dynamic-candidates.d.ts +6 -0
- package/dist/completion/dynamic-candidates.js +98 -0
- package/dist/connection-drivers.d.ts +3 -0
- package/dist/connection-drivers.js +17 -0
- package/dist/connection-recovery.d.ts +34 -0
- package/dist/connection-recovery.js +82 -0
- package/dist/connection.js +3 -1
- package/dist/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
- package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
- package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
- package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
- package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +29 -0
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +190 -0
- package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
- package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
- package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
- package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
- package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
- package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
- package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
- package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
- package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
- package/dist/context/ingest/ingest-bundle.runner.d.ts +8 -0
- package/dist/context/ingest/ingest-bundle.runner.js +72 -15
- package/dist/context/ingest/ingest-profile.d.ts +102 -0
- package/dist/context/ingest/ingest-profile.js +306 -0
- package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
- package/dist/context/ingest/isolated-diff/work-unit-executor.js +25 -2
- package/dist/context/ingest/local-adapters.js +21 -4
- package/dist/context/ingest/local-bundle-runtime.js +4 -2
- package/dist/context/ingest/local-ingest.d.ts +1 -1
- package/dist/context/ingest/local-ingest.js +6 -4
- package/dist/context/ingest/memory-flow/events.js +2 -1
- package/dist/context/ingest/ports.d.ts +2 -0
- package/dist/context/ingest/reports.d.ts +3 -0
- package/dist/context/ingest/reports.js +10 -0
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +3 -1
- package/dist/context/ingest/stages/stage-3-work-units.js +2 -0
- package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +2 -1
- package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
- package/dist/context/ingest/tools/tool-call-logger.d.ts +6 -0
- package/dist/context/ingest/tools/tool-call-logger.js +36 -1
- package/dist/context/llm/ai-sdk-runtime.js +32 -3
- package/dist/context/llm/claude-code-runtime.js +35 -2
- package/dist/context/llm/codex-exec-events.d.ts +20 -0
- package/dist/context/llm/codex-exec-events.js +155 -0
- package/dist/context/llm/codex-isolation.d.ts +3 -0
- package/dist/context/llm/codex-isolation.js +5 -0
- package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
- package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
- package/dist/context/llm/codex-models.d.ts +2 -0
- package/dist/context/llm/codex-models.js +17 -0
- package/dist/context/llm/codex-runtime-config.d.ts +16 -0
- package/dist/context/llm/codex-runtime-config.js +19 -0
- package/dist/context/llm/codex-runtime.d.ts +37 -0
- package/dist/context/llm/codex-runtime.js +304 -0
- package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
- package/dist/context/llm/codex-sdk-runner.js +63 -0
- package/dist/context/llm/local-config.d.ts +2 -0
- package/dist/context/llm/local-config.js +12 -1
- package/dist/context/llm/runtime-port.d.ts +25 -0
- package/dist/context/mcp/context-tools.d.ts +2 -1
- package/dist/context/mcp/context-tools.js +82 -15
- package/dist/context/mcp/server.js +4 -0
- package/dist/context/mcp/types.d.ts +15 -1
- package/dist/context/project/config.d.ts +3 -0
- package/dist/context/project/config.js +6 -2
- package/dist/context/project/driver-schemas.js +1 -1
- package/dist/context/search/discover.js +4 -3
- package/dist/context/sl/local-sl.d.ts +15 -0
- package/dist/context/sl/local-sl.js +30 -0
- package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
- package/dist/context/sql-analysis/ports.d.ts +12 -2
- package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
- package/dist/context/wiki/local-knowledge.d.ts +10 -0
- package/dist/context/wiki/local-knowledge.js +22 -0
- package/dist/context-build-view.d.ts +0 -3
- package/dist/context-build-view.js +5 -39
- package/dist/ingest.js +7 -10
- package/dist/io/buffered-command-io.d.ts +11 -0
- package/dist/io/buffered-command-io.js +28 -0
- package/dist/knowledge.d.ts +5 -0
- package/dist/knowledge.js +10 -1
- package/dist/llm/types.d.ts +1 -1
- package/dist/local-adapters.d.ts +10 -2
- package/dist/local-adapters.js +19 -3
- package/dist/next-steps.js +1 -2
- package/dist/progress-port-adapter.d.ts +6 -0
- package/dist/progress-port-adapter.js +18 -0
- package/dist/public-ingest-copy.js +1 -1
- package/dist/public-ingest.d.ts +20 -8
- package/dist/public-ingest.js +198 -61
- package/dist/scan.js +3 -1
- package/dist/setup-context.d.ts +2 -0
- package/dist/setup-context.js +138 -64
- package/dist/setup-databases.d.ts +17 -1
- package/dist/setup-databases.js +366 -326
- package/dist/setup-models.d.ts +10 -1
- package/dist/setup-models.js +90 -2
- package/dist/setup-ready-menu.d.ts +16 -2
- package/dist/setup-ready-menu.js +37 -5
- package/dist/setup-sources.js +141 -33
- package/dist/setup.js +24 -12
- package/dist/skills/analytics/SKILL.md +6 -1
- package/dist/sl.d.ts +6 -1
- package/dist/sl.js +32 -8
- package/dist/status-project.d.ts +11 -0
- package/dist/status-project.js +50 -1
- package/dist/telemetry/command-hook.d.ts +1 -0
- package/dist/telemetry/command-hook.js +3 -1
- package/dist/telemetry/emitter.js +1 -1
- package/dist/telemetry/events.d.ts +15 -9
- package/dist/telemetry/events.js +17 -5
- package/dist/telemetry/identity.d.ts +1 -2
- package/dist/telemetry/identity.js +13 -10
- package/dist/telemetry/index.d.ts +13 -1
- package/dist/telemetry/index.js +18 -3
- package/dist/telemetry/scrubber.d.ts +10 -0
- package/dist/telemetry/scrubber.js +20 -0
- package/package.json +20 -19
- package/dist/ingest-depth.d.ts +0 -8
- package/dist/ingest-depth.js +0 -56
- package/dist/setup-database-context-depth.d.ts +0 -23
- package/dist/setup-database-context-depth.js +0 -84
package/dist/cli-program.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { Command, InvalidArgumentError } from '@commander-js/extra-typings';
|
|
4
|
+
import { registerCompletionCommands } from './commands/completion-commands.js';
|
|
4
5
|
import { registerConnectionCommands } from './commands/connection-commands.js';
|
|
5
6
|
import { registerIngestCommands } from './commands/ingest-commands.js';
|
|
6
7
|
import { registerWikiCommands } from './commands/knowledge-commands.js';
|
|
@@ -319,6 +320,11 @@ export function collectCommandFlagsPresent(command) {
|
|
|
319
320
|
export function buildKtxProgram(options) {
|
|
320
321
|
const program = createBaseProgram(options.packageInfo, options.io);
|
|
321
322
|
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
323
|
+
// The hidden completion command must stay silent and side-effect free: skip
|
|
324
|
+
// the telemetry notice, command span, and project checks entirely.
|
|
325
|
+
if (commandPath(actionCommand).includes('__complete')) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
322
328
|
const telemetry = await import('./telemetry/index.js');
|
|
323
329
|
options.setTelemetryModule?.(telemetry);
|
|
324
330
|
await telemetry.showTelemetryNoticeIfNeeded(options.io, options.packageInfo);
|
|
@@ -362,6 +368,7 @@ export function buildKtxProgram(options) {
|
|
|
362
368
|
registerStatusCommands(program, context);
|
|
363
369
|
registerMcpCommands(program, context);
|
|
364
370
|
registerAdminCommands(program, context);
|
|
371
|
+
registerCompletionCommands(program, context);
|
|
365
372
|
return program;
|
|
366
373
|
}
|
|
367
374
|
export async function runCommanderKtxCli(argv, io, deps, info, options) {
|
package/dist/cli-runtime.js
CHANGED
|
@@ -35,11 +35,58 @@ async function runInit(args, io) {
|
|
|
35
35
|
export async function runInitForCommander(args, io) {
|
|
36
36
|
return await runInit(args, io);
|
|
37
37
|
}
|
|
38
|
+
function signalExitCode(signal) {
|
|
39
|
+
// 128 + signal number: SIGINT (2) -> 130, SIGTERM (15) -> 143.
|
|
40
|
+
return signal === 'SIGTERM' ? 143 : 130;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Flush telemetry on interrupt for the real CLI process. `capture()` is
|
|
44
|
+
* fire-and-forget and the only flush guarantee lives in a `finally` a signal
|
|
45
|
+
* skips, so Ctrl-C / `kill` of a long-running command (ingest, `mcp stdio`)
|
|
46
|
+
* would otherwise drop its `command` event and queued events. Installed only
|
|
47
|
+
* when driving the actual process; programmatic/test callers pass their own
|
|
48
|
+
* `io` and never reach here. Returns a disposer that removes the listeners.
|
|
49
|
+
*/
|
|
50
|
+
function installTelemetrySignalFlush(io, info) {
|
|
51
|
+
let handling = false;
|
|
52
|
+
const handle = (signal) => {
|
|
53
|
+
if (handling) {
|
|
54
|
+
process.exit(signalExitCode(signal));
|
|
55
|
+
}
|
|
56
|
+
handling = true;
|
|
57
|
+
void (async () => {
|
|
58
|
+
try {
|
|
59
|
+
const { emitAbortedCommandAndShutdown } = await import('./telemetry/index.js');
|
|
60
|
+
await emitAbortedCommandAndShutdown({ packageInfo: info, io });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Best-effort: never let a telemetry hiccup block the interrupt exit.
|
|
64
|
+
}
|
|
65
|
+
process.exit(signalExitCode(signal));
|
|
66
|
+
})();
|
|
67
|
+
};
|
|
68
|
+
const onSigint = () => handle('SIGINT');
|
|
69
|
+
const onSigterm = () => handle('SIGTERM');
|
|
70
|
+
process.on('SIGINT', onSigint);
|
|
71
|
+
process.on('SIGTERM', onSigterm);
|
|
72
|
+
return () => {
|
|
73
|
+
process.off('SIGINT', onSigint);
|
|
74
|
+
process.off('SIGTERM', onSigterm);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
38
77
|
export async function runKtxCli(argv = process.argv.slice(2), io = process, deps = {}) {
|
|
39
78
|
const info = getKtxCliPackageInfo();
|
|
40
79
|
profileMark('runtime:runKtxCli');
|
|
41
80
|
const { runCommanderKtxCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
81
|
+
// Real-process entry only: flush telemetry if interrupted. Test/programmatic
|
|
82
|
+
// callers pass their own `io`, so they never install process-level handlers.
|
|
83
|
+
const removeSignalFlush = io === process ? installTelemetrySignalFlush(io, info) : undefined;
|
|
84
|
+
try {
|
|
85
|
+
return await runCommanderKtxCli(argv, io, deps, info, {
|
|
86
|
+
runInit: runInitForCommander,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
removeSignalFlush?.();
|
|
91
|
+
}
|
|
45
92
|
}
|
package/dist/command-tree.js
CHANGED
|
@@ -5,7 +5,11 @@ export function walkCommandTree(command) {
|
|
|
5
5
|
description: command.description(),
|
|
6
6
|
aliases: command.aliases(),
|
|
7
7
|
arguments: command.registeredArguments.map(formatArgumentDeclaration),
|
|
8
|
-
|
|
8
|
+
// Internal commands (e.g. the shell-completion helper `__complete`) use a
|
|
9
|
+
// `__` prefix and are omitted from the human-facing command tree.
|
|
10
|
+
children: command.commands
|
|
11
|
+
.filter((child) => !child.name().startsWith('__'))
|
|
12
|
+
.map((child) => walkCommandTree(child)),
|
|
9
13
|
};
|
|
10
14
|
}
|
|
11
15
|
export function formatCommandTree(node) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Argument } from '@commander-js/extra-typings';
|
|
2
|
+
import { computeCompletions } from '../completion/complete-engine.js';
|
|
3
|
+
import { completionScript } from '../completion/completion-scripts.js';
|
|
4
|
+
import { createProjectCompletionProviders } from '../completion/dynamic-candidates.js';
|
|
5
|
+
import { profileMark } from '../startup-profile.js';
|
|
6
|
+
profileMark('module:commands/completion-commands');
|
|
7
|
+
export function registerCompletionCommands(program, context) {
|
|
8
|
+
program
|
|
9
|
+
.command('completion')
|
|
10
|
+
.description('Print a shell completion script for ktx')
|
|
11
|
+
.addArgument(new Argument('<shell>', 'Target shell').choices(['zsh', 'bash']))
|
|
12
|
+
.addHelpText('after', '\nEnable completion by adding the matching line to your shell startup file:\n' +
|
|
13
|
+
' zsh: eval "$(ktx completion zsh)"\n' +
|
|
14
|
+
' bash: eval "$(ktx completion bash)"\n')
|
|
15
|
+
.action((shell) => {
|
|
16
|
+
context.io.stdout.write(completionScript(shell));
|
|
17
|
+
});
|
|
18
|
+
// Hidden command invoked by the generated shell scripts. It must only ever
|
|
19
|
+
// print newline-separated candidates to stdout and exit 0, so a TAB press is
|
|
20
|
+
// never disrupted by an error, a telemetry notice, or a parse failure.
|
|
21
|
+
program
|
|
22
|
+
.command('__complete', { hidden: true })
|
|
23
|
+
.argument('[words...]')
|
|
24
|
+
.allowUnknownOption(true)
|
|
25
|
+
.helpOption(false)
|
|
26
|
+
.action(async (words) => {
|
|
27
|
+
try {
|
|
28
|
+
const candidates = await computeCompletions(program, words, createProjectCompletionProviders());
|
|
29
|
+
if (candidates.length > 0) {
|
|
30
|
+
context.io.stdout.write(`${candidates.join('\n')}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Swallow: completion must never break the shell.
|
|
35
|
+
}
|
|
36
|
+
context.setExitCode(0);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -11,8 +11,6 @@ export function registerIngestCommands(program, context, commandOptions) {
|
|
|
11
11
|
.usage('[options] [connectionId]')
|
|
12
12
|
.argument('[connectionId]', 'Configured connection id to ingest (omit to ingest all)')
|
|
13
13
|
.option('--all', 'Ingest all configured connections', false)
|
|
14
|
-
.addOption(new Option('--fast', 'Use deterministic database schema ingest').conflicts('deep'))
|
|
15
|
-
.addOption(new Option('--deep', 'Use AI-enriched database ingest').conflicts('fast'))
|
|
16
14
|
.addOption(new Option('--query-history', 'Include database query-history usage patterns').conflicts('noQueryHistory'))
|
|
17
15
|
.addOption(new Option('--no-query-history', 'Skip database query-history usage patterns'))
|
|
18
16
|
.option('--query-history-window-days <days>', 'Query-history lookback window for this run', parsePositiveIntegerOption)
|
|
@@ -57,8 +55,6 @@ export function registerIngestCommands(program, context, commandOptions) {
|
|
|
57
55
|
all: selection.kind === 'all',
|
|
58
56
|
json: options.json === true,
|
|
59
57
|
inputMode: options.input === false ? 'disabled' : 'auto',
|
|
60
|
-
...(options.fast === true ? { depth: 'fast' } : {}),
|
|
61
|
-
...(options.deep === true ? { depth: 'deep' } : {}),
|
|
62
58
|
queryHistory,
|
|
63
59
|
...(options.queryHistoryWindowDays !== undefined ? { queryHistoryWindowDays: options.queryHistoryWindowDays } : {}),
|
|
64
60
|
cliVersion: context.packageInfo.version,
|
|
@@ -11,9 +11,9 @@ function isDebugEnabled(command) {
|
|
|
11
11
|
return options.debug === true;
|
|
12
12
|
}
|
|
13
13
|
export function registerWikiCommands(program, context) {
|
|
14
|
-
program
|
|
14
|
+
const wiki = program
|
|
15
15
|
.command('wiki')
|
|
16
|
-
.description('List or
|
|
16
|
+
.description('List, search, or read local wiki pages')
|
|
17
17
|
.usage('[options] [query...]')
|
|
18
18
|
.argument('[query...]', 'Search query; omit to list all pages')
|
|
19
19
|
.option('--user-id <id>', 'Local user id', 'local')
|
|
@@ -50,4 +50,17 @@ export function registerWikiCommands(program, context) {
|
|
|
50
50
|
cliVersion: context.packageInfo.version,
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
+
wiki
|
|
54
|
+
.command('read')
|
|
55
|
+
.description('Read a wiki page file by key')
|
|
56
|
+
.argument('<key>', 'Wiki page key')
|
|
57
|
+
.action(async (key, _options, command) => {
|
|
58
|
+
const parentOpts = command.parent?.opts();
|
|
59
|
+
await runKnowledgeArgs(context, {
|
|
60
|
+
command: 'read',
|
|
61
|
+
projectDir: resolveCommandProjectDir(command),
|
|
62
|
+
key,
|
|
63
|
+
userId: parentOpts?.userId ?? 'local',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
53
66
|
}
|
|
@@ -18,7 +18,7 @@ function embeddingBackend(value) {
|
|
|
18
18
|
throw new InvalidArgumentError(`invalid choice '${value}'`);
|
|
19
19
|
}
|
|
20
20
|
function llmBackend(value) {
|
|
21
|
-
if (value === 'anthropic' || value === 'vertex' || value === 'claude-code') {
|
|
21
|
+
if (value === 'anthropic' || value === 'vertex' || value === 'claude-code' || value === 'codex') {
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
24
|
throw new InvalidArgumentError(`invalid choice '${value}'`);
|
|
@@ -195,9 +195,9 @@ export function registerSetupCommands(program, context) {
|
|
|
195
195
|
.addOption(new Option('--source-git-url <url>', 'Git URL for dbt, MetricFlow, or LookML').hideHelp())
|
|
196
196
|
.addOption(new Option('--source-branch <branch>', 'Git branch for source setup').hideHelp())
|
|
197
197
|
.addOption(new Option('--source-subpath <path>', 'Repo subpath for source setup').hideHelp())
|
|
198
|
-
.addOption(new Option('--source-auth-token-ref <ref>', 'env: or file: credential ref for source repo auth').hideHelp())
|
|
198
|
+
.addOption(new Option('--source-auth-token-ref <ref>', 'env: or file: credential ref for source repo auth or Notion integration token').hideHelp())
|
|
199
199
|
.addOption(new Option('--source-url <url>', 'Source service URL for Metabase or Looker').hideHelp())
|
|
200
|
-
.addOption(new Option('--source-api-key-ref <ref>', 'env: or file: API key ref for Metabase
|
|
200
|
+
.addOption(new Option('--source-api-key-ref <ref>', 'env: or file: API key ref for Metabase').hideHelp())
|
|
201
201
|
.addOption(new Option('--source-client-id <id>', 'Looker client id').hideHelp())
|
|
202
202
|
.addOption(new Option('--source-client-secret-ref <ref>', 'env: or file: Looker client secret ref').hideHelp())
|
|
203
203
|
.addOption(new Option('--source-warehouse-connection-id <id>', 'Mapped warehouse connection id').hideHelp())
|
|
@@ -63,19 +63,27 @@ export function registerSlCommands(program, context, commandName = 'sl') {
|
|
|
63
63
|
cliVersion: context.packageInfo.version,
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
|
+
sl.command('read')
|
|
67
|
+
.description('Read a semantic-layer source YAML file')
|
|
68
|
+
.argument('<sourceName>', 'Semantic-layer source name')
|
|
69
|
+
.action(async (sourceName, _options, command) => {
|
|
70
|
+
const parentOpts = command.parent?.opts();
|
|
71
|
+
await runSlArgs(context, {
|
|
72
|
+
command: 'read',
|
|
73
|
+
projectDir: resolveCommandProjectDir(command),
|
|
74
|
+
connectionId: parentOpts?.connectionId,
|
|
75
|
+
sourceName,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
66
78
|
sl.command('validate')
|
|
67
|
-
.description('Validate a semantic-layer source
|
|
79
|
+
.description('Validate a semantic-layer source')
|
|
68
80
|
.argument('<sourceName>', 'Semantic-layer source name')
|
|
69
81
|
.action(async (sourceName, _options, command) => {
|
|
70
82
|
const parentOpts = command.parent?.opts();
|
|
71
|
-
const connectionId = parentOpts?.connectionId;
|
|
72
|
-
if (connectionId === undefined) {
|
|
73
|
-
command.error("error: required option '--connection-id <id>' not specified");
|
|
74
|
-
}
|
|
75
83
|
await runSlArgs(context, {
|
|
76
84
|
command: 'validate',
|
|
77
85
|
projectDir: resolveCommandProjectDir(command),
|
|
78
|
-
connectionId: connectionId,
|
|
86
|
+
connectionId: parentOpts?.connectionId,
|
|
79
87
|
sourceName,
|
|
80
88
|
});
|
|
81
89
|
});
|
|
@@ -99,10 +107,14 @@ export function registerSlCommands(program, context, commandName = 'sl') {
|
|
|
99
107
|
throw new Error('sl query requires at least one --measure');
|
|
100
108
|
}
|
|
101
109
|
const parentOpts = command.parent?.opts();
|
|
110
|
+
const connectionId = parentOpts?.connectionId;
|
|
111
|
+
if (connectionId === undefined) {
|
|
112
|
+
command.error("error: required option '--connection-id <id>' not specified");
|
|
113
|
+
}
|
|
102
114
|
const args = slQueryCommandSchema.parse({
|
|
103
115
|
command: 'query',
|
|
104
116
|
projectDir: resolveCommandProjectDir(command),
|
|
105
|
-
connectionId
|
|
117
|
+
connectionId,
|
|
106
118
|
...(options.queryFile
|
|
107
119
|
? { queryFile: options.queryFile }
|
|
108
120
|
: {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CommandUnknownOpts } from '@commander-js/extra-typings';
|
|
2
|
+
/**
|
|
3
|
+
* Dynamic completion candidates that depend on project state (semantic-layer
|
|
4
|
+
* source names, wiki page keys, connection ids). Injected so the engine stays
|
|
5
|
+
* pure and unit-testable without touching the filesystem.
|
|
6
|
+
*/
|
|
7
|
+
export interface CompletionProviders {
|
|
8
|
+
/** Candidate operands for a positional argument of the active command path. */
|
|
9
|
+
positionalCandidates(commandPath: string[], typedTokens: string[]): Promise<string[]>;
|
|
10
|
+
/** Candidate values for an option that has no static `choices` (e.g. `--connection-id`). */
|
|
11
|
+
optionValueCandidates(commandPath: string[], optionFlag: string, typedTokens: string[]): Promise<string[]>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compute completion candidates for the partial last element of `words`
|
|
15
|
+
* (everything the shell has on the line after `ktx`). The active command and
|
|
16
|
+
* its flags are derived by walking the live Commander tree, so completion never
|
|
17
|
+
* drifts from the real command structure.
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeCompletions(program: CommandUnknownOpts, words: string[], providers: CompletionProviders): Promise<string[]>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
function isHiddenCommand(command) {
|
|
2
|
+
// Completion mirrors `ktx --help`: commands registered with `{ hidden: true }`
|
|
3
|
+
// (the `__complete` helper and `mcp serve-internal`) are internal and must not
|
|
4
|
+
// surface. Commander exposes this only through the private `_hidden` field its
|
|
5
|
+
// own help renderer reads, so a name heuristic like a `__` prefix is not enough.
|
|
6
|
+
return command._hidden === true;
|
|
7
|
+
}
|
|
8
|
+
function resolveCommand(program, typedTokens) {
|
|
9
|
+
let command = program;
|
|
10
|
+
const commandPath = [];
|
|
11
|
+
for (let index = 0; index < typedTokens.length; index += 1) {
|
|
12
|
+
const token = typedTokens[index];
|
|
13
|
+
if (token.startsWith('-')) {
|
|
14
|
+
// A value-taking option in the `--flag value` form consumes the next token
|
|
15
|
+
// as its value, so skip that value before matching subcommands. Otherwise a
|
|
16
|
+
// connection id like `query` would be resolved as the `sl query` subcommand
|
|
17
|
+
// instead of being treated as the `--connection-id` value. The `--flag=value`
|
|
18
|
+
// form carries its own value and consumes nothing extra.
|
|
19
|
+
if (!token.includes('=')) {
|
|
20
|
+
const option = findOption(command, token);
|
|
21
|
+
if (option && !option.isBoolean()) {
|
|
22
|
+
index += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const sub = command.commands.find((candidate) => candidate.name() === token || candidate.aliases().includes(token));
|
|
28
|
+
if (sub) {
|
|
29
|
+
command = sub;
|
|
30
|
+
commandPath.push(sub.name());
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { command, commandPath };
|
|
34
|
+
}
|
|
35
|
+
function collectOptions(command) {
|
|
36
|
+
const options = [];
|
|
37
|
+
let current = command;
|
|
38
|
+
while (current) {
|
|
39
|
+
options.push(...current.options);
|
|
40
|
+
current = current.parent;
|
|
41
|
+
}
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
function findOption(command, flag) {
|
|
45
|
+
return collectOptions(command).find((option) => option.long === flag || option.short === flag);
|
|
46
|
+
}
|
|
47
|
+
function isRepeatableOption(option) {
|
|
48
|
+
// Variadic options, and options backed by a collector with an array default
|
|
49
|
+
// (e.g. `--measure`/`--dimension`), may be supplied more than once.
|
|
50
|
+
return option.variadic || Array.isArray(option.defaultValue);
|
|
51
|
+
}
|
|
52
|
+
function flagCandidates(command, typedTokens) {
|
|
53
|
+
const present = new Set(typedTokens.filter((token) => token.startsWith('-')));
|
|
54
|
+
const candidates = [];
|
|
55
|
+
for (const option of collectOptions(command)) {
|
|
56
|
+
if (option.hidden || !option.long) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (present.has(option.long) && !isRepeatableOption(option)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
candidates.push(option.long);
|
|
63
|
+
}
|
|
64
|
+
return candidates;
|
|
65
|
+
}
|
|
66
|
+
async function optionValueCandidates(resolved, option, typedTokens, providers) {
|
|
67
|
+
if (option.argChoices && option.argChoices.length > 0) {
|
|
68
|
+
return option.argChoices;
|
|
69
|
+
}
|
|
70
|
+
return providers.optionValueCandidates(resolved.commandPath, option.long ?? option.name(), typedTokens);
|
|
71
|
+
}
|
|
72
|
+
function dedupeSortFilter(candidates, partial) {
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const matches = [];
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
if (!candidate.startsWith(partial) || seen.has(candidate)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
seen.add(candidate);
|
|
80
|
+
matches.push(candidate);
|
|
81
|
+
}
|
|
82
|
+
return matches.sort();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compute completion candidates for the partial last element of `words`
|
|
86
|
+
* (everything the shell has on the line after `ktx`). The active command and
|
|
87
|
+
* its flags are derived by walking the live Commander tree, so completion never
|
|
88
|
+
* drifts from the real command structure.
|
|
89
|
+
*/
|
|
90
|
+
export async function computeCompletions(program, words, providers) {
|
|
91
|
+
const partial = words.length > 0 ? (words[words.length - 1] ?? '') : '';
|
|
92
|
+
const typedTokens = words.slice(0, -1);
|
|
93
|
+
const resolved = resolveCommand(program, typedTokens);
|
|
94
|
+
// (a) Option value via the `--opt=value` form.
|
|
95
|
+
const equalsMatch = /^(--[^=]+)=(.*)$/.exec(partial);
|
|
96
|
+
if (equalsMatch) {
|
|
97
|
+
const [, flag, valuePartial] = equalsMatch;
|
|
98
|
+
const option = findOption(resolved.command, flag);
|
|
99
|
+
if (!option || option.isBoolean()) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const values = await optionValueCandidates(resolved, option, typedTokens, providers);
|
|
103
|
+
return dedupeSortFilter(values.map((value) => `${flag}=${value}`), `${flag}=${valuePartial}`);
|
|
104
|
+
}
|
|
105
|
+
// (b) Option value via the `--opt value` form (previous token is a value-taking option).
|
|
106
|
+
const previous = typedTokens[typedTokens.length - 1];
|
|
107
|
+
if (previous && previous.startsWith('-') && !partial.startsWith('-')) {
|
|
108
|
+
const option = findOption(resolved.command, previous);
|
|
109
|
+
if (option && !option.isBoolean()) {
|
|
110
|
+
return dedupeSortFilter(await optionValueCandidates(resolved, option, typedTokens, providers), partial);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// (c) Flag completion.
|
|
114
|
+
if (partial.startsWith('-')) {
|
|
115
|
+
return dedupeSortFilter(flagCandidates(resolved.command, typedTokens), partial);
|
|
116
|
+
}
|
|
117
|
+
// (d) Positional: subcommand names union static argument choices union dynamic operand candidates.
|
|
118
|
+
const candidates = resolved.command.commands
|
|
119
|
+
.filter((sub) => !isHiddenCommand(sub))
|
|
120
|
+
.map((sub) => sub.name());
|
|
121
|
+
for (const argument of resolved.command.registeredArguments) {
|
|
122
|
+
if (argument.argChoices) {
|
|
123
|
+
candidates.push(...argument.argChoices);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
candidates.push(...(await providers.positionalCandidates(resolved.commandPath, typedTokens)));
|
|
127
|
+
return dedupeSortFilter(candidates, partial);
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function completionScript(shell: 'zsh' | 'bash'): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Static shell completion scripts emitted by `ktx completion <shell>`.
|
|
2
|
+
//
|
|
3
|
+
// Both scripts gather the words on the current command line (excluding the
|
|
4
|
+
// leading `ktx`), append the partial word under the cursor, and delegate to the
|
|
5
|
+
// hidden `ktx __complete` command, which prints newline-separated candidates.
|
|
6
|
+
// All command/flag/entity knowledge lives in `ktx __complete` so these scripts
|
|
7
|
+
// never have to encode the command tree.
|
|
8
|
+
//
|
|
9
|
+
// Lines are single-quoted JS strings so the shell `${...}` expansions are
|
|
10
|
+
// emitted verbatim (a template literal would try to interpolate them).
|
|
11
|
+
const ZSH_SCRIPT = [
|
|
12
|
+
'#compdef ktx',
|
|
13
|
+
'_ktx() {',
|
|
14
|
+
' local -a candidates',
|
|
15
|
+
' local out',
|
|
16
|
+
' out="$(ktx __complete -- "${words[@]:1:$((CURRENT-1))}" 2>/dev/null)" || return 0',
|
|
17
|
+
' candidates=("${(@f)out}")',
|
|
18
|
+
' compadd -- $candidates',
|
|
19
|
+
'}',
|
|
20
|
+
'compdef _ktx ktx',
|
|
21
|
+
'',
|
|
22
|
+
].join('\n');
|
|
23
|
+
const BASH_SCRIPT = [
|
|
24
|
+
'_ktx() {',
|
|
25
|
+
' local cur out',
|
|
26
|
+
' cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
27
|
+
' out="$(ktx __complete -- "${COMP_WORDS[@]:1:COMP_CWORD}" 2>/dev/null)" || { COMPREPLY=(); return 0; }',
|
|
28
|
+
" local IFS=$'\\n'",
|
|
29
|
+
' COMPREPLY=($(compgen -W "${out}" -- "$cur"))',
|
|
30
|
+
'}',
|
|
31
|
+
'complete -F _ktx ktx',
|
|
32
|
+
'',
|
|
33
|
+
].join('\n');
|
|
34
|
+
export function completionScript(shell) {
|
|
35
|
+
return shell === 'zsh' ? ZSH_SCRIPT : BASH_SCRIPT;
|
|
36
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CompletionProviders } from './complete-engine.js';
|
|
2
|
+
/**
|
|
3
|
+
* Project-backed completion providers. Every entry swallows its own errors so a
|
|
4
|
+
* failed lookup never breaks the shell — completion degrades to commands/flags.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createProjectCompletionProviders(): CompletionProviders;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { resolveKtxProjectDir } from '../project-resolver.js';
|
|
4
|
+
/** Extract an option value from already-typed tokens (`--flag value` or `--flag=value`). */
|
|
5
|
+
function extractOptionValue(tokens, flag) {
|
|
6
|
+
const prefix = `${flag}=`;
|
|
7
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
8
|
+
const token = tokens[index];
|
|
9
|
+
if (token === flag) {
|
|
10
|
+
const next = tokens[index + 1];
|
|
11
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
12
|
+
return next;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else if (token.startsWith(prefix)) {
|
|
16
|
+
return token.slice(prefix.length);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve and load the project the user is completing against. Honors a
|
|
23
|
+
* `--project-dir` typed on the line, then `KTX_PROJECT_DIR`, then the nearest
|
|
24
|
+
* `ktx.yaml`. Returns null (no completions) when there is no project, without
|
|
25
|
+
* creating any files.
|
|
26
|
+
*/
|
|
27
|
+
async function loadCompletionProject(typedTokens) {
|
|
28
|
+
const explicitProjectDir = extractOptionValue(typedTokens, '--project-dir');
|
|
29
|
+
const projectDir = resolveKtxProjectDir(explicitProjectDir !== undefined ? { explicitProjectDir } : {});
|
|
30
|
+
if (!existsSync(join(projectDir, 'ktx.yaml'))) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const { loadKtxProject } = await import('../context/project/project.js');
|
|
34
|
+
return loadKtxProject({ projectDir });
|
|
35
|
+
}
|
|
36
|
+
async function sourceNames(typedTokens) {
|
|
37
|
+
const project = await loadCompletionProject(typedTokens);
|
|
38
|
+
if (!project) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const connectionId = extractOptionValue(typedTokens, '--connection-id');
|
|
42
|
+
const { listLocalSlSources } = await import('../context/sl/local-sl.js');
|
|
43
|
+
const summaries = await listLocalSlSources(project, connectionId !== undefined ? { connectionId } : {});
|
|
44
|
+
return [...new Set(summaries.map((summary) => summary.name))];
|
|
45
|
+
}
|
|
46
|
+
async function wikiPageKeys(typedTokens) {
|
|
47
|
+
const project = await loadCompletionProject(typedTokens);
|
|
48
|
+
if (!project) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const userId = extractOptionValue(typedTokens, '--user-id');
|
|
52
|
+
const { listLocalKnowledgePageKeys } = await import('../context/wiki/local-knowledge.js');
|
|
53
|
+
return listLocalKnowledgePageKeys(project, userId !== undefined ? { userId } : {});
|
|
54
|
+
}
|
|
55
|
+
async function connectionIds(typedTokens) {
|
|
56
|
+
const project = await loadCompletionProject(typedTokens);
|
|
57
|
+
if (!project) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return Object.keys(project.config.connections).sort();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Project-backed completion providers. Every entry swallows its own errors so a
|
|
64
|
+
* failed lookup never breaks the shell — completion degrades to commands/flags.
|
|
65
|
+
*/
|
|
66
|
+
export function createProjectCompletionProviders() {
|
|
67
|
+
return {
|
|
68
|
+
async positionalCandidates(commandPath, typedTokens) {
|
|
69
|
+
try {
|
|
70
|
+
const key = commandPath.join(' ');
|
|
71
|
+
if (key === 'sl read' || key === 'sl validate') {
|
|
72
|
+
return await sourceNames(typedTokens);
|
|
73
|
+
}
|
|
74
|
+
if (key === 'wiki read') {
|
|
75
|
+
return await wikiPageKeys(typedTokens);
|
|
76
|
+
}
|
|
77
|
+
if (key === 'connection test' || key === 'ingest') {
|
|
78
|
+
return await connectionIds(typedTokens);
|
|
79
|
+
}
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
async optionValueCandidates(_commandPath, optionFlag, typedTokens) {
|
|
87
|
+
try {
|
|
88
|
+
if (optionFlag === '--connection-id' || optionFlag === '--connection') {
|
|
89
|
+
return await connectionIds(typedTokens);
|
|
90
|
+
}
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const KTX_DATABASE_DRIVER_IDS = new Set([
|
|
2
|
+
'sqlite',
|
|
3
|
+
'postgres',
|
|
4
|
+
'mysql',
|
|
5
|
+
'clickhouse',
|
|
6
|
+
'sqlserver',
|
|
7
|
+
'bigquery',
|
|
8
|
+
'snowflake',
|
|
9
|
+
]);
|
|
10
|
+
export function normalizeConnectionDriver(connection) {
|
|
11
|
+
return String(connection.driver ?? '')
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
export function isDatabaseDriver(driver) {
|
|
16
|
+
return KTX_DATABASE_DRIVER_IDS.has(driver.trim().toLowerCase());
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { KtxCliIo } from './cli-runtime.js';
|
|
2
|
+
import type { KtxSetupPromptOption } from './setup-prompts.js';
|
|
3
|
+
export type RecoveryOutcome = 'ready' | 'skip' | 'back' | 'failed';
|
|
4
|
+
/** @internal */
|
|
5
|
+
export interface RecoveryAction {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
run: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export type ConfigureResult = 'configured' | 'back' | 'cancelled';
|
|
11
|
+
export type ValidateResult = {
|
|
12
|
+
status: 'ok';
|
|
13
|
+
} | {
|
|
14
|
+
status: 'back';
|
|
15
|
+
} | {
|
|
16
|
+
status: 'failed';
|
|
17
|
+
extraActions?: RecoveryAction[];
|
|
18
|
+
};
|
|
19
|
+
export interface ConnectionRecoveryInput {
|
|
20
|
+
label: string;
|
|
21
|
+
interactive: boolean;
|
|
22
|
+
allowSkip: boolean;
|
|
23
|
+
io: KtxCliIo;
|
|
24
|
+
prompts: {
|
|
25
|
+
select(options: {
|
|
26
|
+
message: string;
|
|
27
|
+
options: KtxSetupPromptOption[];
|
|
28
|
+
}): Promise<string>;
|
|
29
|
+
};
|
|
30
|
+
snapshot: () => Promise<() => Promise<void>>;
|
|
31
|
+
configure: () => Promise<ConfigureResult>;
|
|
32
|
+
validate: () => Promise<ValidateResult>;
|
|
33
|
+
}
|
|
34
|
+
export declare function runConnectionSetupWithRecovery(input: ConnectionRecoveryInput): Promise<RecoveryOutcome>;
|