@kaelio/ktx 0.1.0 → 0.2.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.
Files changed (122) hide show
  1. package/assets/python/{kaelio_ktx-0.1.0-py3-none-any.whl → kaelio_ktx-0.2.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/admin-reindex.d.ts +15 -0
  4. package/dist/admin-reindex.js +168 -0
  5. package/dist/admin-reindex.test.js +116 -0
  6. package/dist/{dev.d.ts → admin.d.ts} +1 -1
  7. package/dist/{dev.js → admin.js} +14 -12
  8. package/dist/admin.test.d.ts +1 -0
  9. package/dist/{dev.test.js → admin.test.js} +36 -31
  10. package/dist/cli-program.js +7 -7
  11. package/dist/cli-program.test.js +1 -1
  12. package/dist/cli-runtime.d.ts +2 -0
  13. package/dist/commands/connection-commands.js +11 -10
  14. package/dist/commands/connection-selection.d.ts +11 -0
  15. package/dist/commands/connection-selection.js +9 -0
  16. package/dist/commands/ingest-commands.js +32 -26
  17. package/dist/commands/knowledge-commands.js +17 -28
  18. package/dist/commands/mcp-commands.js +17 -11
  19. package/dist/commands/setup-commands.js +14 -26
  20. package/dist/commands/sl-commands.js +27 -32
  21. package/dist/doctor.test.js +7 -8
  22. package/dist/example-smoke.test.js +3 -3
  23. package/dist/index.test.js +102 -70
  24. package/dist/ingest-depth.js +0 -1
  25. package/dist/ingest.test-utils.js +2 -2
  26. package/dist/ingest.test.js +4 -4
  27. package/dist/io/print-list.test.js +4 -4
  28. package/dist/knowledge.js +1 -1
  29. package/dist/managed-local-embeddings.d.ts +2 -0
  30. package/dist/managed-local-embeddings.js +2 -0
  31. package/dist/managed-local-embeddings.test.js +2 -0
  32. package/dist/managed-mcp-daemon.js +3 -2
  33. package/dist/managed-mcp-daemon.test.js +25 -0
  34. package/dist/managed-python-command.js +2 -2
  35. package/dist/managed-python-command.test.js +4 -3
  36. package/dist/managed-python-daemon.js +3 -2
  37. package/dist/managed-python-daemon.test.js +20 -0
  38. package/dist/managed-python-runtime.d.ts +5 -1
  39. package/dist/managed-python-runtime.js +50 -6
  40. package/dist/managed-python-runtime.test.js +53 -23
  41. package/dist/memory-flow-tui.test.js +2 -2
  42. package/dist/next-steps.d.ts +6 -6
  43. package/dist/next-steps.js +4 -4
  44. package/dist/next-steps.test.js +5 -5
  45. package/dist/print-command-tree.test.js +1 -1
  46. package/dist/proxy-env.d.ts +1 -0
  47. package/dist/proxy-env.js +23 -0
  48. package/dist/proxy-env.test.d.ts +1 -0
  49. package/dist/proxy-env.test.js +17 -0
  50. package/dist/public-ingest.js +3 -5
  51. package/dist/public-ingest.test.js +7 -3
  52. package/dist/runtime.test.js +2 -1
  53. package/dist/scan.test.js +2 -2
  54. package/dist/setup-agents.js +6 -4
  55. package/dist/setup-agents.test.js +35 -1
  56. package/dist/setup-embeddings.d.ts +1 -0
  57. package/dist/setup-embeddings.js +29 -7
  58. package/dist/setup-embeddings.test.js +49 -7
  59. package/dist/setup-models.d.ts +0 -1
  60. package/dist/setup-models.js +2 -3
  61. package/dist/setup-models.test.js +8 -10
  62. package/dist/setup-project.d.ts +9 -1
  63. package/dist/setup-project.js +52 -25
  64. package/dist/setup-project.test.js +8 -8
  65. package/dist/setup-runtime.test.js +4 -2
  66. package/dist/setup.d.ts +1 -2
  67. package/dist/setup.js +21 -5
  68. package/dist/setup.test.js +160 -43
  69. package/dist/sl.js +1 -1
  70. package/dist/sl.test.js +2 -1
  71. package/dist/standalone-smoke.test.js +8 -5
  72. package/dist/status-project.js +1 -10
  73. package/node_modules/@ktx/context/dist/index-sync/index.d.ts +2 -0
  74. package/node_modules/@ktx/context/dist/index-sync/index.js +1 -0
  75. package/node_modules/@ktx/context/dist/index-sync/reindex.d.ts +20 -0
  76. package/node_modules/@ktx/context/dist/index-sync/reindex.js +141 -0
  77. package/node_modules/@ktx/context/dist/index-sync/reindex.test.d.ts +1 -0
  78. package/node_modules/@ktx/context/dist/index-sync/reindex.test.js +139 -0
  79. package/node_modules/@ktx/context/dist/index-sync/types.d.ts +29 -0
  80. package/node_modules/@ktx/context/dist/index-sync/types.js +1 -0
  81. package/node_modules/@ktx/context/dist/index.d.ts +1 -0
  82. package/node_modules/@ktx/context/dist/index.js +1 -0
  83. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +1 -1
  84. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +8 -8
  85. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +4 -1
  86. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +3 -3
  87. package/node_modules/@ktx/context/dist/ingest/local-embedding-provider.integration.test.js +9 -10
  88. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +2 -2
  89. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +2 -2
  90. package/node_modules/@ktx/context/dist/llm/local-config.js +2 -15
  91. package/node_modules/@ktx/context/dist/llm/local-config.test.js +3 -7
  92. package/node_modules/@ktx/context/dist/memory/local-memory.js +9 -3
  93. package/node_modules/@ktx/context/dist/project/config.d.ts +0 -5
  94. package/node_modules/@ktx/context/dist/project/config.js +5 -5
  95. package/node_modules/@ktx/context/dist/project/config.test.js +4 -7
  96. package/node_modules/@ktx/context/dist/scan/enrichment-state.test.js +4 -4
  97. package/node_modules/@ktx/context/dist/scan/index.d.ts +1 -1
  98. package/node_modules/@ktx/context/dist/scan/local-enrichment.d.ts +2 -6
  99. package/node_modules/@ktx/context/dist/scan/local-enrichment.js +31 -47
  100. package/node_modules/@ktx/context/dist/scan/local-enrichment.test.js +35 -18
  101. package/node_modules/@ktx/context/dist/scan/local-scan.test.js +2 -3
  102. package/node_modules/@ktx/context/dist/sl/ports.d.ts +3 -3
  103. package/node_modules/@ktx/context/dist/sl/sl-search.service.d.ts +3 -2
  104. package/node_modules/@ktx/context/dist/sl/sl-search.service.js +47 -45
  105. package/node_modules/@ktx/context/dist/sl/sl-search.service.test.js +61 -0
  106. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.d.ts +4 -3
  107. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.js +15 -5
  108. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.test.js +24 -0
  109. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.d.ts +3 -2
  110. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.js +62 -51
  111. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.test.js +59 -3
  112. package/node_modules/@ktx/context/dist/wiki/ports.d.ts +3 -3
  113. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.d.ts +33 -0
  114. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.js +155 -2
  115. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.test.js +26 -0
  116. package/node_modules/@ktx/context/package.json +5 -0
  117. package/node_modules/@ktx/llm/dist/embedding-provider.d.ts +0 -7
  118. package/node_modules/@ktx/llm/dist/embedding-provider.js +12 -138
  119. package/node_modules/@ktx/llm/dist/embedding-provider.test.js +10 -25
  120. package/node_modules/@ktx/llm/dist/types.d.ts +1 -1
  121. package/package.json +1 -1
  122. /package/dist/{dev.test.d.ts → admin-reindex.test.d.ts} +0 -0
@@ -2,10 +2,10 @@
2
2
  "schemaVersion": 1,
3
3
  "distributionName": "kaelio-ktx",
4
4
  "normalizedName": "kaelio_ktx",
5
- "version": "0.1.0",
5
+ "version": "0.2.0",
6
6
  "wheel": {
7
- "file": "kaelio_ktx-0.1.0-py3-none-any.whl",
8
- "sha256": "1145cf0c90879270dc335a8d0ca49a598f81e0f47a37fdf646fc6c6798fe3c8b",
9
- "bytes": 80523
7
+ "file": "kaelio_ktx-0.2.0-py3-none-any.whl",
8
+ "sha256": "5ede628d4d72b2a7eb515fb538473be5a5d82eb430ae98daca8ac90596a7a6e3",
9
+ "bytes": 80524
10
10
  }
11
11
  }
