@kaelio/ktx 0.11.0 → 0.12.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.11.0-py3-none-any.whl → kaelio_ktx-0.12.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/admin.js +1 -1
- package/dist/clack.d.ts +16 -0
- package/dist/clack.js +37 -6
- package/dist/claude-code-prompt-caching.js +1 -1
- package/dist/cli-program.js +3 -3
- package/dist/cli-runtime.js +2 -2
- package/dist/commands/connection-commands.js +1 -1
- package/dist/commands/ingest-commands.js +4 -4
- package/dist/commands/mcp-commands.js +12 -12
- package/dist/commands/runtime-commands.js +4 -4
- package/dist/commands/setup-commands.js +6 -5
- package/dist/commands/sl-commands.js +1 -1
- package/dist/commands/sql-commands.js +1 -1
- package/dist/commands/status-commands.js +1 -1
- package/dist/connection.js +1 -1
- package/dist/connectors/clickhouse/connector.js +1 -1
- package/dist/connectors/mysql/connector.js +1 -1
- package/dist/connectors/snowflake/connector.d.ts +1 -1
- package/dist/connectors/sqlite/connector.js +2 -25
- package/dist/connectors/sqlserver/connector.js +3 -3
- package/dist/context/connections/connection-type.d.ts +1 -1
- package/dist/context/connections/read-only-sql.d.ts +1 -0
- package/dist/context/connections/read-only-sql.js +116 -2
- package/dist/context/core/git.service.d.ts +23 -0
- package/dist/context/core/git.service.js +71 -8
- package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
- package/dist/context/ingest/adapters/looker/client.js +7 -2
- package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
- package/dist/context/ingest/adapters/looker/factory.js +9 -0
- package/dist/context/ingest/adapters/looker/mapping.js +1 -1
- package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.js +1 -1
- package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
- package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
- package/dist/context/ingest/artifact-gates.d.ts +2 -6
- package/dist/context/ingest/artifact-gates.js +5 -47
- package/dist/context/ingest/constrained-repair.d.ts +55 -0
- package/dist/context/ingest/constrained-repair.js +167 -0
- package/dist/context/ingest/final-gate-repair.d.ts +9 -11
- package/dist/context/ingest/final-gate-repair.js +40 -128
- package/dist/context/ingest/finalization-scope.d.ts +1 -1
- package/dist/context/ingest/finalization-scope.js +15 -15
- package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
- package/dist/context/ingest/ingest-bundle.runner.js +101 -67
- package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
- package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
- package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
- package/dist/context/ingest/local-bundle-runtime.js +9 -10
- package/dist/context/ingest/local-ingest.d.ts +2 -0
- package/dist/context/ingest/local-ingest.js +2 -0
- package/dist/context/ingest/memory-flow/view-model.js +1 -1
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
- package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
- package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
- package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
- package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
- package/dist/context/llm/ai-sdk-runtime.js +2 -2
- package/dist/context/llm/claude-code-runtime.js +1 -1
- package/dist/context/llm/local-config.js +1 -1
- package/dist/context/llm/runtime-tools.js +2 -2
- package/dist/context/mcp/context-tools.js +7 -7
- package/dist/context/mcp/local-project-ports.js +23 -54
- package/dist/context/memory/local-memory.js +4 -1
- package/dist/context/memory/memory-agent.service.js +1 -1
- package/dist/context/project/config.d.ts +11 -4
- package/dist/context/project/config.js +85 -30
- package/dist/context/project/driver-schemas.js +1 -1
- package/dist/context/project/mappings-yaml-schema.js +2 -2
- package/dist/context/project/project.js +12 -4
- package/dist/context/scan/description-generation.js +4 -4
- package/dist/context/scan/local-enrichment-artifacts.js +2 -1
- package/dist/context/scan/local-scan.js +2 -2
- package/dist/context/scan/local-structural-artifacts.js +5 -5
- package/dist/context/scan/relationship-benchmark-report.js +1 -1
- package/dist/context/scan/relationship-discovery.js +3 -3
- package/dist/context/scan/relationship-llm-proposal.js +3 -3
- package/dist/context/sl/local-query.js +3 -33
- package/dist/context/sl/local-sl.d.ts +0 -8
- package/dist/context/sl/local-sl.js +44 -69
- package/dist/context/sl/semantic-layer.service.d.ts +25 -8
- package/dist/context/sl/semantic-layer.service.js +109 -56
- package/dist/context/sl/source-files.d.ts +46 -0
- package/dist/context/sl/source-files.js +131 -0
- package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
- package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
- package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
- package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
- package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
- package/dist/context/sql-analysis/dialect.d.ts +2 -0
- package/dist/context/sql-analysis/dialect.js +20 -0
- package/dist/context/tools/base-tool.d.ts +6 -19
- package/dist/context/tools/base-tool.js +0 -14
- package/dist/context-build-view.js +5 -5
- package/dist/database-tree-picker.js +18 -3
- package/dist/demo-assets.js +0 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -23
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +44 -0
- package/dist/ingest.d.ts +1 -1
- package/dist/ingest.js +8 -2
- package/dist/io/symbols.d.ts +2 -0
- package/dist/io/symbols.js +2 -0
- package/dist/io/tty.d.ts +8 -0
- package/dist/io/tty.js +16 -0
- package/dist/llm/embedding-health.js +1 -1
- package/dist/llm/embedding-provider.js +3 -3
- package/dist/llm/model-provider.js +1 -1
- package/dist/local-adapters.d.ts +1 -0
- package/dist/local-adapters.js +2 -2
- package/dist/local-scan-connectors.js +1 -1
- package/dist/managed-local-embeddings.js +17 -8
- package/dist/managed-mcp-daemon.js +3 -3
- package/dist/managed-python-command.d.ts +7 -0
- package/dist/managed-python-command.js +34 -8
- package/dist/managed-python-daemon.js +2 -2
- package/dist/managed-python-http.js +3 -3
- package/dist/managed-python-runtime.d.ts +30 -1
- package/dist/managed-python-runtime.js +134 -18
- package/dist/managed-uv-release.d.ts +7 -0
- package/dist/managed-uv-release.js +11 -0
- package/dist/mcp-http-server.js +4 -4
- package/dist/mcp-server-factory.js +3 -3
- package/dist/mcp-stdio-server.js +1 -1
- package/dist/memory-flow-hud.js +2 -2
- package/dist/next-steps.js +2 -2
- package/dist/prompt-navigation.d.ts +17 -0
- package/dist/prompt-navigation.js +49 -3
- package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
- package/dist/prompts/memory_agent_external_ingest.md +2 -2
- package/dist/public-ingest-copy.js +1 -1
- package/dist/public-ingest.js +3 -3
- package/dist/release-version.js +1 -1
- package/dist/runtime-requirements.js +1 -1
- package/dist/runtime.js +9 -9
- package/dist/scan.js +1 -1
- package/dist/setup-agents.js +21 -30
- package/dist/setup-banner.d.ts +20 -0
- package/dist/setup-banner.js +39 -0
- package/dist/setup-context.js +24 -15
- package/dist/setup-databases.js +31 -59
- package/dist/setup-demo-tour.js +12 -8
- package/dist/setup-embeddings.js +9 -9
- package/dist/setup-interrupt.js +1 -1
- package/dist/setup-models.d.ts +4 -1
- package/dist/setup-models.js +54 -28
- package/dist/setup-project.js +29 -5
- package/dist/setup-prompts.js +16 -1
- package/dist/setup-ready-menu.js +1 -1
- package/dist/setup-sources.js +27 -7
- package/dist/setup.js +13 -13
- package/dist/skills/analytics/SKILL.md +3 -3
- package/dist/skills/dbt_ingest/SKILL.md +3 -3
- package/dist/skills/looker_ingest/SKILL.md +3 -3
- package/dist/skills/lookml_ingest/SKILL.md +7 -7
- package/dist/skills/metabase_ingest/SKILL.md +4 -4
- package/dist/skills/metricflow_ingest/SKILL.md +15 -15
- package/dist/skills/notion_synthesize/SKILL.md +1 -1
- package/dist/skills/sl/SKILL.md +3 -3
- package/dist/skills/sl_capture/SKILL.md +1 -1
- package/dist/skills/wiki_capture/SKILL.md +1 -1
- package/dist/source-mapping.js +1 -1
- package/dist/startup-profile.js +1 -1
- package/dist/status-project.d.ts +0 -2
- package/dist/status-project.js +4 -6
- package/dist/telemetry/events.d.ts +1 -1
- package/dist/telemetry/exception.js +14 -0
- package/dist/text-ingest.js +1 -1
- package/dist/tree-picker-tui.d.ts +0 -1
- package/dist/tree-picker-tui.js +2 -3
- package/package.json +1 -1
package/dist/admin.js
CHANGED
|
@@ -18,7 +18,7 @@ export function registerAdminCommands(program, context) {
|
|
|
18
18
|
});
|
|
19
19
|
admin
|
|
20
20
|
.command('init')
|
|
21
|
-
.description('Initialize a Git-backed
|
|
21
|
+
.description('Initialize a Git-backed ktx project directory for maintenance scripts')
|
|
22
22
|
.argument('[directory]', 'Project directory')
|
|
23
23
|
.option('--force', 'Rewrite ktx.yaml and scaffold files in an existing project', false)
|
|
24
24
|
.action(async (projectDir, commandOptions, command) => {
|
package/dist/clack.d.ts
CHANGED
|
@@ -39,5 +39,21 @@ export interface KtxCliPromptAdapter {
|
|
|
39
39
|
spinner(): KtxCliSpinner;
|
|
40
40
|
}
|
|
41
41
|
export declare function createClackSpinner(): KtxCliSpinner;
|
|
42
|
+
/**
|
|
43
|
+
* Stderr-only, non-animated spinner. Use this instead of {@link createCliSpinner}
|
|
44
|
+
* when the next step reads stdin in raw mode (an Ink TUI or a keypress wait):
|
|
45
|
+
* the animated clack spinner seizes stdin via `@clack/core`'s `block()` and
|
|
46
|
+
* leaves it dirty, which the following raw-mode reader misreads as a stray key.
|
|
47
|
+
*/
|
|
42
48
|
export declare function createStaticCliSpinner(io: KtxCliSpinnerIo): KtxCliSpinner;
|
|
49
|
+
/**
|
|
50
|
+
* Animated spinner in an interactive terminal, static `◐/◇/■` lines otherwise
|
|
51
|
+
* (scripts, CI, piped output) so logs stay clean and uncluttered by frames.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createCliSpinner(io: KtxCliIo): KtxCliSpinner;
|
|
54
|
+
export declare function runWithCliSpinner<T>(spinner: KtxCliSpinner, text: {
|
|
55
|
+
start: string;
|
|
56
|
+
success: string;
|
|
57
|
+
failure: string;
|
|
58
|
+
}, run: () => Promise<T>): Promise<T>;
|
|
43
59
|
export declare function createClackPromptAdapter(): KtxCliPromptAdapter;
|
package/dist/clack.js
CHANGED
|
@@ -36,30 +36,61 @@ class KtxCliPromptCancelledError extends Error {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
export function createClackSpinner() {
|
|
39
|
-
|
|
39
|
+
// clack colors the animated spinner frame magenta by default; styleFrame
|
|
40
|
+
// (typed in SpinnerOptions, absent from the README) recolors it ktx orange.
|
|
41
|
+
return spinner({ styleFrame: orange });
|
|
40
42
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
// ktx mascot orange (#FF8A4C) via 24-bit truecolor.
|
|
44
|
+
function orange(text) {
|
|
45
|
+
if (!ansiEnabled()) {
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
return `${ESC}[38;2;255;138;76m${text}${ESC}[39m`;
|
|
43
49
|
}
|
|
44
50
|
function red(text) {
|
|
45
51
|
return ansiColor(text, 31, 39);
|
|
46
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Stderr-only, non-animated spinner. Use this instead of {@link createCliSpinner}
|
|
55
|
+
* when the next step reads stdin in raw mode (an Ink TUI or a keypress wait):
|
|
56
|
+
* the animated clack spinner seizes stdin via `@clack/core`'s `block()` and
|
|
57
|
+
* leaves it dirty, which the following raw-mode reader misreads as a stray key.
|
|
58
|
+
*/
|
|
47
59
|
export function createStaticCliSpinner(io) {
|
|
48
60
|
return {
|
|
49
61
|
start(message) {
|
|
50
|
-
io.stderr.write(`${
|
|
62
|
+
io.stderr.write(`${orange('◐')} ${message}\n`);
|
|
51
63
|
},
|
|
52
64
|
message(message) {
|
|
53
|
-
io.stderr.write(`${
|
|
65
|
+
io.stderr.write(`${orange('│')} ${message}\n`);
|
|
54
66
|
},
|
|
55
67
|
stop(message) {
|
|
56
|
-
io.stderr.write(`${
|
|
68
|
+
io.stderr.write(`${orange('◇')} ${message}\n`);
|
|
57
69
|
},
|
|
58
70
|
error(message) {
|
|
59
71
|
io.stderr.write(`${red('■')} ${message}\n`);
|
|
60
72
|
},
|
|
61
73
|
};
|
|
62
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Animated spinner in an interactive terminal, static `◐/◇/■` lines otherwise
|
|
77
|
+
* (scripts, CI, piped output) so logs stay clean and uncluttered by frames.
|
|
78
|
+
*/
|
|
79
|
+
export function createCliSpinner(io) {
|
|
80
|
+
return io.stdout.isTTY === true ? createClackSpinner() : createStaticCliSpinner(io);
|
|
81
|
+
}
|
|
82
|
+
export async function runWithCliSpinner(spinner, text, run) {
|
|
83
|
+
spinner.start(text.start);
|
|
84
|
+
try {
|
|
85
|
+
const value = await run();
|
|
86
|
+
spinner.stop(text.success);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
spinner.error(text.failure);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
63
94
|
export function createClackPromptAdapter() {
|
|
64
95
|
return {
|
|
65
96
|
async confirm(options) {
|
|
@@ -15,7 +15,7 @@ export function formatClaudeCodePromptCachingWarning(fields) {
|
|
|
15
15
|
if (fields.length === 0) {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
|
-
return `claude-code ignores ${fields.join(', ')} because the Claude Agent SDK does not expose
|
|
18
|
+
return `claude-code ignores ${fields.join(', ')} because the Claude Agent SDK does not expose ktx prompt-cache TTL, tool, or history markers.`;
|
|
19
19
|
}
|
|
20
20
|
export function formatClaudeCodePromptCachingFix() {
|
|
21
21
|
return 'Remove those promptCaching fields or use anthropic, vertex, or gateway when those cache knobs are required.';
|
package/dist/cli-program.js
CHANGED
|
@@ -162,8 +162,8 @@ export function resolveCommandProjectDirOverride(command) {
|
|
|
162
162
|
function createBaseProgram(info, io) {
|
|
163
163
|
return new Command()
|
|
164
164
|
.name('ktx')
|
|
165
|
-
.description('
|
|
166
|
-
.option('--project-dir <path>', '
|
|
165
|
+
.description('ktx data agent context layer CLI')
|
|
166
|
+
.option('--project-dir <path>', 'ktx project directory (default: KTX_PROJECT_DIR, nearest ktx.yaml, or cwd)')
|
|
167
167
|
.option('--debug', 'Enable diagnostic logging to stderr')
|
|
168
168
|
.version(`${info.name} ${info.version}`, '-v, --version', 'Show CLI version')
|
|
169
169
|
.helpOption('-h, --help', 'Show this help text')
|
|
@@ -349,7 +349,7 @@ export function buildKtxProgram(options) {
|
|
|
349
349
|
const attachProjectGroup = shouldAttachCommandProjectGroup(path, hasProject);
|
|
350
350
|
telemetry.beginCommandSpan({
|
|
351
351
|
commandPath: path,
|
|
352
|
-
flagsPresent: collectCommandFlagsPresent(
|
|
352
|
+
flagsPresent: collectCommandFlagsPresent(actionCommand),
|
|
353
353
|
projectDir: attachProjectGroup ? projectDir : undefined,
|
|
354
354
|
hasProject,
|
|
355
355
|
attachProjectGroup,
|
package/dist/cli-runtime.js
CHANGED
|
@@ -14,7 +14,7 @@ export function packageInfoFromJson(packageJson) {
|
|
|
14
14
|
!('version' in packageJson) ||
|
|
15
15
|
typeof packageJson.name !== 'string' ||
|
|
16
16
|
typeof packageJson.version !== 'string') {
|
|
17
|
-
throw new Error('Invalid
|
|
17
|
+
throw new Error('Invalid ktx CLI package metadata');
|
|
18
18
|
}
|
|
19
19
|
return {
|
|
20
20
|
name: packageJson.name,
|
|
@@ -28,7 +28,7 @@ async function runInit(args, io) {
|
|
|
28
28
|
projectDir: args.projectDir,
|
|
29
29
|
force: args.force,
|
|
30
30
|
});
|
|
31
|
-
io.stdout.write(`Initialized
|
|
31
|
+
io.stdout.write(`Initialized ktx project at ${result.projectDir}\n`);
|
|
32
32
|
io.stdout.write(`Config: ${result.configPath}\n`);
|
|
33
33
|
io.stdout.write(`Commit: ${result.commitHash ?? 'none'}\n`);
|
|
34
34
|
return 0;
|
|
@@ -27,7 +27,7 @@ export function registerConnectionCommands(program, context, commandName = 'conn
|
|
|
27
27
|
connection
|
|
28
28
|
.command('test')
|
|
29
29
|
.description('Test one or all configured connections (default: all)')
|
|
30
|
-
.argument('[connectionId]', '
|
|
30
|
+
.argument('[connectionId]', 'ktx connection id to test (omit to test all)')
|
|
31
31
|
.option('--all', 'Test every configured connection and print a summary list')
|
|
32
32
|
.action(async (connectionId, options, command) => {
|
|
33
33
|
if (options.all === true && connectionId !== undefined) {
|
|
@@ -7,16 +7,16 @@ profileMark('module:commands/ingest-commands');
|
|
|
7
7
|
export function registerIngestCommands(program, context, commandOptions) {
|
|
8
8
|
const ingest = program
|
|
9
9
|
.command('ingest')
|
|
10
|
-
.description('Build or inspect
|
|
10
|
+
.description('Build or inspect ktx context, or capture text into memory')
|
|
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
14
|
.addOption(new Option('--query-history', 'Include database query-history usage patterns').conflicts('noQueryHistory'))
|
|
15
15
|
.addOption(new Option('--no-query-history', 'Skip database query-history usage patterns'))
|
|
16
16
|
.option('--query-history-window-days <days>', 'Query-history lookback window for this run', parsePositiveIntegerOption)
|
|
17
|
-
.option('--text <content>', 'Capture inline text into
|
|
18
|
-
.option('--file <path>', 'Capture a text file into
|
|
19
|
-
.option('--connection-id <connectionId>', '
|
|
17
|
+
.option('--text <content>', 'Capture inline text into ktx memory; repeatable', collectOption, [])
|
|
18
|
+
.option('--file <path>', 'Capture a text file into ktx memory; use - for stdin; repeatable', collectOption, [])
|
|
19
|
+
.option('--connection-id <connectionId>', 'ktx connection id to tag captured text/file notes')
|
|
20
20
|
.option('--user-id <id>', 'Memory user id for text/file capture attribution', 'local-cli')
|
|
21
21
|
.option('--fail-fast', 'Stop after the first failed text/file item', false)
|
|
22
22
|
.addOption(new Option('--plain', 'Print plain text output').conflicts(['json']))
|
|
@@ -13,11 +13,11 @@ function binPath() {
|
|
|
13
13
|
}
|
|
14
14
|
function formatMcpStartResultMessage(input) {
|
|
15
15
|
return [
|
|
16
|
-
input.status === 'started' ? `
|
|
16
|
+
input.status === 'started' ? `ktx MCP daemon started: ${input.url}` : `ktx MCP daemon already running: ${input.url}`,
|
|
17
17
|
'',
|
|
18
|
-
'
|
|
19
|
-
'Open your agent for this
|
|
20
|
-
' "Use
|
|
18
|
+
'ktx is ready for configured agents.',
|
|
19
|
+
'Open your agent for this ktx project and ask a data question, for example:',
|
|
20
|
+
' "Use ktx to show me the available tables and metrics."',
|
|
21
21
|
'',
|
|
22
22
|
].join('\n');
|
|
23
23
|
}
|
|
@@ -34,13 +34,13 @@ async function printMcpStatus(context, projectDir) {
|
|
|
34
34
|
export function registerMcpCommands(program, context) {
|
|
35
35
|
const mcp = program
|
|
36
36
|
.command('mcp')
|
|
37
|
-
.description('Manage the
|
|
37
|
+
.description('Manage the ktx MCP HTTP server (bare command: show status)')
|
|
38
38
|
.action(async (_options, command) => {
|
|
39
39
|
await printMcpStatus(context, resolveCommandProjectDir(command));
|
|
40
40
|
});
|
|
41
41
|
mcp
|
|
42
42
|
.command('stdio')
|
|
43
|
-
.description('Run the
|
|
43
|
+
.description('Run the ktx MCP server over stdio')
|
|
44
44
|
.action(async (_options, command) => {
|
|
45
45
|
await (context.deps.mcp?.runStdioServer ?? runKtxMcpStdioServer)({
|
|
46
46
|
projectDir: resolveCommandProjectDir(command),
|
|
@@ -50,7 +50,7 @@ export function registerMcpCommands(program, context) {
|
|
|
50
50
|
});
|
|
51
51
|
mcp
|
|
52
52
|
.command('start')
|
|
53
|
-
.description('Start the
|
|
53
|
+
.description('Start the ktx MCP HTTP server')
|
|
54
54
|
.option('--host <host>', 'Host to bind', '127.0.0.1')
|
|
55
55
|
.option('--port <n>', 'Port to bind', parsePositiveIntegerOption, 7878)
|
|
56
56
|
.option('--token <token>', 'Bearer token required for non-loopback binding')
|
|
@@ -78,7 +78,7 @@ export function registerMcpCommands(program, context) {
|
|
|
78
78
|
allowedOrigins: options.allowedOrigin,
|
|
79
79
|
io: context.io,
|
|
80
80
|
});
|
|
81
|
-
context.io.stdout.write(`
|
|
81
|
+
context.io.stdout.write(`ktx MCP server listening at http://${options.host}:${options.port}/mcp\n`);
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
const result = await (context.deps.mcp?.startDaemon ?? startKtxMcpDaemon)({
|
|
@@ -95,22 +95,22 @@ export function registerMcpCommands(program, context) {
|
|
|
95
95
|
});
|
|
96
96
|
mcp
|
|
97
97
|
.command('stop')
|
|
98
|
-
.description('Stop the
|
|
98
|
+
.description('Stop the ktx MCP daemon')
|
|
99
99
|
.action(async (_options, command) => {
|
|
100
100
|
const result = await (context.deps.mcp?.stopDaemon ?? stopKtxMcpDaemon)({
|
|
101
101
|
projectDir: resolveCommandProjectDir(command),
|
|
102
102
|
});
|
|
103
|
-
context.io.stdout.write(result.status === 'stopped' ? '
|
|
103
|
+
context.io.stdout.write(result.status === 'stopped' ? 'ktx MCP daemon stopped.\n' : 'ktx MCP daemon is not running.\n');
|
|
104
104
|
});
|
|
105
105
|
mcp
|
|
106
106
|
.command('status')
|
|
107
|
-
.description('Show
|
|
107
|
+
.description('Show ktx MCP daemon status')
|
|
108
108
|
.action(async (_options, command) => {
|
|
109
109
|
await printMcpStatus(context, resolveCommandProjectDir(command));
|
|
110
110
|
});
|
|
111
111
|
mcp
|
|
112
112
|
.command('logs')
|
|
113
|
-
.description('Print the
|
|
113
|
+
.description('Print the ktx MCP daemon log')
|
|
114
114
|
.option('--follow', 'Follow log output', false)
|
|
115
115
|
.action(async (options, command) => {
|
|
116
116
|
const logPath = mcpDaemonLayout(resolveCommandProjectDir(command)).logPath;
|
|
@@ -12,7 +12,7 @@ async function runRuntimeArgs(context, args) {
|
|
|
12
12
|
export function registerRuntimeCommands(program, context) {
|
|
13
13
|
const runtime = program
|
|
14
14
|
.command('runtime')
|
|
15
|
-
.description('Install, start, stop, and inspect the
|
|
15
|
+
.description('Install, start, stop, and inspect the ktx-managed Python runtime')
|
|
16
16
|
.showHelpAfterError();
|
|
17
17
|
runtime
|
|
18
18
|
.command('install')
|
|
@@ -30,7 +30,7 @@ export function registerRuntimeCommands(program, context) {
|
|
|
30
30
|
});
|
|
31
31
|
runtime
|
|
32
32
|
.command('start')
|
|
33
|
-
.description('Start the
|
|
33
|
+
.description('Start the ktx daemon')
|
|
34
34
|
.addOption(createRuntimeFeatureOption())
|
|
35
35
|
.option('--force', 'Restart even when a matching daemon is already running', false)
|
|
36
36
|
.action(async (options, command) => {
|
|
@@ -44,8 +44,8 @@ export function registerRuntimeCommands(program, context) {
|
|
|
44
44
|
});
|
|
45
45
|
runtime
|
|
46
46
|
.command('stop')
|
|
47
|
-
.description('Stop the
|
|
48
|
-
.option('--all', 'Stop all
|
|
47
|
+
.description('Stop the ktx daemon')
|
|
48
|
+
.option('--all', 'Stop all ktx daemon processes recorded or discoverable on this machine', false)
|
|
49
49
|
.action(async (options, command) => {
|
|
50
50
|
await runRuntimeArgs(context, {
|
|
51
51
|
command: 'stop',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InvalidArgumentError, Option } from '@commander-js/extra-typings';
|
|
2
2
|
import { resolveCommandProjectDir } from '../cli-program.js';
|
|
3
|
+
import { isKtxSetupLlmBackend } from '../setup-models.js';
|
|
3
4
|
async function runSetupArgs(context, args) {
|
|
4
5
|
const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
|
|
5
6
|
context.setExitCode(await runner(args, context.io));
|
|
@@ -7,7 +8,7 @@ async function runSetupArgs(context, args) {
|
|
|
7
8
|
function positiveInteger(value) {
|
|
8
9
|
const parsed = Number.parseInt(value, 10);
|
|
9
10
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
10
|
-
throw new
|
|
11
|
+
throw new InvalidArgumentError(`Expected a positive integer, received ${value}`);
|
|
11
12
|
}
|
|
12
13
|
return parsed;
|
|
13
14
|
}
|
|
@@ -18,7 +19,7 @@ function embeddingBackend(value) {
|
|
|
18
19
|
throw new InvalidArgumentError(`invalid choice '${value}'`);
|
|
19
20
|
}
|
|
20
21
|
function llmBackend(value) {
|
|
21
|
-
if (value
|
|
22
|
+
if (isKtxSetupLlmBackend(value)) {
|
|
22
23
|
return value;
|
|
23
24
|
}
|
|
24
25
|
throw new InvalidArgumentError(`invalid choice '${value}'`);
|
|
@@ -125,8 +126,8 @@ function shouldShowSetupEntryMenu(options, command) {
|
|
|
125
126
|
export function registerSetupCommands(program, context) {
|
|
126
127
|
const setup = program
|
|
127
128
|
.command('setup')
|
|
128
|
-
.description('Set up or resume a local
|
|
129
|
-
.addOption(new Option('--project-dir <path>', '
|
|
129
|
+
.description('Set up or resume a local ktx project')
|
|
130
|
+
.addOption(new Option('--project-dir <path>', 'ktx project directory').hideHelp())
|
|
130
131
|
.option('--agents', 'Install agent integration only', false)
|
|
131
132
|
.addOption(new Option('--target <target>', 'Agent target').choices([
|
|
132
133
|
'claude-code',
|
|
@@ -184,7 +185,7 @@ export function registerSetupCommands(program, context) {
|
|
|
184
185
|
.argParser((value, previous) => [...previous, value])
|
|
185
186
|
.default([])
|
|
186
187
|
.hideHelp())
|
|
187
|
-
.addOption(new Option('--skip-databases', 'Leave database setup incomplete;
|
|
188
|
+
.addOption(new Option('--skip-databases', 'Leave database setup incomplete; ktx cannot work until a database is added')
|
|
188
189
|
.hideHelp()
|
|
189
190
|
.default(false))
|
|
190
191
|
.addOption(new Option('--source <type>', 'Source connector type').argParser(sourceType).hideHelp())
|
|
@@ -30,7 +30,7 @@ export function registerSlCommands(program, context, commandName = 'sl') {
|
|
|
30
30
|
.description('List, search, validate, or query local semantic-layer sources')
|
|
31
31
|
.usage('[options] [query...]')
|
|
32
32
|
.argument('[query...]', 'Search query; omit to list all sources')
|
|
33
|
-
.option('--connection-id <id>', '
|
|
33
|
+
.option('--connection-id <id>', 'ktx connection id')
|
|
34
34
|
.option('--limit <number>', 'Maximum search results (search mode only)', parsePositiveIntegerOption)
|
|
35
35
|
.addOption(new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
|
36
36
|
'pretty',
|
|
@@ -20,7 +20,7 @@ export function registerSqlCommands(program, context) {
|
|
|
20
20
|
.command('sql')
|
|
21
21
|
.description('Execute parser-validated read-only SQL against a configured connection')
|
|
22
22
|
.argument('<sql...>', 'SQL query to execute')
|
|
23
|
-
.requiredOption('-c, --connection <id>', '
|
|
23
|
+
.requiredOption('-c, --connection <id>', 'ktx connection id')
|
|
24
24
|
.option('--max-rows <n>', 'Maximum rows to return', parseSqlMaxRowsOption, DEFAULT_MAX_ROWS)
|
|
25
25
|
.addOption(new Option('--output <mode>', 'Output mode: pretty (default), plain (TSV), or json').choices([
|
|
26
26
|
'pretty',
|
|
@@ -10,7 +10,7 @@ function inputMode(options) {
|
|
|
10
10
|
export function registerStatusCommands(program, context) {
|
|
11
11
|
program
|
|
12
12
|
.command('status')
|
|
13
|
-
.description('Check current
|
|
13
|
+
.description('Check current ktx setup and project readiness')
|
|
14
14
|
.option('--json', 'Print JSON output', false)
|
|
15
15
|
.option('-v, --verbose', 'Show every check, including passing ones', false)
|
|
16
16
|
.option('--validate', 'Only validate the ktx.yaml schema; skip readiness checks', false)
|
package/dist/connection.js
CHANGED
|
@@ -86,7 +86,7 @@ async function testMetabaseConnection(project, connectionId, createClient) {
|
|
|
86
86
|
}
|
|
87
87
|
async function createDefaultLookerClient(project, connectionId) {
|
|
88
88
|
const factory = new DefaultLookerConnectionClientFactory(createLocalLookerCredentialResolver(project));
|
|
89
|
-
return
|
|
89
|
+
return factory.createLookerClient(connectionId);
|
|
90
90
|
}
|
|
91
91
|
async function testLookerConnection(project, connectionId, createClient) {
|
|
92
92
|
const client = await createClient(project, connectionId);
|
|
@@ -432,7 +432,7 @@ export class KtxClickHouseScanConnector {
|
|
|
432
432
|
}
|
|
433
433
|
assertConnection(connectionId) {
|
|
434
434
|
if (connectionId !== this.connectionId) {
|
|
435
|
-
throw new Error(`
|
|
435
|
+
throw new Error(`ktx ClickHouse connector ${this.id} cannot serve connection ${connectionId}`);
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
438
|
}
|
|
@@ -508,7 +508,7 @@ export class KtxMysqlScanConnector {
|
|
|
508
508
|
}
|
|
509
509
|
assertConnection(connectionId) {
|
|
510
510
|
if (connectionId !== this.connectionId) {
|
|
511
|
-
throw new Error(`
|
|
511
|
+
throw new Error(`ktx MySQL connector ${this.id} cannot serve connection ${connectionId}`);
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
}
|
|
@@ -72,7 +72,7 @@ export interface KtxSnowflakeScanConnectorOptions {
|
|
|
72
72
|
connectionId: string;
|
|
73
73
|
connection: KtxSnowflakeConnectionConfig | undefined;
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* ktx project directory. When provided, snowflake-sdk's logger is redirected to
|
|
76
76
|
* `<projectDir>/.ktx/logs/snowflake.log` so its JSON output does not bleed into
|
|
77
77
|
* the CLI's TTY. Tests that use a fake driverFactory can leave this undefined.
|
|
78
78
|
*/
|
|
@@ -35,29 +35,6 @@ function sqlitePathFromUrl(url) {
|
|
|
35
35
|
}
|
|
36
36
|
return url;
|
|
37
37
|
}
|
|
38
|
-
function stripLeadingSqlComments(sql) {
|
|
39
|
-
let index = 0;
|
|
40
|
-
while (index < sql.length) {
|
|
41
|
-
while (/\s/.test(sql[index] ?? '')) {
|
|
42
|
-
index += 1;
|
|
43
|
-
}
|
|
44
|
-
if (sql.startsWith('--', index)) {
|
|
45
|
-
const end = sql.indexOf('\n', index + 2);
|
|
46
|
-
index = end === -1 ? sql.length : end + 1;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (sql.startsWith('/*', index)) {
|
|
50
|
-
const end = sql.indexOf('*/', index + 2);
|
|
51
|
-
if (end === -1) {
|
|
52
|
-
return sql.slice(index);
|
|
53
|
-
}
|
|
54
|
-
index = end + 2;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
return sql.slice(index);
|
|
60
|
-
}
|
|
61
38
|
export function isKtxSqliteConnectionConfig(connection) {
|
|
62
39
|
const driver = String(connection?.driver ?? '').toLowerCase();
|
|
63
40
|
return driver === 'sqlite';
|
|
@@ -171,7 +148,7 @@ export class KtxSqliteScanConnector {
|
|
|
171
148
|
}
|
|
172
149
|
async executeReadOnly(input, _ctx) {
|
|
173
150
|
this.assertConnection(input.connectionId);
|
|
174
|
-
const result = this.query(limitSqlForExecution(
|
|
151
|
+
const result = this.query(limitSqlForExecution(input.sql, input.maxRows), input.params);
|
|
175
152
|
return { ...result, rowCount: result.rows.length };
|
|
176
153
|
}
|
|
177
154
|
async getColumnDistinctValues(table, columnName, options) {
|
|
@@ -274,7 +251,7 @@ export class KtxSqliteScanConnector {
|
|
|
274
251
|
}
|
|
275
252
|
assertConnection(connectionId) {
|
|
276
253
|
if (connectionId !== this.connectionId) {
|
|
277
|
-
throw new Error(`
|
|
254
|
+
throw new Error(`ktx SQLite connector ${this.id} cannot serve connection ${connectionId}`);
|
|
278
255
|
}
|
|
279
256
|
}
|
|
280
257
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertReadOnlySql } from '../../context/connections/read-only-sql.js';
|
|
1
|
+
import { assertReadOnlySql, stripTrailingSqlNoise } from '../../context/connections/read-only-sql.js';
|
|
2
2
|
import { getDialectForDriver } from '../../context/connections/dialects.js';
|
|
3
3
|
import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
|
|
4
4
|
import { scopedTableNames } from '../../context/scan/table-ref.js';
|
|
@@ -142,7 +142,7 @@ function isDeniedError(error) {
|
|
|
142
142
|
return number === 229 || number === 230 || number === 297;
|
|
143
143
|
}
|
|
144
144
|
function limitSqlForSqlServerExecution(sqlText, maxRows) {
|
|
145
|
-
const trimmed = assertReadOnlySql(sqlText)
|
|
145
|
+
const trimmed = stripTrailingSqlNoise(assertReadOnlySql(sqlText));
|
|
146
146
|
if (!maxRows) {
|
|
147
147
|
return trimmed;
|
|
148
148
|
}
|
|
@@ -575,7 +575,7 @@ export class KtxSqlServerScanConnector {
|
|
|
575
575
|
}
|
|
576
576
|
assertConnection(connectionId) {
|
|
577
577
|
if (connectionId !== this.connectionId) {
|
|
578
|
-
throw new Error(`
|
|
578
|
+
throw new Error(`ktx SQL Server connector ${this.id} cannot serve connection ${connectionId}`);
|
|
579
579
|
}
|
|
580
580
|
}
|
|
581
581
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const connectionTypeSchema: z.ZodEnum<{
|
|
3
|
+
PLAIN: "PLAIN";
|
|
3
4
|
BIGQUERY: "BIGQUERY";
|
|
4
5
|
SNOWFLAKE: "SNOWFLAKE";
|
|
5
6
|
MYSQL: "MYSQL";
|
|
@@ -19,7 +20,6 @@ export declare const connectionTypeSchema: z.ZodEnum<{
|
|
|
19
20
|
METABASE: "METABASE";
|
|
20
21
|
LOOKER: "LOOKER";
|
|
21
22
|
NOTION: "NOTION";
|
|
22
|
-
PLAIN: "PLAIN";
|
|
23
23
|
BETTERSTACK: "BETTERSTACK";
|
|
24
24
|
}>;
|
|
25
25
|
export type ConnectionType = z.infer<typeof connectionTypeSchema>;
|
|
@@ -1,14 +1,128 @@
|
|
|
1
1
|
const MUTATING_SQL = /^\s*(insert|update|delete|merge|alter|drop|create|truncate|grant|revoke|copy|call|do|vacuum|analyze|refresh)\b/i;
|
|
2
2
|
const READ_SQL = /^\s*(select|with)\b/i;
|
|
3
|
+
// Agents (and the daemon's sqlglot validator, which ignores comments) routinely
|
|
4
|
+
// emit read-only queries prefixed with `-- ...` or `/* ... */`. Strip leading
|
|
5
|
+
// comments so the prefix check sees the real statement; otherwise valid SELECT/WITH
|
|
6
|
+
// SQL is rejected here while the parser-backed validator accepts it.
|
|
7
|
+
function stripLeadingSqlComments(sql) {
|
|
8
|
+
let index = 0;
|
|
9
|
+
while (index < sql.length) {
|
|
10
|
+
while (/\s/.test(sql[index] ?? '')) {
|
|
11
|
+
index += 1;
|
|
12
|
+
}
|
|
13
|
+
if (sql.startsWith('--', index)) {
|
|
14
|
+
const end = sql.indexOf('\n', index + 2);
|
|
15
|
+
index = end === -1 ? sql.length : end + 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (sql.startsWith('/*', index)) {
|
|
19
|
+
const end = sql.indexOf('*/', index + 2);
|
|
20
|
+
if (end === -1) {
|
|
21
|
+
return sql.slice(index);
|
|
22
|
+
}
|
|
23
|
+
index = end + 2;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
return sql.slice(index);
|
|
29
|
+
}
|
|
30
|
+
// Lexes past one string literal, quoted identifier, or comment starting at
|
|
31
|
+
// `index`, using standard-SQL rules ('' and "" escapes; no dialect extensions
|
|
32
|
+
// such as backslash escapes or dollar quoting). Returns the index after the
|
|
33
|
+
// token, or `index` unchanged when no quoted/comment token starts there.
|
|
34
|
+
function skipQuotedOrComment(sql, index) {
|
|
35
|
+
const quote = sql[index];
|
|
36
|
+
if (quote === "'" || quote === '"') {
|
|
37
|
+
let i = index + 1;
|
|
38
|
+
while (i < sql.length) {
|
|
39
|
+
if (sql[i] === quote) {
|
|
40
|
+
if (sql[i + 1] === quote) {
|
|
41
|
+
i += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
return i + 1;
|
|
45
|
+
}
|
|
46
|
+
i += 1;
|
|
47
|
+
}
|
|
48
|
+
return sql.length;
|
|
49
|
+
}
|
|
50
|
+
if (sql.startsWith('--', index)) {
|
|
51
|
+
const end = sql.indexOf('\n', index + 2);
|
|
52
|
+
return end === -1 ? sql.length : end + 1;
|
|
53
|
+
}
|
|
54
|
+
if (sql.startsWith('/*', index)) {
|
|
55
|
+
const end = sql.indexOf('*/', index + 2);
|
|
56
|
+
return end === -1 ? sql.length : end + 2;
|
|
57
|
+
}
|
|
58
|
+
return index;
|
|
59
|
+
}
|
|
60
|
+
// Backstop against statement smuggling (`select 1; drop table x`): reject any
|
|
61
|
+
// semicolon that is followed by real content. Semicolons inside string
|
|
62
|
+
// literals, quoted identifiers, and comments are fine, as are trailing
|
|
63
|
+
// semicolons (optionally followed by whitespace and comments). This deliberately
|
|
64
|
+
// lexes standard SQL only, so dialect-specific escapes can cause a false
|
|
65
|
+
// reject — never a false accept; the canonical gate is the daemon's
|
|
66
|
+
// sqlglot-backed validateReadOnly.
|
|
67
|
+
function assertSingleSqlStatement(sql) {
|
|
68
|
+
let index = 0;
|
|
69
|
+
let sawSemicolon = false;
|
|
70
|
+
while (index < sql.length) {
|
|
71
|
+
const skipped = skipQuotedOrComment(sql, index);
|
|
72
|
+
if (skipped > index) {
|
|
73
|
+
index = skipped;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (sql[index] === ';') {
|
|
77
|
+
sawSemicolon = true;
|
|
78
|
+
}
|
|
79
|
+
else if (sawSemicolon && !/\s/.test(sql[index])) {
|
|
80
|
+
throw new Error('Only one SQL statement can be executed.');
|
|
81
|
+
}
|
|
82
|
+
index += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
3
85
|
export function assertReadOnlySql(sql) {
|
|
4
|
-
const trimmed = sql.trim();
|
|
86
|
+
const trimmed = stripLeadingSqlComments(sql).trim();
|
|
5
87
|
if (!READ_SQL.test(trimmed) || MUTATING_SQL.test(trimmed)) {
|
|
6
88
|
throw new Error('Only read-only SELECT/WITH queries can be executed locally.');
|
|
7
89
|
}
|
|
90
|
+
assertSingleSqlStatement(trimmed);
|
|
8
91
|
return trimmed;
|
|
9
92
|
}
|
|
93
|
+
// `assertReadOnlySql` deliberately keeps trailing semicolons, comments, and
|
|
94
|
+
// whitespace (e.g. `select 1; -- done`) — harmless for direct single-statement
|
|
95
|
+
// execution. A row-limit subquery wrapper needs a bare expression instead: a
|
|
96
|
+
// trailing `;` would sit illegally inside the subquery, and a trailing line
|
|
97
|
+
// comment would comment out the closing paren and limit clause. Lex forward with
|
|
98
|
+
// the same standard-SQL rules as the single-statement gate and truncate at the
|
|
99
|
+
// end of the last meaningful token, dropping trailing semicolons, comments, and
|
|
100
|
+
// whitespace. Characters inside string literals and quoted identifiers stay
|
|
101
|
+
// meaningful, so a `;` or `--` within a literal is never mistaken for a
|
|
102
|
+
// terminator (a plain regex cannot make that distinction).
|
|
103
|
+
export function stripTrailingSqlNoise(sql) {
|
|
104
|
+
let index = 0;
|
|
105
|
+
let meaningfulEnd = 0;
|
|
106
|
+
while (index < sql.length) {
|
|
107
|
+
if (sql.startsWith('--', index) || sql.startsWith('/*', index)) {
|
|
108
|
+
index = skipQuotedOrComment(sql, index);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const afterQuoted = skipQuotedOrComment(sql, index);
|
|
112
|
+
if (afterQuoted > index) {
|
|
113
|
+
meaningfulEnd = afterQuoted;
|
|
114
|
+
index = afterQuoted;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (sql[index] !== ';' && !/\s/.test(sql[index] ?? '')) {
|
|
118
|
+
meaningfulEnd = index + 1;
|
|
119
|
+
}
|
|
120
|
+
index += 1;
|
|
121
|
+
}
|
|
122
|
+
return sql.slice(0, meaningfulEnd);
|
|
123
|
+
}
|
|
10
124
|
export function limitSqlForExecution(sql, maxRows) {
|
|
11
|
-
const trimmed = assertReadOnlySql(sql)
|
|
125
|
+
const trimmed = stripTrailingSqlNoise(assertReadOnlySql(sql));
|
|
12
126
|
if (!maxRows) {
|
|
13
127
|
return trimmed;
|
|
14
128
|
}
|