@equinor/fusion-framework-cli-plugin-ai-index 3.0.6 → 3.0.8

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 (195) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/esm/bin/apply-metadata.js +13 -5
  3. package/dist/esm/bin/apply-metadata.js.map +1 -1
  4. package/dist/esm/bin/apply-schema.js +7 -1
  5. package/dist/esm/bin/apply-schema.js.map +1 -1
  6. package/dist/esm/bin/apply-schema.test.js +2 -1
  7. package/dist/esm/bin/apply-schema.test.js.map +1 -1
  8. package/dist/esm/bin/{delete-removed-files.js → create-delete-removed-files-stream.js} +12 -1
  9. package/dist/esm/bin/create-delete-removed-files-stream.js.map +1 -0
  10. package/dist/esm/bin/embed.js +260 -97
  11. package/dist/esm/bin/embed.js.map +1 -1
  12. package/dist/esm/bin/execute-pipeline.js +5 -0
  13. package/dist/esm/bin/execute-pipeline.js.map +1 -1
  14. package/dist/esm/bin/get-diff.js +6 -0
  15. package/dist/esm/bin/get-diff.js.map +1 -1
  16. package/dist/esm/{embeddings-command.options.js → command-options-schema.js} +1 -1
  17. package/dist/esm/command-options-schema.js.map +1 -0
  18. package/dist/esm/{embeddings-command.js → command.js} +4 -2
  19. package/dist/esm/command.js.map +1 -0
  20. package/dist/esm/{create-command.js → create-index-command.js} +13 -5
  21. package/dist/esm/create-index-command.js.map +1 -0
  22. package/dist/esm/{schema.js → define-index-schema.js} +1 -1
  23. package/dist/esm/define-index-schema.js.map +1 -0
  24. package/dist/esm/delete-command.js +11 -2
  25. package/dist/esm/delete-command.js.map +1 -1
  26. package/dist/esm/delete-index-command.js +4 -0
  27. package/dist/esm/delete-index-command.js.map +1 -1
  28. package/dist/esm/{delete-command.options.js → delete-options-schema.js} +1 -1
  29. package/dist/esm/delete-options-schema.js.map +1 -0
  30. package/dist/esm/embed-command.js +3 -4
  31. package/dist/esm/embed-command.js.map +1 -1
  32. package/dist/esm/index.js +3 -3
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/search-command.js +20 -0
  35. package/dist/esm/search-command.js.map +1 -1
  36. package/dist/esm/utils/git/{metadata.js → extract-git-metadata.js} +4 -2
  37. package/dist/esm/utils/git/extract-git-metadata.js.map +1 -0
  38. package/dist/esm/utils/git/get-changed-files.js +87 -0
  39. package/dist/esm/utils/git/get-changed-files.js.map +1 -0
  40. package/dist/esm/utils/git/{file-changes.js → get-file-status.js} +9 -95
  41. package/dist/esm/utils/git/get-file-status.js.map +1 -0
  42. package/dist/esm/utils/git/{status.js → get-git-status.js} +5 -2
  43. package/dist/esm/utils/git/get-git-status.js.map +1 -0
  44. package/dist/esm/utils/git/get-git.js +30 -0
  45. package/dist/esm/utils/git/get-git.js.map +1 -0
  46. package/dist/esm/utils/git/index.js +7 -4
  47. package/dist/esm/utils/git/index.js.map +1 -1
  48. package/dist/esm/utils/git/is-file-changed.js +19 -0
  49. package/dist/esm/utils/git/is-file-changed.js.map +1 -0
  50. package/dist/esm/utils/git/resolve-project-root.js +22 -0
  51. package/dist/esm/utils/git/resolve-project-root.js.map +1 -0
  52. package/dist/esm/utils/markdown/index.js +3 -1
  53. package/dist/esm/utils/markdown/index.js.map +1 -1
  54. package/dist/esm/utils/markdown/is-markdown-file.js +10 -0
  55. package/dist/esm/utils/markdown/is-markdown-file.js.map +1 -0
  56. package/dist/esm/utils/markdown/parse-markdown-file.js +29 -0
  57. package/dist/esm/utils/markdown/parse-markdown-file.js.map +1 -0
  58. package/dist/esm/utils/markdown/{parser.js → parse-markdown.js} +8 -37
  59. package/dist/esm/utils/markdown/parse-markdown.js.map +1 -0
  60. package/dist/esm/utils/{embedding-dimensions.js → resolve-embedding-dimensions.js} +3 -1
  61. package/dist/esm/utils/resolve-embedding-dimensions.js.map +1 -0
  62. package/dist/esm/utils/{package-resolver.js → resolve-package.js} +4 -1
  63. package/dist/esm/utils/resolve-package.js.map +1 -0
  64. package/dist/esm/utils/ts-doc/create-typescript-document.js +27 -0
  65. package/dist/esm/utils/ts-doc/create-typescript-document.js.map +1 -0
  66. package/dist/esm/utils/ts-doc/extract-document-from-class-node.js +86 -0
  67. package/dist/esm/utils/ts-doc/extract-document-from-class-node.js.map +1 -0
  68. package/dist/esm/utils/ts-doc/extract-document-from-node.js +82 -0
  69. package/dist/esm/utils/ts-doc/extract-document-from-node.js.map +1 -0
  70. package/dist/esm/utils/ts-doc/index.js +3 -1
  71. package/dist/esm/utils/ts-doc/index.js.map +1 -1
  72. package/dist/esm/utils/ts-doc/is-typescript-file.js +10 -0
  73. package/dist/esm/utils/ts-doc/is-typescript-file.js.map +1 -0
  74. package/dist/esm/utils/ts-doc/{constants.js → node-kinds.js} +1 -1
  75. package/dist/esm/utils/ts-doc/node-kinds.js.map +1 -0
  76. package/dist/esm/utils/ts-doc/parse-ts-doc-from-file-sync.js +23 -0
  77. package/dist/esm/utils/ts-doc/parse-ts-doc-from-file-sync.js.map +1 -0
  78. package/dist/esm/utils/ts-doc/parse-ts-doc-sync.js +18 -0
  79. package/dist/esm/utils/ts-doc/parse-ts-doc-sync.js.map +1 -0
  80. package/dist/esm/utils/ts-doc/process-source-file.js +22 -0
  81. package/dist/esm/utils/ts-doc/process-source-file.js.map +1 -0
  82. package/dist/esm/utils/zod-to-azure-fields.js +7 -0
  83. package/dist/esm/utils/zod-to-azure-fields.js.map +1 -1
  84. package/dist/esm/utils/zod-to-azure-fields.test.js +8 -7
  85. package/dist/esm/utils/zod-to-azure-fields.test.js.map +1 -1
  86. package/dist/esm/version.js +1 -1
  87. package/dist/tsconfig.tsbuildinfo +1 -1
  88. package/dist/types/bin/apply-schema.d.ts +1 -1
  89. package/dist/types/config.d.ts +1 -1
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/types/utils/git/get-changed-files.d.ts +13 -0
  92. package/dist/types/utils/git/get-file-status.d.ts +13 -0
  93. package/dist/types/utils/git/{git-client.d.ts → get-git.d.ts} +0 -10
  94. package/dist/types/utils/git/index.d.ts +7 -4
  95. package/dist/types/utils/git/is-file-changed.d.ts +12 -0
  96. package/dist/types/utils/git/resolve-project-root.d.ts +10 -0
  97. package/dist/types/utils/markdown/index.d.ts +3 -1
  98. package/dist/types/utils/markdown/is-markdown-file.d.ts +7 -0
  99. package/dist/types/utils/markdown/parse-markdown-file.d.ts +14 -0
  100. package/dist/types/utils/markdown/parse-markdown.d.ts +14 -0
  101. package/dist/types/utils/ts-doc/create-typescript-document.d.ts +12 -0
  102. package/dist/types/utils/ts-doc/extract-document-from-class-node.d.ts +15 -0
  103. package/dist/types/utils/ts-doc/extract-document-from-node.d.ts +20 -0
  104. package/dist/types/utils/ts-doc/index.d.ts +3 -1
  105. package/dist/types/utils/ts-doc/is-typescript-file.d.ts +7 -0
  106. package/dist/types/utils/ts-doc/parse-ts-doc-from-file-sync.d.ts +14 -0
  107. package/dist/types/utils/ts-doc/parse-ts-doc-sync.d.ts +12 -0
  108. package/dist/types/utils/ts-doc/process-source-file.d.ts +11 -0
  109. package/dist/types/version.d.ts +1 -1
  110. package/package.json +8 -8
  111. package/src/bin/apply-metadata.ts +69 -57
  112. package/src/bin/apply-schema.test.ts +2 -1
  113. package/src/bin/apply-schema.ts +6 -1
  114. package/src/bin/{delete-removed-files.ts → create-delete-removed-files-stream.ts} +12 -1
  115. package/src/bin/embed.ts +389 -145
  116. package/src/bin/execute-pipeline.ts +5 -0
  117. package/src/bin/get-diff.ts +7 -1
  118. package/src/bin/types.ts +1 -1
  119. package/src/{embeddings-command.ts → command.ts} +3 -1
  120. package/src/config.ts +1 -1
  121. package/src/{create-command.ts → create-index-command.ts} +15 -5
  122. package/src/delete-command.ts +11 -2
  123. package/src/delete-index-command.ts +4 -0
  124. package/src/embed-command.ts +3 -6
  125. package/src/index.ts +3 -3
  126. package/src/search-command.ts +20 -0
  127. package/src/utils/git/{metadata.ts → extract-git-metadata.ts} +3 -1
  128. package/src/utils/git/get-changed-files.ts +93 -0
  129. package/src/utils/git/{file-changes.ts → get-file-status.ts} +9 -103
  130. package/src/utils/git/{status.ts → get-git-status.ts} +4 -1
  131. package/src/utils/git/{git-client.ts → get-git.ts} +4 -22
  132. package/src/utils/git/index.ts +7 -4
  133. package/src/utils/git/is-file-changed.ts +21 -0
  134. package/src/utils/git/resolve-project-root.ts +22 -0
  135. package/src/utils/markdown/index.ts +3 -1
  136. package/src/utils/markdown/is-markdown-file.ts +9 -0
  137. package/src/utils/markdown/parse-markdown-file.ts +36 -0
  138. package/src/utils/markdown/{parser.ts → parse-markdown.ts} +16 -51
  139. package/src/utils/{embedding-dimensions.ts → resolve-embedding-dimensions.ts} +2 -0
  140. package/src/utils/{package-resolver.ts → resolve-package.ts} +3 -0
  141. package/src/utils/ts-doc/create-typescript-document.ts +36 -0
  142. package/src/utils/ts-doc/extract-document-from-class-node.ts +106 -0
  143. package/src/utils/ts-doc/extract-document-from-node.ts +120 -0
  144. package/src/utils/ts-doc/index.ts +3 -1
  145. package/src/utils/ts-doc/is-typescript-file.ts +9 -0
  146. package/src/utils/ts-doc/parse-ts-doc-from-file-sync.ts +32 -0
  147. package/src/utils/ts-doc/parse-ts-doc-sync.ts +23 -0
  148. package/src/utils/ts-doc/process-source-file.ts +30 -0
  149. package/src/utils/zod-to-azure-fields.test.ts +10 -7
  150. package/src/utils/zod-to-azure-fields.ts +7 -0
  151. package/src/version.ts +1 -1
  152. package/dist/esm/bin/delete-removed-files.js.map +0 -1
  153. package/dist/esm/bin/file-stream.js +0 -22
  154. package/dist/esm/bin/file-stream.js.map +0 -1
  155. package/dist/esm/create-command.js.map +0 -1
  156. package/dist/esm/delete-command.options.js.map +0 -1
  157. package/dist/esm/embeddings-command.js.map +0 -1
  158. package/dist/esm/embeddings-command.options.js.map +0 -1
  159. package/dist/esm/schema.js.map +0 -1
  160. package/dist/esm/utils/embedding-dimensions.js.map +0 -1
  161. package/dist/esm/utils/git/file-changes.js.map +0 -1
  162. package/dist/esm/utils/git/git-client.js +0 -48
  163. package/dist/esm/utils/git/git-client.js.map +0 -1
  164. package/dist/esm/utils/git/metadata.js.map +0 -1
  165. package/dist/esm/utils/git/status.js.map +0 -1
  166. package/dist/esm/utils/markdown/parser.js.map +0 -1
  167. package/dist/esm/utils/package-resolver.js.map +0 -1
  168. package/dist/esm/utils/ts-doc/constants.js.map +0 -1
  169. package/dist/esm/utils/ts-doc/extractors.js +0 -189
  170. package/dist/esm/utils/ts-doc/extractors.js.map +0 -1
  171. package/dist/esm/utils/ts-doc/parser.js +0 -46
  172. package/dist/esm/utils/ts-doc/parser.js.map +0 -1
  173. package/dist/types/bin/file-stream.d.ts +0 -1
  174. package/dist/types/utils/git/file-changes.d.ts +0 -36
  175. package/dist/types/utils/markdown/parser.d.ts +0 -34
  176. package/dist/types/utils/ts-doc/extractors.d.ts +0 -42
  177. package/dist/types/utils/ts-doc/parser.d.ts +0 -32
  178. package/src/bin/file-stream.ts +0 -34
  179. package/src/utils/ts-doc/extractors.ts +0 -260
  180. package/src/utils/ts-doc/parser.ts +0 -60
  181. /package/dist/types/bin/{delete-removed-files.d.ts → create-delete-removed-files-stream.d.ts} +0 -0
  182. /package/dist/types/{embeddings-command.options.d.ts → command-options-schema.d.ts} +0 -0
  183. /package/dist/types/{embeddings-command.d.ts → command.d.ts} +0 -0
  184. /package/dist/types/{create-command.d.ts → create-index-command.d.ts} +0 -0
  185. /package/dist/types/{schema.d.ts → define-index-schema.d.ts} +0 -0
  186. /package/dist/types/{delete-command.options.d.ts → delete-options-schema.d.ts} +0 -0
  187. /package/dist/types/utils/git/{metadata.d.ts → extract-git-metadata.d.ts} +0 -0
  188. /package/dist/types/utils/git/{status.d.ts → get-git-status.d.ts} +0 -0
  189. /package/dist/types/utils/{embedding-dimensions.d.ts → resolve-embedding-dimensions.d.ts} +0 -0
  190. /package/dist/types/utils/{package-resolver.d.ts → resolve-package.d.ts} +0 -0
  191. /package/dist/types/utils/ts-doc/{constants.d.ts → node-kinds.d.ts} +0 -0
  192. /package/src/{embeddings-command.options.ts → command-options-schema.ts} +0 -0
  193. /package/src/{schema.ts → define-index-schema.ts} +0 -0
  194. /package/src/{delete-command.options.ts → delete-options-schema.ts} +0 -0
  195. /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,
@@ -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 '../embeddings-command.options.js';
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 '../embeddings-command.options.js';
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 './embeddings-command.options.js';
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
- const conflictingSchemaFields = schemaFields
71
- .map((field) => field.name)
72
- .filter((name) => reservedFieldNames.includes(name as (typeof reservedFieldNames)[number]));
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})`);
@@ -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-command.options.js';
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
- return sources.map((s) => `metadata/source eq '${s}'`).join(' or ');
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})`);
@@ -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
- console.log(
37
- `Vector (first 8): [${vector
38
- .slice(0, 8)
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 './embeddings-command.js';
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.
@@ -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-client.js';
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
+ };