@@ -0,0 +1,15 @@
1
+ import { type ReindexSummary } from '@ktx/context/index-sync';
2
+ import { type Command } from '@commander-js/extra-typings';
3
+ import type { KtxCliCommandContext } from './cli-program.js';
4
+ import type { KtxCliIo } from './cli-runtime.js';
5
+ export interface KtxAdminReindexArgs {
6
+ projectDir: string;
7
+ force: boolean;
8
+ output?: 'pretty' | 'plain' | 'json';
9
+ json?: boolean;
10
+ cliVersion: string;
11
+ }
12
+ export declare function registerAdminReindexCommand(admin: Command, context: KtxCliCommandContext): void;
13
+ export declare function reindexHasErrors(summary: ReindexSummary): boolean;
14
+ export declare function renderReindexPlain(summary: ReindexSummary, io: KtxCliIo): void;
15
+ export declare function renderReindexJson(summary: ReindexSummary, io: KtxCliIo): void;
@@ -0,0 +1,168 @@
1
+ import { createLocalKtxEmbeddingProviderFromConfig, KtxIngestEmbeddingPortAdapter, MANAGED_SENTENCE_TRANSFORMERS_BASE_URL, } from '@ktx/context';
2
+ import { reindexLocalIndexes } from '@ktx/context/index-sync';
3
+ import { loadKtxProject } from '@ktx/context/project';
4
+ import { Option } from '@commander-js/extra-typings';
5
+ import { cancel, intro, log, note, outro } from '@clack/prompts';
6
+ import { resolveOutputMode } from './io/mode.js';
7
+ import { green, red, SYMBOLS } from './io/symbols.js';
8
+ import { ensureManagedLocalEmbeddingsDaemon } from './managed-local-embeddings.js';
9
+ export function registerAdminReindexCommand(admin, context) {
10
+ admin
11
+ .command('reindex')
12
+ .description('Sync local wiki and semantic-layer search indexes from disk')
13
+ .option('--force', 'Clear each discovered scope before rebuilding it', false)
14
+ .option('--json', 'Shortcut for --output=json (overrides --output)', false)
15
+ .addOption(new Option('--output <mode>', 'Output mode: pretty, plain, or json').choices(['pretty', 'plain', 'json']))
16
+ .action(async (options, command) => {
17
+ const runner = context.deps.adminReindex ?? runKtxAdminReindex;
18
+ const { resolveCommandProjectDir } = await import('./cli-program.js');
19
+ context.setExitCode(await runner({
20
+ projectDir: resolveCommandProjectDir(command),
21
+ force: options.force === true,
22
+ json: options.json === true,
23
+ output: options.output,
24
+ cliVersion: context.packageInfo.version,
25
+ }, context.io));
26
+ });
27
+ }
28
+ async function resolveReindexEmbeddingService(project, args, io) {
29
+ const config = project.config.ingest.embeddings;
30
+ if (config.backend === 'none') {
31
+ return null;
32
+ }
33
+ if (config.backend === 'sentence-transformers' &&
34
+ config.sentenceTransformers?.base_url === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL) {
35
+ const daemon = await ensureManagedLocalEmbeddingsDaemon({
36
+ cliVersion: args.cliVersion,
37
+ projectDir: project.projectDir,
38
+ installPolicy: 'never',
39
+ io,
40
+ });
41
+ const provider = createLocalKtxEmbeddingProviderFromConfig(config, { env: { ...process.env, ...daemon.env } });
42
+ return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
43
+ }
44
+ const provider = createLocalKtxEmbeddingProviderFromConfig(config);
45
+ return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
46
+ }
47
+ function scopeKey(scope) {
48
+ if (scope.kind === 'wiki') {
49
+ return scope.scope === 'user' ? `wiki/user/${scope.scopeId ?? 'local'}` : 'wiki/global';
50
+ }
51
+ return `sl/${scope.connectionId ?? scope.label}`;
52
+ }
53
+ function quotePlainValue(value) {
54
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
55
+ }
56
+ export function reindexHasErrors(summary) {
57
+ return summary.scopes.some((scope) => scope.error);
58
+ }
59
+ export function renderReindexPlain(summary, io) {
60
+ const updateKey = summary.force ? 'rebuilt' : 'updated';
61
+ for (const scope of summary.scopes) {
62
+ const cells = [
63
+ scopeKey(scope),
64
+ `scanned=${scope.scanned}`,
65
+ `${updateKey}=${scope.updated}`,
66
+ `deleted=${scope.deleted}`,
67
+ `embeddings=${summary.embeddingsAvailable ? String(scope.embeddingsRecomputed) : '-'}`,
68
+ `duration_ms=${scope.durationMs}`,
69
+ ...(scope.error ? [`error=${quotePlainValue(scope.error)}`] : []),
70
+ ];
71
+ io.stderr.write(`${cells.join('\t')}\n`);
72
+ }
73
+ const failed = summary.scopes.filter((scope) => scope.error).length;
74
+ io.stdout.write([
75
+ 'reindex',
76
+ `scopes=${summary.scopes.length}`,
77
+ `scanned=${summary.totals.scanned}`,
78
+ `${updateKey}=${summary.totals.updated}`,
79
+ `deleted=${summary.totals.deleted}`,
80
+ `embeddings=${summary.embeddingsAvailable ? String(summary.totals.embeddingsRecomputed) : '-'}`,
81
+ `duration_ms=${summary.durationMs}`,
82
+ ...(failed > 0 ? [`failed=${failed}`] : []),
83
+ ].join('\t') + '\n');
84
+ }
85
+ export function renderReindexJson(summary, io) {
86
+ io.stdout.write(`${JSON.stringify({ kind: 'reindex', data: summary, meta: { command: 'admin reindex' } }, null, 2)}\n`);
87
+ }
88
+ function noun(scope) {
89
+ return scope.kind === 'wiki' ? 'pages' : 'sources';
90
+ }
91
+ function formatScopeLine(scope, force, embeddingsAvailable) {
92
+ if (scope.error) {
93
+ return `${scope.kind === 'wiki' ? 'Wiki' : 'SL'}: ${scope.label} ${SYMBOLS.emDash} failed: ${scope.error}`;
94
+ }
95
+ const changedLabel = force ? 'rebuilt' : 'updated';
96
+ const parts = [`${scope.scanned} ${noun(scope)}`];
97
+ if (scope.updated > 0) {
98
+ parts.push(`${scope.updated} ${changedLabel}`);
99
+ }
100
+ else {
101
+ parts.push('unchanged');
102
+ }
103
+ if (!force && scope.deleted > 0) {
104
+ parts.push(`${scope.deleted} deleted`);
105
+ }
106
+ if (embeddingsAvailable) {
107
+ parts.push(`${scope.embeddingsRecomputed} embeddings recomputed`);
108
+ }
109
+ parts.push(`${scope.durationMs}ms`);
110
+ return `${scope.kind === 'wiki' ? 'Wiki' : 'SL'}: ${scope.label} ${SYMBOLS.emDash} ${parts.join(` ${SYMBOLS.middot} `)}`;
111
+ }
112
+ function renderReindexPretty(summary, io) {
113
+ intro(summary.force ? 'ktx admin reindex --force' : 'ktx admin reindex');
114
+ if (!summary.embeddingsAvailable) {
115
+ log.warn(`Embeddings: not configured ${SYMBOLS.emDash} indexing lexical only`);
116
+ }
117
+ for (const scope of summary.scopes) {
118
+ const line = formatScopeLine(scope, summary.force, summary.embeddingsAvailable);
119
+ if (scope.error) {
120
+ log.error(red(line));
121
+ }
122
+ else {
123
+ log.success(green(line));
124
+ }
125
+ }
126
+ const failed = summary.scopes.filter((scope) => scope.error).length;
127
+ note([
128
+ `scopes ${summary.scopes.length}`,
129
+ `scanned ${summary.totals.scanned}`,
130
+ `${summary.force ? 'rebuilt' : 'updated'} ${summary.totals.updated}`,
131
+ `deleted ${summary.totals.deleted}`,
132
+ `embeddings ${summary.embeddingsAvailable ? summary.totals.embeddingsRecomputed : SYMBOLS.emDash}`,
133
+ `index ${summary.dbPath}`,
134
+ ...(failed > 0 ? [`failed ${failed}`] : []),
135
+ ].join('\n'), 'Summary');
136
+ if (failed > 0) {
137
+ cancel(`reindex completed with ${failed} error${failed === 1 ? '' : 's'}`);
138
+ }
139
+ else {
140
+ outro(`Done in ${(summary.durationMs / 1000).toFixed(1)}s`);
141
+ }
142
+ void io;
143
+ }
144
+ async function runKtxAdminReindex(args, io = process) {
145
+ try {
146
+ const project = await loadKtxProject({ projectDir: args.projectDir });
147
+ const embeddingService = await resolveReindexEmbeddingService(project, args, io);
148
+ const summary = await reindexLocalIndexes(project, { force: args.force, embeddingService });
149
+ const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
150
+ if (!summary.embeddingsAvailable && mode === 'plain') {
151
+ io.stderr.write(`Embeddings: not configured ${SYMBOLS.emDash} indexing lexical only\n`);
152
+ }
153
+ if (mode === 'json') {
154
+ renderReindexJson(summary, io);
155
+ }
156
+ else if (mode === 'plain') {
157
+ renderReindexPlain(summary, io);
158
+ }
159
+ else {
160
+ renderReindexPretty(summary, io);
161
+ }
162
+ return reindexHasErrors(summary) ? 1 : 0;
163
+ }
164
+ catch (error) {
165
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
166
+ return 1;
167
+ }
168
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { renderReindexJson, renderReindexPlain, reindexHasErrors } from './admin-reindex.js';
3
+ import { runKtxCli } from './index.js';
4
+ function makeIo(options = {}) {
5
+ let stdout = '';
6
+ let stderr = '';
7
+ return {
8
+ io: {
9
+ stdout: {
10
+ isTTY: options.stdoutIsTTY,
11
+ write: (chunk) => {
12
+ stdout += chunk;
13
+ },
14
+ },
15
+ stderr: {
16
+ write: (chunk) => {
17
+ stderr += chunk;
18
+ },
19
+ },
20
+ },
21
+ stdout: () => stdout,
22
+ stderr: () => stderr,
23
+ };
24
+ }
25
+ function summary(overrides = {}) {
26
+ return {
27
+ scopes: [
28
+ {
29
+ kind: 'wiki',
30
+ label: 'global',
31
+ scope: 'global',
32
+ scopeId: null,
33
+ scanned: 42,
34
+ updated: 3,
35
+ deleted: 1,
36
+ embeddingsRecomputed: 3,
37
+ embeddingsFailed: 0,
38
+ durationMs: 412,
39
+ },
40
+ {
41
+ kind: 'sl',
42
+ label: 'warehouse',
43
+ connectionId: 'warehouse',
44
+ scanned: 18,
45
+ updated: 2,
46
+ deleted: 0,
47
+ embeddingsRecomputed: 2,
48
+ embeddingsFailed: 0,
49
+ durationMs: 287,
50
+ },
51
+ ],
52
+ totals: { scanned: 60, updated: 5, deleted: 1, embeddingsRecomputed: 5, embeddingsFailed: 0 },
53
+ dbPath: '.ktx/db.sqlite',
54
+ force: false,
55
+ embeddingsAvailable: true,
56
+ durationMs: 1234,
57
+ ...overrides,
58
+ };
59
+ }
60
+ describe('admin reindex renderers', () => {
61
+ it('renders plain scope lines to stderr and summary to stdout', () => {
62
+ const io = makeIo();
63
+ renderReindexPlain(summary(), io.io);
64
+ expect(io.stderr()).toContain('wiki/global\tscanned=42\tupdated=3\tdeleted=1\tembeddings=3\tduration_ms=412\n');
65
+ expect(io.stderr()).toContain('sl/warehouse\tscanned=18\tupdated=2\tdeleted=0\tembeddings=2\tduration_ms=287\n');
66
+ expect(io.stdout()).toBe('reindex\tscopes=2\tscanned=60\tupdated=5\tdeleted=1\tembeddings=5\tduration_ms=1234\n');
67
+ });
68
+ it('renders rebuilt labels in plain force mode', () => {
69
+ const io = makeIo();
70
+ renderReindexPlain(summary({ force: true }), io.io);
71
+ expect(io.stderr()).toContain('rebuilt=3');
72
+ expect(io.stdout()).toContain('rebuilt=5');
73
+ expect(io.stdout()).not.toContain('updated=5');
74
+ });
75
+ it('renders json envelope to stdout only', () => {
76
+ const io = makeIo();
77
+ renderReindexJson(summary(), io.io);
78
+ expect(JSON.parse(io.stdout())).toMatchObject({
79
+ kind: 'reindex',
80
+ data: { totals: { scanned: 60, updated: 5 } },
81
+ meta: { command: 'admin reindex' },
82
+ });
83
+ expect(io.stderr()).toBe('');
84
+ });
85
+ it('detects per-scope errors', () => {
86
+ expect(reindexHasErrors(summary({
87
+ scopes: [{ ...summary().scopes[0], error: 'provider failed' }],
88
+ }))).toBe(true);
89
+ });
90
+ });
91
+ describe('admin reindex Commander routing', () => {
92
+ it('routes flags to the injectable reindex runner', async () => {
93
+ const { mkdir, mkdtemp, rm, writeFile } = await import('node:fs/promises');
94
+ const { tmpdir } = await import('node:os');
95
+ const { join } = await import('node:path');
96
+ const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-reindex-cli-'));
97
+ const projectDir = join(tempDir, 'project');
98
+ const io = makeIo();
99
+ const adminReindex = vi.fn(async () => 0);
100
+ try {
101
+ await mkdir(projectDir, { recursive: true });
102
+ await writeFile(join(projectDir, 'ktx.yaml'), '{}\n', 'utf-8');
103
+ await expect(runKtxCli(['--project-dir', projectDir, 'admin', 'reindex', '--force', '--json', '--output', 'plain'], io.io, { adminReindex })).resolves.toBe(0);
104
+ }
105
+ finally {
106
+ await rm(tempDir, { recursive: true, force: true });
107
+ }
108
+ expect(adminReindex).toHaveBeenCalledWith({
109
+ projectDir,
110
+ force: true,
111
+ json: true,
112
+ output: 'plain',
113
+ cliVersion: '0.1.0-rc.1',
114
+ }, io.io);
115
+ });
116
+ });
@@ -1,3 +1,3 @@
1
1
  import type { Command } from '@commander-js/extra-typings';
2
2
  import { type KtxCliCommandContext } from './cli-program.js';
3
- export declare function registerDevCommands(program: Command, context: KtxCliCommandContext): void;
3
+ export declare function registerAdminCommands(program: Command, context: KtxCliCommandContext): void;
@@ -1,21 +1,22 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { resolveCommandProjectDir } from './cli-program.js';
3
+ import { registerAdminReindexCommand } from './admin-reindex.js';
3
4
  import { registerRuntimeCommands } from './commands/runtime-commands.js';
4
5
  import { profileMark } from './startup-profile.js';
5
- profileMark('module:dev');
6
- export function registerDevCommands(program, context) {
7
- const dev = program
8
- .command('dev')
9
- .description('Low-level project initialization and runtime management')
6
+ profileMark('module:admin');
7
+ export function registerAdminCommands(program, context) {
8
+ const admin = program
9
+ .command('admin')
10
+ .description('Low-level project initialization, runtime, and index management')
10
11
  .showHelpAfterError();
11
- dev.hook('preAction', (_thisCommand, actionCommand) => {
12
- context.writeDebug?.('dev', actionCommand);
12
+ admin.hook('preAction', (_thisCommand, actionCommand) => {
13
+ context.writeDebug?.('admin', actionCommand);
13
14
  });
14
- dev.action(() => {
15
- dev.outputHelp();
15
+ admin.action(() => {
16
+ admin.outputHelp();
16
17
  context.setExitCode(0);
17
18
  });
18
- dev
19
+ admin
19
20
  .command('init')
20
21
  .description('Initialize a Git-backed KTX project directory for maintenance scripts')
21
22
  .argument('[directory]', 'Project directory')
@@ -26,7 +27,7 @@ export function registerDevCommands(program, context) {
26
27
  force: commandOptions.force === true,
27
28
  }, context.io));
28
29
  });
29
- dev
30
+ admin
30
31
  .command('schema')
31
32
  .description('Print a JSON Schema describing ktx.yaml (for editors and LLM agents)')
32
33
  .option('--output <file>', 'Write the schema to a file instead of stdout')
@@ -44,5 +45,6 @@ export function registerDevCommands(program, context) {
44
45
  }
45
46
  context.setExitCode(0);
46
47
  });
47
- registerRuntimeCommands(dev, context);
48
+ registerRuntimeCommands(admin, context);
49
+ registerAdminReindexCommand(admin, context);
48
50
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -20,12 +20,12 @@ function makeIo() {
20
20
  stderr: () => stderr,
21
21
  };
22
22
  }
