@equinor/fusion-framework-cli-plugin-ai-index 3.0.5 → 3.0.7
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/CHANGELOG.md +41 -0
- package/dist/esm/bin/apply-metadata.js +13 -5
- package/dist/esm/bin/apply-metadata.js.map +1 -1
- package/dist/esm/bin/apply-schema.js +7 -1
- package/dist/esm/bin/apply-schema.js.map +1 -1
- package/dist/esm/bin/apply-schema.test.js +2 -1
- package/dist/esm/bin/apply-schema.test.js.map +1 -1
- package/dist/esm/bin/{delete-removed-files.js → create-delete-removed-files-stream.js} +12 -1
- package/dist/esm/bin/create-delete-removed-files-stream.js.map +1 -0
- package/dist/esm/bin/embed.js +260 -97
- package/dist/esm/bin/embed.js.map +1 -1
- package/dist/esm/bin/execute-pipeline.js +5 -0
- package/dist/esm/bin/execute-pipeline.js.map +1 -1
- package/dist/esm/bin/get-diff.js +6 -0
- package/dist/esm/bin/get-diff.js.map +1 -1
- package/dist/esm/{embeddings-command.options.js → command-options-schema.js} +1 -1
- package/dist/esm/command-options-schema.js.map +1 -0
- package/dist/esm/{embeddings-command.js → command.js} +4 -2
- package/dist/esm/command.js.map +1 -0
- package/dist/esm/{create-command.js → create-index-command.js} +13 -5
- package/dist/esm/create-index-command.js.map +1 -0
- package/dist/esm/{schema.js → define-index-schema.js} +1 -1
- package/dist/esm/define-index-schema.js.map +1 -0
- package/dist/esm/delete-command.js +11 -2
- package/dist/esm/delete-command.js.map +1 -1
- package/dist/esm/delete-index-command.js +4 -0
- package/dist/esm/delete-index-command.js.map +1 -1
- package/dist/esm/{delete-command.options.js → delete-options-schema.js} +1 -1
- package/dist/esm/delete-options-schema.js.map +1 -0
- package/dist/esm/embed-command.js +3 -4
- package/dist/esm/embed-command.js.map +1 -1
- package/dist/esm/index.js +3 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/search-command.js +20 -0
- package/dist/esm/search-command.js.map +1 -1
- package/dist/esm/utils/git/{metadata.js → extract-git-metadata.js} +4 -2
- package/dist/esm/utils/git/extract-git-metadata.js.map +1 -0
- package/dist/esm/utils/git/get-changed-files.js +87 -0
- package/dist/esm/utils/git/get-changed-files.js.map +1 -0
- package/dist/esm/utils/git/{file-changes.js → get-file-status.js} +9 -95
- package/dist/esm/utils/git/get-file-status.js.map +1 -0
- package/dist/esm/utils/git/{status.js → get-git-status.js} +5 -2
- package/dist/esm/utils/git/get-git-status.js.map +1 -0
- package/dist/esm/utils/git/get-git.js +30 -0
- package/dist/esm/utils/git/get-git.js.map +1 -0
- package/dist/esm/utils/git/index.js +7 -4
- package/dist/esm/utils/git/index.js.map +1 -1
- package/dist/esm/utils/git/is-file-changed.js +19 -0
- package/dist/esm/utils/git/is-file-changed.js.map +1 -0
- package/dist/esm/utils/git/resolve-project-root.js +22 -0
- package/dist/esm/utils/git/resolve-project-root.js.map +1 -0
- package/dist/esm/utils/markdown/index.js +3 -1
- package/dist/esm/utils/markdown/index.js.map +1 -1
- package/dist/esm/utils/markdown/is-markdown-file.js +10 -0
- package/dist/esm/utils/markdown/is-markdown-file.js.map +1 -0
- package/dist/esm/utils/markdown/parse-markdown-file.js +29 -0
- package/dist/esm/utils/markdown/parse-markdown-file.js.map +1 -0
- package/dist/esm/utils/markdown/{parser.js → parse-markdown.js} +8 -37
- package/dist/esm/utils/markdown/parse-markdown.js.map +1 -0
- package/dist/esm/utils/{embedding-dimensions.js → resolve-embedding-dimensions.js} +3 -1
- package/dist/esm/utils/resolve-embedding-dimensions.js.map +1 -0
- package/dist/esm/utils/{package-resolver.js → resolve-package.js} +4 -1
- package/dist/esm/utils/resolve-package.js.map +1 -0
- package/dist/esm/utils/ts-doc/create-typescript-document.js +27 -0
- package/dist/esm/utils/ts-doc/create-typescript-document.js.map +1 -0
- package/dist/esm/utils/ts-doc/extract-document-from-class-node.js +86 -0
- package/dist/esm/utils/ts-doc/extract-document-from-class-node.js.map +1 -0
- package/dist/esm/utils/ts-doc/extract-document-from-node.js +82 -0
- package/dist/esm/utils/ts-doc/extract-document-from-node.js.map +1 -0
- package/dist/esm/utils/ts-doc/index.js +3 -1
- package/dist/esm/utils/ts-doc/index.js.map +1 -1
- package/dist/esm/utils/ts-doc/is-typescript-file.js +10 -0
- package/dist/esm/utils/ts-doc/is-typescript-file.js.map +1 -0
- package/dist/esm/utils/ts-doc/{constants.js → node-kinds.js} +1 -1
- package/dist/esm/utils/ts-doc/node-kinds.js.map +1 -0
- package/dist/esm/utils/ts-doc/parse-ts-doc-from-file-sync.js +23 -0
- package/dist/esm/utils/ts-doc/parse-ts-doc-from-file-sync.js.map +1 -0
- package/dist/esm/utils/ts-doc/parse-ts-doc-sync.js +18 -0
- package/dist/esm/utils/ts-doc/parse-ts-doc-sync.js.map +1 -0
- package/dist/esm/utils/ts-doc/process-source-file.js +22 -0
- package/dist/esm/utils/ts-doc/process-source-file.js.map +1 -0
- package/dist/esm/utils/zod-to-azure-fields.js +7 -0
- package/dist/esm/utils/zod-to-azure-fields.js.map +1 -1
- package/dist/esm/utils/zod-to-azure-fields.test.js +8 -7
- package/dist/esm/utils/zod-to-azure-fields.test.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/bin/apply-schema.d.ts +1 -1
- package/dist/types/config.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/utils/git/get-changed-files.d.ts +13 -0
- package/dist/types/utils/git/get-file-status.d.ts +13 -0
- package/dist/types/utils/git/{git-client.d.ts → get-git.d.ts} +0 -10
- package/dist/types/utils/git/index.d.ts +7 -4
- package/dist/types/utils/git/is-file-changed.d.ts +12 -0
- package/dist/types/utils/git/resolve-project-root.d.ts +10 -0
- package/dist/types/utils/markdown/index.d.ts +3 -1
- package/dist/types/utils/markdown/is-markdown-file.d.ts +7 -0
- package/dist/types/utils/markdown/parse-markdown-file.d.ts +14 -0
- package/dist/types/utils/markdown/parse-markdown.d.ts +14 -0
- package/dist/types/utils/ts-doc/create-typescript-document.d.ts +12 -0
- package/dist/types/utils/ts-doc/extract-document-from-class-node.d.ts +15 -0
- package/dist/types/utils/ts-doc/extract-document-from-node.d.ts +20 -0
- package/dist/types/utils/ts-doc/index.d.ts +3 -1
- package/dist/types/utils/ts-doc/is-typescript-file.d.ts +7 -0
- package/dist/types/utils/ts-doc/parse-ts-doc-from-file-sync.d.ts +14 -0
- package/dist/types/utils/ts-doc/parse-ts-doc-sync.d.ts +12 -0
- package/dist/types/utils/ts-doc/process-source-file.d.ts +11 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +8 -8
- package/src/bin/apply-metadata.ts +69 -57
- package/src/bin/apply-schema.test.ts +2 -1
- package/src/bin/apply-schema.ts +6 -1
- package/src/bin/{delete-removed-files.ts → create-delete-removed-files-stream.ts} +12 -1
- package/src/bin/embed.ts +389 -145
- package/src/bin/execute-pipeline.ts +5 -0
- package/src/bin/get-diff.ts +7 -1
- package/src/bin/types.ts +1 -1
- package/src/{embeddings-command.ts → command.ts} +3 -1
- package/src/config.ts +1 -1
- package/src/{create-command.ts → create-index-command.ts} +15 -5
- package/src/delete-command.ts +11 -2
- package/src/delete-index-command.ts +4 -0
- package/src/embed-command.ts +3 -6
- package/src/index.ts +3 -3
- package/src/search-command.ts +20 -0
- package/src/utils/git/{metadata.ts → extract-git-metadata.ts} +3 -1
- package/src/utils/git/get-changed-files.ts +93 -0
- package/src/utils/git/{file-changes.ts → get-file-status.ts} +9 -103
- package/src/utils/git/{status.ts → get-git-status.ts} +4 -1
- package/src/utils/git/{git-client.ts → get-git.ts} +4 -22
- package/src/utils/git/index.ts +7 -4
- package/src/utils/git/is-file-changed.ts +21 -0
- package/src/utils/git/resolve-project-root.ts +22 -0
- package/src/utils/markdown/index.ts +3 -1
- package/src/utils/markdown/is-markdown-file.ts +9 -0
- package/src/utils/markdown/parse-markdown-file.ts +36 -0
- package/src/utils/markdown/{parser.ts → parse-markdown.ts} +16 -51
- package/src/utils/{embedding-dimensions.ts → resolve-embedding-dimensions.ts} +2 -0
- package/src/utils/{package-resolver.ts → resolve-package.ts} +3 -0
- package/src/utils/ts-doc/create-typescript-document.ts +36 -0
- package/src/utils/ts-doc/extract-document-from-class-node.ts +106 -0
- package/src/utils/ts-doc/extract-document-from-node.ts +120 -0
- package/src/utils/ts-doc/index.ts +3 -1
- package/src/utils/ts-doc/is-typescript-file.ts +9 -0
- package/src/utils/ts-doc/parse-ts-doc-from-file-sync.ts +32 -0
- package/src/utils/ts-doc/parse-ts-doc-sync.ts +23 -0
- package/src/utils/ts-doc/process-source-file.ts +30 -0
- package/src/utils/zod-to-azure-fields.test.ts +10 -7
- package/src/utils/zod-to-azure-fields.ts +7 -0
- package/src/version.ts +1 -1
- package/dist/esm/bin/delete-removed-files.js.map +0 -1
- package/dist/esm/bin/file-stream.js +0 -22
- package/dist/esm/bin/file-stream.js.map +0 -1
- package/dist/esm/create-command.js.map +0 -1
- package/dist/esm/delete-command.options.js.map +0 -1
- package/dist/esm/embeddings-command.js.map +0 -1
- package/dist/esm/embeddings-command.options.js.map +0 -1
- package/dist/esm/schema.js.map +0 -1
- package/dist/esm/utils/embedding-dimensions.js.map +0 -1
- package/dist/esm/utils/git/file-changes.js.map +0 -1
- package/dist/esm/utils/git/git-client.js +0 -48
- package/dist/esm/utils/git/git-client.js.map +0 -1
- package/dist/esm/utils/git/metadata.js.map +0 -1
- package/dist/esm/utils/git/status.js.map +0 -1
- package/dist/esm/utils/markdown/parser.js.map +0 -1
- package/dist/esm/utils/package-resolver.js.map +0 -1
- package/dist/esm/utils/ts-doc/constants.js.map +0 -1
- package/dist/esm/utils/ts-doc/extractors.js +0 -189
- package/dist/esm/utils/ts-doc/extractors.js.map +0 -1
- package/dist/esm/utils/ts-doc/parser.js +0 -46
- package/dist/esm/utils/ts-doc/parser.js.map +0 -1
- package/dist/types/bin/file-stream.d.ts +0 -1
- package/dist/types/utils/git/file-changes.d.ts +0 -36
- package/dist/types/utils/markdown/parser.d.ts +0 -34
- package/dist/types/utils/ts-doc/extractors.d.ts +0 -42
- package/dist/types/utils/ts-doc/parser.d.ts +0 -32
- package/src/bin/file-stream.ts +0 -34
- package/src/utils/ts-doc/extractors.ts +0 -260
- package/src/utils/ts-doc/parser.ts +0 -60
- /package/dist/types/bin/{delete-removed-files.d.ts → create-delete-removed-files-stream.d.ts} +0 -0
- /package/dist/types/{embeddings-command.options.d.ts → command-options-schema.d.ts} +0 -0
- /package/dist/types/{embeddings-command.d.ts → command.d.ts} +0 -0
- /package/dist/types/{create-command.d.ts → create-index-command.d.ts} +0 -0
- /package/dist/types/{schema.d.ts → define-index-schema.d.ts} +0 -0
- /package/dist/types/{delete-command.options.d.ts → delete-options-schema.d.ts} +0 -0
- /package/dist/types/utils/git/{metadata.d.ts → extract-git-metadata.d.ts} +0 -0
- /package/dist/types/utils/git/{status.d.ts → get-git-status.d.ts} +0 -0
- /package/dist/types/utils/{embedding-dimensions.d.ts → resolve-embedding-dimensions.d.ts} +0 -0
- /package/dist/types/utils/{package-resolver.d.ts → resolve-package.d.ts} +0 -0
- /package/dist/types/utils/ts-doc/{constants.d.ts → node-kinds.d.ts} +0 -0
- /package/src/{embeddings-command.options.ts → command-options-schema.ts} +0 -0
- /package/src/{schema.ts → define-index-schema.ts} +0 -0
- /package/src/{delete-command.options.ts → delete-options-schema.ts} +0 -0
- /package/src/utils/ts-doc/{constants.ts → node-kinds.ts} +0 -0
|
@@ -4,6 +4,9 @@ import type { Observable } from 'rxjs';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Executes the pipeline and tracks results.
|
|
7
|
+
*
|
|
8
|
+
* @param deleteRemovedFiles$ - Stream of file-deletion results, processed before additions.
|
|
9
|
+
* @param updateVectorStore$ - Stream of document-addition results.
|
|
7
10
|
* @internal
|
|
8
11
|
*/
|
|
9
12
|
export function executePipeline(
|
|
@@ -22,10 +25,12 @@ export function executePipeline(
|
|
|
22
25
|
next: (result) => {
|
|
23
26
|
// Track deleted files by relative path
|
|
24
27
|
if (result.status === 'deleted') {
|
|
28
|
+
// Reduce each deleted file entry down to its relative path for reporting.
|
|
25
29
|
indexingResults.deleted.push(...result.files.map((file) => file.relativePath));
|
|
26
30
|
}
|
|
27
31
|
// Track added documents with source and ID (one file can produce multiple IDs)
|
|
28
32
|
else if (result.status === 'added') {
|
|
33
|
+
// Reduce each added document down to its source path and id for reporting.
|
|
29
34
|
indexingResults.added.push(
|
|
30
35
|
...result.documents.map((document) => ({
|
|
31
36
|
source: document.metadata.source,
|
package/src/bin/get-diff.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ChangedFile } from '../utils/git/index.js';
|
|
2
2
|
import { getChangedFiles, getGitStatus } from '../utils/git/index.js';
|
|
3
|
-
import type { CommandOptions } from '../
|
|
3
|
+
import type { CommandOptions } from '../command-options-schema.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Handles diff-based processing to get changed files from git.
|
|
7
|
+
*
|
|
8
|
+
* @param options - Command options controlling diff mode and base reference.
|
|
9
|
+
* @returns The list of changed files matching the configured patterns.
|
|
7
10
|
* @internal
|
|
8
11
|
*/
|
|
9
12
|
export async function getDiff(options: CommandOptions): Promise<ChangedFile[]> {
|
|
@@ -19,13 +22,16 @@ export async function getDiff(options: CommandOptions): Promise<ChangedFile[]> {
|
|
|
19
22
|
baseRef: options.baseRef,
|
|
20
23
|
});
|
|
21
24
|
|
|
25
|
+
// Bail out early when no changed files match the configured patterns.
|
|
22
26
|
if (changedFiles.length === 0) {
|
|
23
27
|
console.log('✅ No changed files match the provided patterns. Nothing to process.');
|
|
24
28
|
process.exit(0);
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
console.log(`📝 Found ${changedFiles.length} changed files matching patterns`);
|
|
32
|
+
// Log each changed file's status when --debug is set.
|
|
28
33
|
if (options.debug) {
|
|
34
|
+
// Print status/filepath pairs one at a time for readable debug output.
|
|
29
35
|
for (const file of changedFiles) {
|
|
30
36
|
console.debug(`[debug] ${file.status}: ${file.filepath}`);
|
|
31
37
|
}
|
package/src/bin/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { VectorStoreDocument } from '@equinor/fusion-framework-module-ai/lib';
|
|
2
2
|
import type { ChangedFile } from '../utils/git/index.js';
|
|
3
|
-
import type { CommandOptions } from '../
|
|
3
|
+
import type { CommandOptions } from '../command-options-schema.js';
|
|
4
4
|
import type { FrameworkInstance } from '@equinor/fusion-framework-cli-plugin-ai-base';
|
|
5
5
|
import type { FusionAIConfigWithIndex } from '../config.js';
|
|
6
6
|
|
|
@@ -4,7 +4,7 @@ import { loadFusionAIConfig, setupFramework } from '@equinor/fusion-framework-cl
|
|
|
4
4
|
import { withOptions as withAiOptions } from '@equinor/fusion-framework-cli-plugin-ai-base/command-options';
|
|
5
5
|
|
|
6
6
|
import { embed } from './bin/embed.js';
|
|
7
|
-
import { CommandOptionsSchema, type CommandOptions } from './
|
|
7
|
+
import { CommandOptionsSchema, type CommandOptions } from './command-options-schema.js';
|
|
8
8
|
import type { FusionAIConfigWithIndex } from './config.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -68,9 +68,11 @@ const _command = createCommand('add')
|
|
|
68
68
|
);
|
|
69
69
|
const indexConfig = config.index ?? {};
|
|
70
70
|
|
|
71
|
+
// Fall back to the configured index name only if the user didn't pass one explicitly
|
|
71
72
|
if (indexConfig.name && !opts.indexName?.trim()) {
|
|
72
73
|
thisCommand.setOptionValue('indexName', indexConfig.name);
|
|
73
74
|
}
|
|
75
|
+
// Fall back to the configured embed model only if the user didn't pass one explicitly
|
|
74
76
|
if (indexConfig.model && !opts.embedModel?.trim()) {
|
|
75
77
|
thisCommand.setOptionValue('embedModel', indexConfig.model);
|
|
76
78
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { VectorStoreDocument } from '@equinor/fusion-framework-module-ai/lib';
|
|
2
2
|
import type { FusionAIConfig } from '@equinor/fusion-framework-cli-plugin-ai-base';
|
|
3
|
-
import type { IndexSchemaConfig } from './schema.js';
|
|
3
|
+
import type { IndexSchemaConfig } from './define-index-schema.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Index-specific configuration for Fusion AI document indexing operations.
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import type { FusionAIConfigWithIndex } from './config.js';
|
|
10
10
|
import { zodToAzureFields } from './utils/zod-to-azure-fields.js';
|
|
11
|
-
import { resolveEmbeddingDimensions } from './utils/embedding-dimensions.js';
|
|
11
|
+
import { resolveEmbeddingDimensions } from './utils/resolve-embedding-dimensions.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* CLI command: `ai index create`
|
|
@@ -47,6 +47,7 @@ const _command = createCommand('create')
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const indexConfig = config.index;
|
|
50
|
+
// Config must define a schema via defineIndexSchema() before an index can be created
|
|
50
51
|
if (!indexConfig?.schema) {
|
|
51
52
|
console.error(
|
|
52
53
|
'❌ No schema defined in config. Add a `schema` property to `index` using defineIndexSchema().',
|
|
@@ -57,6 +58,7 @@ const _command = createCommand('create')
|
|
|
57
58
|
// Resolve index name from config
|
|
58
59
|
const indexName = indexConfig.name;
|
|
59
60
|
|
|
61
|
+
// Index name is required to know where to create/update the schema
|
|
60
62
|
if (!indexName) {
|
|
61
63
|
console.error('❌ Index name is required. Set `name` in the index config.');
|
|
62
64
|
process.exit(1);
|
|
@@ -67,10 +69,14 @@ const _command = createCommand('create')
|
|
|
67
69
|
|
|
68
70
|
// Guard against schema fields that collide with reserved base-schema names
|
|
69
71
|
const reservedFieldNames = ['id', 'content', 'content_vector', 'metadata'] as const;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
// Extract field names to check them against the reserved list
|
|
73
|
+
const schemaFieldNames = schemaFields.map((field) => field.name);
|
|
74
|
+
// Isolate the names that collide with reserved base-schema fields
|
|
75
|
+
const conflictingSchemaFields = schemaFieldNames.filter((name) =>
|
|
76
|
+
reservedFieldNames.includes(name as (typeof reservedFieldNames)[number]),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Fail fast so the user can rename the offending schema fields before creating the index
|
|
74
80
|
if (conflictingSchemaFields.length > 0) {
|
|
75
81
|
const conflicts = [...new Set(conflictingSchemaFields)].sort().join(', ');
|
|
76
82
|
console.error(
|
|
@@ -156,6 +162,7 @@ const _command = createCommand('create')
|
|
|
156
162
|
},
|
|
157
163
|
];
|
|
158
164
|
|
|
165
|
+
// Combine the base fields with any additional caller-supplied fields
|
|
159
166
|
const fullSchema = {
|
|
160
167
|
name: indexName,
|
|
161
168
|
fields: [...baseFields, ...schemaFields],
|
|
@@ -170,6 +177,7 @@ const _command = createCommand('create')
|
|
|
170
177
|
},
|
|
171
178
|
};
|
|
172
179
|
|
|
180
|
+
// Dry-run mode previews the schema without contacting the AI service
|
|
173
181
|
if (commandOptions.dryRun) {
|
|
174
182
|
console.log('📋 Index schema preview (dry-run):');
|
|
175
183
|
console.log(JSON.stringify(fullSchema, null, 2));
|
|
@@ -186,6 +194,7 @@ const _command = createCommand('create')
|
|
|
186
194
|
const scopes = service.scopes ?? service.defaultScopes ?? [];
|
|
187
195
|
const token = await framework.auth.acquireAccessToken({ request: { scopes } });
|
|
188
196
|
|
|
197
|
+
// Cannot call the AI service without a valid access token
|
|
189
198
|
if (!token) {
|
|
190
199
|
console.error('❌ Failed to acquire access token for the AI service.');
|
|
191
200
|
process.exit(1);
|
|
@@ -201,6 +210,7 @@ const _command = createCommand('create')
|
|
|
201
210
|
body: JSON.stringify(fullSchema),
|
|
202
211
|
});
|
|
203
212
|
|
|
213
|
+
// Surface the service error response so the user can diagnose the failure
|
|
204
214
|
if (!response.ok) {
|
|
205
215
|
const body = await response.text();
|
|
206
216
|
console.error(`❌ Index creation failed (${response.status} ${response.statusText})`);
|
package/src/delete-command.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { loadFusionAIConfig, setupFramework } from '@equinor/fusion-framework-cl
|
|
|
4
4
|
import { withOptions as withAiOptions } from '@equinor/fusion-framework-cli-plugin-ai-base/command-options';
|
|
5
5
|
import type { FusionAIConfigWithIndex } from './config.js';
|
|
6
6
|
|
|
7
|
-
import { DeleteOptionsSchema, type DeleteOptions } from './delete-
|
|
7
|
+
import { DeleteOptionsSchema, type DeleteOptions } from './delete-options-schema.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Builds an OData filter expression from source paths and/or a raw filter.
|
|
@@ -19,8 +19,11 @@ import { DeleteOptionsSchema, type DeleteOptions } from './delete-command.option
|
|
|
19
19
|
* sources nor a raw filter were provided.
|
|
20
20
|
*/
|
|
21
21
|
function buildFilter(sources: string[], rawFilter?: string): string | undefined {
|
|
22
|
+
// Source-path filters take precedence over a raw filter to avoid broad deletions
|
|
22
23
|
if (sources.length > 0) {
|
|
23
|
-
|
|
24
|
+
// Build one OData clause per source path before combining them
|
|
25
|
+
const sourceClauses = sources.map((s) => `metadata/source eq '${s}'`);
|
|
26
|
+
return sourceClauses.join(' or ');
|
|
24
27
|
}
|
|
25
28
|
return rawFilter;
|
|
26
29
|
}
|
|
@@ -79,9 +82,11 @@ const _command = createCommand('remove')
|
|
|
79
82
|
);
|
|
80
83
|
const indexConfig = config.index ?? {};
|
|
81
84
|
|
|
85
|
+
// Fall back to the configured index name only if the user didn't pass one explicitly
|
|
82
86
|
if (indexConfig.name && !opts.indexName?.trim()) {
|
|
83
87
|
thisCommand.setOptionValue('indexName', indexConfig.name);
|
|
84
88
|
}
|
|
89
|
+
// Fall back to the configured embed model only if the user didn't pass one explicitly
|
|
85
90
|
if (indexConfig.model && !opts.embedModel?.trim()) {
|
|
86
91
|
thisCommand.setOptionValue('embedModel', indexConfig.model);
|
|
87
92
|
}
|
|
@@ -90,14 +95,17 @@ const _command = createCommand('remove')
|
|
|
90
95
|
const options = await DeleteOptionsSchema.parseAsync(commandOptions);
|
|
91
96
|
const filterExpression = buildFilter(sources, options.filter);
|
|
92
97
|
|
|
98
|
+
// Refuse to run an unbounded delete when neither sources nor a filter were given
|
|
93
99
|
if (!filterExpression) {
|
|
94
100
|
throw new Error(
|
|
95
101
|
'Nothing to delete. Provide source file paths as arguments or pass a --filter expression.',
|
|
96
102
|
);
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
// Show the user exactly what will be targeted before deleting
|
|
99
106
|
if (sources.length > 0) {
|
|
100
107
|
console.log(`\nTargeting ${sources.length} source path(s):\n`);
|
|
108
|
+
// List each source path so the user can verify the target set
|
|
101
109
|
for (const src of sources.sort()) {
|
|
102
110
|
console.log(` ${src}`);
|
|
103
111
|
}
|
|
@@ -105,6 +113,7 @@ const _command = createCommand('remove')
|
|
|
105
113
|
console.log(`\nFilter: ${filterExpression}`);
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
// Dry-run mode previews the filter without deleting anything
|
|
108
117
|
if (options.dryRun) {
|
|
109
118
|
console.log('\n🔍 Dry run — no documents were deleted.');
|
|
110
119
|
console.log(` Would apply filter: ${filterExpression}`);
|
|
@@ -54,6 +54,7 @@ const _command = createCommand('delete')
|
|
|
54
54
|
) {
|
|
55
55
|
const indexName = commandOptions.name?.trim();
|
|
56
56
|
|
|
57
|
+
// Cannot proceed without knowing which index to delete
|
|
57
58
|
if (!indexName) {
|
|
58
59
|
console.error('❌ Index name is required. Set `name` in the index config or pass --name.');
|
|
59
60
|
process.exit(1);
|
|
@@ -73,6 +74,7 @@ const _command = createCommand('delete')
|
|
|
73
74
|
});
|
|
74
75
|
rl.close();
|
|
75
76
|
|
|
77
|
+
// Abort unless the user re-typed the exact index name
|
|
76
78
|
if (answer.trim() !== indexName) {
|
|
77
79
|
console.log('❌ Confirmation did not match. Aborting.');
|
|
78
80
|
process.exit(1);
|
|
@@ -85,6 +87,7 @@ const _command = createCommand('delete')
|
|
|
85
87
|
const scopes = service.scopes ?? service.defaultScopes ?? [];
|
|
86
88
|
const token = await framework.auth.acquireAccessToken({ request: { scopes } });
|
|
87
89
|
|
|
90
|
+
// Cannot call the AI service without a valid access token
|
|
88
91
|
if (!token) {
|
|
89
92
|
console.error('❌ Failed to acquire access token for the AI service.');
|
|
90
93
|
process.exit(1);
|
|
@@ -104,6 +107,7 @@ const _command = createCommand('delete')
|
|
|
104
107
|
process.exit(1);
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
// Surface the service error response so the user can diagnose the failure
|
|
107
111
|
if (!response.ok) {
|
|
108
112
|
const body = await response.text();
|
|
109
113
|
console.error(`❌ Index deletion failed (${response.status} ${response.statusText})`);
|
package/src/embed-command.ts
CHANGED
|
@@ -33,12 +33,9 @@ export const embedCommand = withAiOptions(
|
|
|
33
33
|
const vector = await embedder.embedQuery(text);
|
|
34
34
|
|
|
35
35
|
console.log(`Dimensions: ${vector.length}`);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.map((v) => v.toFixed(6))
|
|
40
|
-
.join(', ')}, ...]`,
|
|
41
|
-
);
|
|
36
|
+
// Format the first 8 vector components for a readable preview
|
|
37
|
+
const previewValues = vector.slice(0, 8).map((v) => v.toFixed(6));
|
|
38
|
+
console.log(`Vector (first 8): [${previewValues.join(', ')}, ...]`);
|
|
42
39
|
}),
|
|
43
40
|
{ includeEmbedding: true },
|
|
44
41
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { createCommand } from 'commander';
|
|
3
3
|
import { registerAiPlugin as registerAiPluginBase } from '@equinor/fusion-framework-cli-plugin-ai-base';
|
|
4
|
-
import { command as addCommand } from './
|
|
4
|
+
import { command as addCommand } from './command.js';
|
|
5
5
|
import { deleteCommand as removeCommand } from './delete-command.js';
|
|
6
6
|
import { searchCommand } from './search-command.js';
|
|
7
7
|
import { embedCommand } from './embed-command.js';
|
|
8
|
-
import { createIndexCommand } from './create-command.js';
|
|
8
|
+
import { createIndexCommand } from './create-index-command.js';
|
|
9
9
|
import { deleteIndexCommand } from './delete-index-command.js';
|
|
10
10
|
|
|
11
11
|
export { FusionAIConfigWithIndex, IndexConfig } from './config.js';
|
|
12
|
-
export { defineIndexSchema, IndexSchemaConfig } from './schema.js';
|
|
12
|
+
export { defineIndexSchema, IndexSchemaConfig } from './define-index-schema.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Parent command for the `ai index` group.
|
package/src/search-command.ts
CHANGED
|
@@ -49,9 +49,12 @@ type CommandOptions = AiOptions & {
|
|
|
49
49
|
const normalizeMetadata = (metadata: Record<string, unknown>): Record<string, unknown> => {
|
|
50
50
|
const normalized = { ...metadata };
|
|
51
51
|
|
|
52
|
+
// Only Azure Search documents with an attributes array need flattening
|
|
52
53
|
if (Array.isArray(normalized.attributes)) {
|
|
53
54
|
const attributesObj: Record<string, unknown> = {};
|
|
55
|
+
// Convert each { key, value } pair into a flat map entry
|
|
54
56
|
for (const attr of normalized.attributes) {
|
|
57
|
+
// Skip malformed attribute entries that don't match the expected shape
|
|
55
58
|
if (
|
|
56
59
|
typeof attr === 'object' &&
|
|
57
60
|
attr !== null &&
|
|
@@ -66,6 +69,7 @@ const normalizeMetadata = (metadata: Record<string, unknown>): Record<string, un
|
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
}
|
|
72
|
+
// Flatten the normalized attribute map back onto the parent object
|
|
69
73
|
Object.assign(normalized, attributesObj);
|
|
70
74
|
delete normalized.attributes;
|
|
71
75
|
}
|
|
@@ -134,30 +138,36 @@ const _command = createCommand('search')
|
|
|
134
138
|
);
|
|
135
139
|
const indexConfig = config.index ?? {};
|
|
136
140
|
|
|
141
|
+
// Fall back to the configured index name only if the user didn't pass one explicitly
|
|
137
142
|
if (indexConfig.name && !opts.indexName?.trim()) {
|
|
138
143
|
thisCommand.setOptionValue('indexName', indexConfig.name);
|
|
139
144
|
}
|
|
145
|
+
// Fall back to the configured embed model only if the user didn't pass one explicitly
|
|
140
146
|
if (indexConfig.model && !opts.embedModel?.trim()) {
|
|
141
147
|
thisCommand.setOptionValue('embedModel', indexConfig.model);
|
|
142
148
|
}
|
|
143
149
|
})
|
|
144
150
|
.action(async (query: string, options: CommandOptions) => {
|
|
151
|
+
// Surface progress in verbose mode before the (potentially slow) framework setup
|
|
145
152
|
if (options.verbose) {
|
|
146
153
|
console.log('🔍 Initializing framework...');
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
const framework = await setupFramework(options);
|
|
150
157
|
|
|
158
|
+
// Cannot search without knowing which index to query
|
|
151
159
|
if (!options.indexName) {
|
|
152
160
|
throw new Error('Index name is required');
|
|
153
161
|
}
|
|
154
162
|
|
|
163
|
+
// Print the resolved search parameters in verbose mode
|
|
155
164
|
if (options.verbose) {
|
|
156
165
|
console.log('✅ Framework initialized successfully');
|
|
157
166
|
console.log(`📇 Index: ${options.indexName}`);
|
|
158
167
|
console.log(`🔎 Searching for: "${query}"`);
|
|
159
168
|
console.log(`📊 Limit: ${options.limit}`);
|
|
160
169
|
console.log(`🔍 Search type: ${options.searchType}`);
|
|
170
|
+
// Only show the filter line when a filter was actually supplied
|
|
161
171
|
if (options.filter) {
|
|
162
172
|
console.log(`🔧 Filter: ${options.filter}`);
|
|
163
173
|
}
|
|
@@ -185,14 +195,18 @@ const _command = createCommand('search')
|
|
|
185
195
|
const retriever = vectorStoreService.asRetriever(retrieverOptions);
|
|
186
196
|
const results = await retriever.invoke(query);
|
|
187
197
|
|
|
198
|
+
// Defensively validate the retriever response shape before iterating it
|
|
188
199
|
if (!results || !Array.isArray(results)) {
|
|
189
200
|
throw new Error(
|
|
190
201
|
`Invalid search results: expected array but got ${results === null ? 'null' : typeof results}`,
|
|
191
202
|
);
|
|
192
203
|
}
|
|
193
204
|
|
|
205
|
+
// JSON output mode prints each document as a structured object
|
|
194
206
|
if (options.json) {
|
|
207
|
+
// Emit one JSON entry per matching document
|
|
195
208
|
for (const doc of results) {
|
|
209
|
+
// Raw mode preserves Azure Search's native metadata structure
|
|
196
210
|
if (options.raw) {
|
|
197
211
|
console.log(inspect(doc, { depth: null, colors: true }));
|
|
198
212
|
} else {
|
|
@@ -205,6 +219,7 @@ const _command = createCommand('search')
|
|
|
205
219
|
}
|
|
206
220
|
}
|
|
207
221
|
} else {
|
|
222
|
+
// Nothing further to render when the search returned no matches
|
|
208
223
|
if (results.length === 0) {
|
|
209
224
|
console.log('❌ No results found');
|
|
210
225
|
return;
|
|
@@ -212,6 +227,7 @@ const _command = createCommand('search')
|
|
|
212
227
|
|
|
213
228
|
console.log(`✅ Found ${results.length} result${results.length !== 1 ? 's' : ''}:\n`);
|
|
214
229
|
|
|
230
|
+
// Render each matched document as human-readable text
|
|
215
231
|
results.forEach((doc: Document, index: number) => {
|
|
216
232
|
const processedMetadata = options.raw
|
|
217
233
|
? (doc.metadata as Record<string, unknown>)
|
|
@@ -230,8 +246,10 @@ const _command = createCommand('search')
|
|
|
230
246
|
);
|
|
231
247
|
console.log(`Source: ${source}`);
|
|
232
248
|
|
|
249
|
+
// Only print extra metadata fields in verbose mode
|
|
233
250
|
if (options.verbose) {
|
|
234
251
|
const { source: _, score: __, ...otherMetadata } = metadata;
|
|
252
|
+
// Skip the empty-object noise when there's no extra metadata to show
|
|
235
253
|
if (Object.keys(otherMetadata).length > 0) {
|
|
236
254
|
console.log(`Metadata:`, JSON.stringify(otherMetadata, null, 2));
|
|
237
255
|
}
|
|
@@ -240,6 +258,7 @@ const _command = createCommand('search')
|
|
|
240
258
|
|
|
241
259
|
const content = doc.pageContent;
|
|
242
260
|
const maxLength = 500;
|
|
261
|
+
// Truncate long content so terminal output stays readable
|
|
243
262
|
if (content.length > maxLength) {
|
|
244
263
|
console.log(`${content.substring(0, maxLength)}...`);
|
|
245
264
|
console.log(`\n[Content truncated - ${content.length} characters total]`);
|
|
@@ -255,6 +274,7 @@ const _command = createCommand('search')
|
|
|
255
274
|
console.error(
|
|
256
275
|
`❌ Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
257
276
|
);
|
|
277
|
+
// Print the stack trace in verbose mode to aid debugging
|
|
258
278
|
if (options.verbose && error instanceof Error && error.stack) {
|
|
259
279
|
console.error(error.stack);
|
|
260
280
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { relative } from 'node:path';
|
|
2
2
|
import type { GitMetadata } from './types.js';
|
|
3
|
-
import { getGit } from './git
|
|
3
|
+
import { getGit } from './get-git.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Generate a GitHub permalink for a file
|
|
@@ -16,6 +16,7 @@ const generateGithubPermalink = (
|
|
|
16
16
|
slug?: string,
|
|
17
17
|
): string | undefined => {
|
|
18
18
|
const githubMatch = gitRemoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
19
|
+
// Only GitHub-hosted remotes can be turned into a permalink URL
|
|
19
20
|
if (githubMatch) {
|
|
20
21
|
const [, owner, repo] = githubMatch;
|
|
21
22
|
return `https://github.com/${owner}/${repo}/blob/${slug ?? 'main'}/${filePath}`;
|
|
@@ -34,6 +35,7 @@ const generateGithubPermalink = (
|
|
|
34
35
|
*/
|
|
35
36
|
export const extractGitMetadata = async (filePath: string): Promise<GitMetadata | undefined> => {
|
|
36
37
|
const { git, gitRepoPath: gitRepoRoot } = getGit(filePath) ?? {};
|
|
38
|
+
// No metadata can be extracted outside of a git repository
|
|
37
39
|
if (!git || !gitRepoRoot) {
|
|
38
40
|
return undefined;
|
|
39
41
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ChangedFile, FileChangeStatus, GitDiffOptions } from './types.js';
|
|
2
|
+
import { resolveProjectRoot } from './resolve-project-root.js';
|
|
3
|
+
import { getGit } from './get-git.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a list of files changed between `baseRef` and HEAD.
|
|
7
|
+
*
|
|
8
|
+
* Parses the output of `git diff --name-status` to classify each file as
|
|
9
|
+
* `'new'`, `'modified'`, or `'removed'`. Renames are expanded into a
|
|
10
|
+
* `'removed'` entry for the old path and a `'new'` entry for the new path.
|
|
11
|
+
*
|
|
12
|
+
* @param options - Configuration controlling the diff reference and working directory.
|
|
13
|
+
* @returns Array of changed files with their status.
|
|
14
|
+
* @throws {Error} If the working directory is not inside a git repository.
|
|
15
|
+
*/
|
|
16
|
+
export const getChangedFiles = async (options: GitDiffOptions): Promise<ChangedFile[]> => {
|
|
17
|
+
const { diff, baseRef = 'HEAD~1', cwd = process.cwd() } = options;
|
|
18
|
+
|
|
19
|
+
// Skip git entirely when diff mode wasn't requested
|
|
20
|
+
if (!diff) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
25
|
+
// Diff mode requires a git repository to resolve the project root against
|
|
26
|
+
if (!projectRoot) {
|
|
27
|
+
throw new Error('Not in a git repository. Cannot use --diff option.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { git } = getGit(cwd) ?? {};
|
|
31
|
+
// Cannot proceed without a working git client
|
|
32
|
+
if (!git) {
|
|
33
|
+
throw new Error('Failed to initialize git client');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Get changes since baseRef with status (A=added, M=modified, D=deleted)
|
|
38
|
+
try {
|
|
39
|
+
const diffResult = await git.diff([`${baseRef}`, '--name-status']);
|
|
40
|
+
// Drop blank lines left by the trailing newline in git's output
|
|
41
|
+
const lines = diffResult.split('\n').filter((line) => line.trim() !== '');
|
|
42
|
+
|
|
43
|
+
const changedFiles: ChangedFile[] = [];
|
|
44
|
+
|
|
45
|
+
// Classify each changed line by its git status code
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
// Match status and file path
|
|
48
|
+
// Format: "A\tfile.ts" or "M\tfile.ts" or "D\tfile.ts"
|
|
49
|
+
// Also handle renames: "R100\told.ts\tnew.ts"
|
|
50
|
+
const renameMatch = line.match(/^R\d*\s+(.+?)\s+(.+)$/);
|
|
51
|
+
// Expand a detected rename into a removed old-path entry and a new-path entry
|
|
52
|
+
if (renameMatch) {
|
|
53
|
+
const [, oldFile, newFile] = renameMatch;
|
|
54
|
+
// Add both the removed old file and the new file
|
|
55
|
+
changedFiles.push({ filepath: `${projectRoot}/${oldFile}`, status: 'removed' });
|
|
56
|
+
changedFiles.push({ filepath: `${projectRoot}/${newFile}`, status: 'new' });
|
|
57
|
+
// Rename already handled above; skip the add/modify/delete matching below
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const match = line.match(/^([AMD])\s+(.+)$/);
|
|
62
|
+
// Only lines matching the single-letter add/modify/delete status format are actionable
|
|
63
|
+
if (match) {
|
|
64
|
+
const [, gitStatus, file] = match;
|
|
65
|
+
const fullPath = `${projectRoot}/${file}`;
|
|
66
|
+
|
|
67
|
+
let status: FileChangeStatus;
|
|
68
|
+
// Map git's single-letter status codes to our ChangedFile status values
|
|
69
|
+
if (gitStatus === 'A') {
|
|
70
|
+
status = 'new';
|
|
71
|
+
} else if (gitStatus === 'M') {
|
|
72
|
+
status = 'modified';
|
|
73
|
+
} else if (gitStatus === 'D') {
|
|
74
|
+
status = 'removed';
|
|
75
|
+
} else {
|
|
76
|
+
// Skip unknown statuses (C=copied, etc.)
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
changedFiles.push({ filepath: fullPath, status });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return changedFiles;
|
|
85
|
+
} catch {
|
|
86
|
+
// Handle case where baseRef doesn't exist (e.g., first commit)
|
|
87
|
+
console.warn(`⚠️ Warning: Git reference '${baseRef}' not found. Processing all files.`);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(`Git diff failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
92
|
+
}
|
|
93
|
+
};
|