23
- describe('dev Commander tree', () => {
24
- it('prints visible dev help with only supported low-level command groups', async () => {
23
+ describe('admin Commander tree', () => {
24
+ it('prints visible admin help with supported low-level command groups', async () => {
25
25
  const testIo = makeIo();
26
- await expect(runKtxCli(['dev', '--help'], testIo.io)).resolves.toBe(0);
27
- expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
28
- for (const command of ['init', 'runtime']) {
26
+ await expect(runKtxCli(['admin', '--help'], testIo.io)).resolves.toBe(0);
27
+ expect(testIo.stdout()).toContain('Usage: ktx admin [options] [command]');
28
+ for (const command of ['init', 'runtime', 'reindex']) {
29
29
  expect(testIo.stdout()).toContain(command);
30
30
  }
31
31
  for (const removed of [
@@ -47,23 +47,28 @@ describe('dev Commander tree', () => {
47
47
  }
48
48
  expect(testIo.stderr()).toBe('');
49
49
  });
50
- it('lists dev in root command rows', async () => {
50
+ it('lists admin in root command rows', async () => {
51
51
  const testIo = makeIo();
52
52
  await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
53
53
  expect(testIo.stdout()).not.toContain('Advanced:');
54
- expect(testIo.stdout()).toContain('dev');
55
- expect(testIo.stdout()).toMatch(/Low-level project initialization and runtime\s+management/);
54
+ expect(testIo.stdout()).toContain('admin');
55
+ expect(testIo.stdout()).toMatch(/Low-level project initialization,\s+runtime,\s+and index management/);
56
56
  expect(testIo.stderr()).toBe('');
57
57
  });
58
- it('keeps project scaffolding under dev init', async () => {
58
+ it('does not keep a dev alias', async () => {
59
+ const testIo = makeIo();
60
+ await expect(runKtxCli(['dev', '--help'], testIo.io)).resolves.toBe(1);
61
+ expect(testIo.stderr()).toContain("unknown command 'dev'");
62
+ });
63
+ it('keeps project scaffolding under admin init', async () => {
59
64
  const { mkdtemp, readFile, rm } = await import('node:fs/promises');
60
65
  const { tmpdir } = await import('node:os');
61
66
  const { join } = await import('node:path');
62
- const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-init-'));
67
+ const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-init-'));
63
68
  const projectDir = join(tempDir, 'warehouse');
64
69
  const testIo = makeIo();
65
70
  try {
66
- await expect(runKtxCli(['dev', 'init', projectDir], testIo.io)).resolves.toBe(0);
71
+ await expect(runKtxCli(['admin', 'init', projectDir], testIo.io)).resolves.toBe(0);
67
72
  expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
68
73
  await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.not.toContain('project:');
69
74
  expect(testIo.stderr()).toBe('');
@@ -72,15 +77,15 @@ describe('dev Commander tree', () => {
72
77
  await rm(tempDir, { recursive: true, force: true });
73
78
  }
74
79
  });
75
- it('uses global project-dir for dev init when the positional directory is omitted', async () => {
80
+ it('uses global project-dir for admin init when the positional directory is omitted', async () => {
76
81
  const { mkdtemp, rm } = await import('node:fs/promises');
77
82
  const { tmpdir } = await import('node:os');
78
83
  const { join } = await import('node:path');
79
- const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-init-global-'));
84
+ const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-init-global-'));
80
85
  const projectDir = join(tempDir, 'global-init');
81
86
  const testIo = makeIo();
82
87
  try {
83
- await expect(runKtxCli(['--project-dir', projectDir, 'dev', 'init'], testIo.io)).resolves.toBe(0);
88
+ await expect(runKtxCli(['--project-dir', projectDir, 'admin', 'init'], testIo.io)).resolves.toBe(0);
84
89
  expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
85
90
  expect(testIo.stderr()).toBe('');
86
91
  }
@@ -92,13 +97,13 @@ describe('dev Commander tree', () => {
92
97
  const { mkdtemp, rm } = await import('node:fs/promises');
93
98
  const { tmpdir } = await import('node:os');
94
99
  const { join } = await import('node:path');
95
- const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-schema-'));
100
+ const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-schema-'));
96
101
  const missingProjectDir = join(tempDir, 'missing-project');
97
102
  const originalProjectDir = process.env.KTX_PROJECT_DIR;
98
103
  const testIo = makeIo();
99
104
  try {
100
105
  process.env.KTX_PROJECT_DIR = missingProjectDir;
101
- await expect(runKtxCli(['dev', 'schema'], testIo.io)).resolves.toBe(0);
106
+ await expect(runKtxCli(['admin', 'schema'], testIo.io)).resolves.toBe(0);
102
107
  expect(JSON.parse(testIo.stdout())).toMatchObject({
103
108
  title: 'ktx.yaml',
104
109
  type: 'object',
@@ -115,19 +120,19 @@ describe('dev Commander tree', () => {
115
120
  await rm(tempDir, { recursive: true, force: true });
116
121
  }
117
122
  });
118
- it('rejects removed dev command groups', async () => {
123
+ it('rejects removed admin command groups', async () => {
119
124
  for (const argv of [
120
- ['dev', 'doctor', 'setup'],
121
- ['dev', 'runtime', 'doctor'],
122
- ['dev', 'runtime', 'prune', '--dry-run'],
123
- ['dev', 'scan', 'warehouse'],
124
- ['dev', 'ingest', 'run'],
125
- ['dev', 'mapping', 'list'],
126
- ['dev', 'completion', 'zsh'],
127
- ['dev', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', ''],
128
- ['dev', 'knowledge', 'list'],
129
- ['dev', 'model', 'list'],
130
- ['dev', 'artifacts'],
125
+ ['admin', 'doctor', 'setup'],
126
+ ['admin', 'runtime', 'doctor'],
127
+ ['admin', 'runtime', 'prune', '--dry-run'],
128
+ ['admin', 'scan', 'warehouse'],
129
+ ['admin', 'ingest', 'run'],
130
+ ['admin', 'mapping', 'list'],
131
+ ['admin', 'completion', 'zsh'],
132
+ ['admin', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', ''],
133
+ ['admin', 'knowledge', 'list'],
134
+ ['admin', 'model', 'list'],
135
+ ['admin', 'artifacts'],
131
136
  ]) {
132
137
  const testIo = makeIo();
133
138
  await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1);
@@ -136,8 +141,8 @@ describe('dev Commander tree', () => {
136
141
  });
137
142
  it.each([
138
143
  {
139
- argv: ['dev', 'runtime', '--help'],
140
- expected: ['Usage: ktx dev runtime', 'install', 'start', 'stop', 'status'],
144
+ argv: ['admin', 'runtime', '--help'],
145
+ expected: ['Usage: ktx admin runtime', 'install', 'start', 'stop', 'status'],
141
146
  },
142
147
  ])('prints generated nested help for $argv', async ({ argv, expected }) => {
143
148
  const io = makeIo();
@@ -146,7 +151,7 @@ describe('dev Commander tree', () => {
146
151
  for (const text of expected) {
147
152
  expect(io.stdout()).toContain(text);
148
153
  }
149
- if (argv.join(' ') === 'dev runtime --help') {
154
+ if (argv.join(' ') === 'admin runtime --help') {
150
155
  expect(io.stdout()).not.toContain('prune');
151
156
  expect(io.stdout()).not.toContain('doctor');
152
157
  }
@@ -9,14 +9,14 @@ import { registerSetupCommands } from './commands/setup-commands.js';
9
9
  import { registerSlCommands } from './commands/sl-commands.js';
10
10
  import { registerSqlCommands } from './commands/sql-commands.js';
11
11
  import { registerStatusCommands } from './commands/status-commands.js';
12
- import { registerDevCommands } from './dev.js';
12
+ import { registerAdminCommands } from './admin.js';
13
13
  import { renderMissingProjectMessage } from './doctor.js';
14
14
  import { findNearestKtxProjectDir, resolveKtxProjectDir } from './project-resolver.js';
15
15
  import { profileMark, profileSpan } from './startup-profile.js';
16
16
  profileMark('module:cli-program');
17
17
  const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'sql', 'status', 'mcp']);
18
- const PROJECT_INDEPENDENT_DEV_COMMANDS = new Set(['runtime', 'schema']);
19
- const COMMANDS_THAT_CREATE_PROJECT = new Set(['setup', 'ktx dev init']);
18
+ const PROJECT_INDEPENDENT_ADMIN_COMMANDS = new Set(['runtime', 'schema']);
19
+ const COMMANDS_THAT_CREATE_PROJECT = new Set(['setup', 'ktx admin init']);
20
20
  const COMMANDS_WITH_OWN_MISSING_PROJECT_HANDLING = new Set(['status']);
21
21
  const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']);
22
22
  const GLOBAL_OPTIONS_WITHOUT_VALUE = new Set(['--debug', '--help', '-h', '--version', '-v']);
@@ -105,14 +105,14 @@ function isProjectAwareCommand(path) {
105
105
  return false;
106
106
  }
107
107
  const rootCommand = path[1];
108
- if (rootCommand === 'dev') {
109
- return path[2] !== undefined && !PROJECT_INDEPENDENT_DEV_COMMANDS.has(path[2]);
108
+ if (rootCommand === 'admin') {
109
+ return path[2] !== undefined && !PROJECT_INDEPENDENT_ADMIN_COMMANDS.has(path[2]);
110
110
  }
111
111
  return rootCommand !== undefined && PROJECT_AWARE_ROOT_COMMANDS.has(rootCommand);
112
112
  }
113
113
  function shouldSuppressProjectDirLine(path, options) {
114
114
  const commandPathKey = path.join(' ');
115
- if (commandPathKey === 'ktx dev init') {
115
+ if (commandPathKey === 'ktx admin init') {
116
116
  return true;
117
117
  }
118
118
  if (commandPathKey === 'ktx setup') {
@@ -315,7 +315,7 @@ export function buildKtxProgram(options) {
315
315
  registerSqlCommands(program, context);
316
316
  registerStatusCommands(program, context);
317
317
  registerMcpCommands(program, context);
318
- registerDevCommands(program, context);
318
+ registerAdminCommands(program, context);
319
319
  return program;
320
320
  }
321
321
  export async function runCommanderKtxCli(argv, io, deps, info, options) {
@@ -25,7 +25,7 @@ describe('buildKtxProgram', () => {
25
25
  });
26
26
  expect(program.name()).toBe('ktx');
27
27
  const topLevel = program.commands.map((command) => command.name()).sort();
28
- for (const expected of ['setup', 'connection', 'ingest', 'sl', 'dev']) {
28
+ for (const expected of ['setup', 'connection', 'ingest', 'sl', 'admin']) {
29
29
  expect(topLevel).toContain(expected);
30
30
  }
31
31
  });
@@ -1,4 +1,5 @@
1
1
  import type { KtxConnectionArgs } from './connection.js';
2
+ import type { KtxAdminReindexArgs } from './admin-reindex.js';
2
3
  import type { KtxDoctorArgs } from './doctor.js';
3
4
  import type { KtxKnowledgeArgs } from './knowledge.js';
4
5
  import type { KtxPublicIngestArgs } from './public-ingest.js';
@@ -25,6 +26,7 @@ export interface KtxCliIo {
25
26
  };
26
27
  }
27
28
  export interface KtxCliDeps {
29
+ adminReindex?: (args: KtxAdminReindexArgs, io: KtxCliIo) => Promise<number>;
28
30
  setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise<number>;
29
31
  connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise<number>;
30
32
  doctor?: (args: KtxDoctorArgs, io: KtxCliIo) => Promise<number>;
@@ -1,5 +1,6 @@
1
1
  import { resolveCommandProjectDir } from '../cli-program.js';
2
2
  import { profileMark } from '../startup-profile.js';
3
+ import { resolveConnectionSelection } from './connection-selection.js';
3
4
  profileMark('module:commands/connection-commands');
4
5
  async function runConnectionArgs(context, args) {
5
6
  const runner = context.deps.connection ?? (await import('../connection.js')).runKtxConnection;
@@ -10,7 +11,10 @@ export function registerConnectionCommands(program, context, commandName = 'conn
10
11
  .command(commandName)
11
12
  .description('List and test configured connections')
12
13
  .showHelpAfterError()
13
- .addHelpText('after', '\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the nearest ktx.yaml or current working directory.\n');
14
+ .addHelpText('after', '\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the nearest ktx.yaml or current working directory.\n')
15
+ .action(async (_options, command) => {
16
+ await runConnectionArgs(context, { command: 'list', projectDir: resolveCommandProjectDir(command) });
17
+ });
14
18
  connection.hook('preAction', (_thisCommand, actionCommand) => {
15
19
  context.writeDebug?.(commandName, actionCommand);
16
20
  });
@@ -22,25 +26,22 @@ export function registerConnectionCommands(program, context, commandName = 'conn
22
26
  });
23
27
  connection
24
28
  .command('test')
25
- .description('Test a configured connection')
26
- .argument('[connectionId]', 'KTX connection id (omit when --all is set)')
29
+ .description('Test one or all configured connections (default: all)')
30
+ .argument('[connectionId]', 'KTX connection id to test (omit to test all)')
27
31
  .option('--all', 'Test every configured connection and print a summary list')
28
32
  .action(async (connectionId, options, command) => {
29
- const all = options.all === true;
30
- if (all && connectionId !== undefined) {
33
+ if (options.all === true && connectionId !== undefined) {
31
34
  command.error('error: --all cannot be combined with a connection id argument');
32
35
  }
33
- if (!all && connectionId === undefined) {
34
- command.error('error: missing required argument <connectionId> (or pass --all)');
35
- }
36
- if (all) {
36
+ const selection = resolveConnectionSelection({ connectionId, all: options.all === true });
37
+ if (selection.kind === 'all') {
37
38
  await runConnectionArgs(context, { command: 'test-all', projectDir: resolveCommandProjectDir(command) });
38
39
  return;
39
40
  }
40
41
  await runConnectionArgs(context, {
41
42
  command: 'test',
42
43
  projectDir: resolveCommandProjectDir(command),
43
- connectionId: connectionId,
44
+ connectionId: selection.connectionId,
44
45
  });
45
46
  });
46
47
  }
@@ -0,0 +1,11 @@
1
+ export type ConnectionSelection = {
2
+ kind: 'all';
3
+ } | {
4
+ kind: 'single';
5
+ connectionId: string;
6
+ };
7
+ export interface ResolveConnectionSelectionInput {
8
+ connectionId?: string | undefined;
9
+ all: boolean;
10
+ }
11
+ export declare function resolveConnectionSelection(input: ResolveConnectionSelectionInput): ConnectionSelection;