@hubblecommerce/overmind-core 0.1.6 → 0.1.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.
@@ -7,6 +7,7 @@ interface SearchMatch {
7
7
  lineNumber: number;
8
8
  line: string;
9
9
  matchedText: string;
10
+ filePath: string | null;
10
11
  }
11
12
  interface SearchResult {
12
13
  matches: SearchMatch[];
@@ -16,6 +17,12 @@ interface SearchOptions {
16
17
  ignoreCase?: boolean;
17
18
  contextLines?: number;
18
19
  }
20
+ interface DirectoryStructureOptions {
21
+ /** Subtree root path to start from (e.g. "app/components"). Default: root */
22
+ path?: string;
23
+ /** How many levels deep to return. Default: 1 (top-level only) */
24
+ depth?: number;
25
+ }
19
26
  /**
20
27
  * Search a repomix XML string for lines matching a pattern.
21
28
  * Returns matching lines with optional context.
@@ -27,5 +34,10 @@ export declare function searchRepomix(xml: string, pattern: string, options?: Se
27
34
  * Returns null if the file is not found.
28
35
  */
29
36
  export declare function getRepomixFile(xml: string, filePath: string): string | null;
37
+ /**
38
+ * Extract the directory structure from repomix XML.
39
+ * Returns an indented tree filtered by optional path and depth.
40
+ */
41
+ export declare function getRepomixDirectoryStructure(xml: string, options?: DirectoryStructureOptions): string | null;
30
42
  export {};
31
43
  //# sourceMappingURL=repomix-search.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repomix-search.d.ts","sourceRoot":"","sources":["../../../src/utils/repomix-search.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,UAAU,WAAW;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,YAAY;IAClB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,UAAU,aAAa;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAsED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,YAAY,CASjG;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ3E"}
1
+ {"version":3,"file":"repomix-search.d.ts","sourceRoot":"","sources":["../../../src/utils/repomix-search.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,UAAU,WAAW;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,UAAU,YAAY;IAClB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,UAAU,aAAa;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,yBAAyB;IAC/B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAmGD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,YAAY,CASjG;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ3E;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,MAAM,GAAG,IAAI,CA0D5G"}
@@ -13,8 +13,26 @@ function createRegexPattern(pattern, ignoreCase) {
13
13
  throw new Error(`Invalid regular expression pattern: ${pattern}. ${error instanceof Error ? error.message : String(error)}`);
14
14
  }
15
15
  }
16
+ const FILE_TAG_REGEX = /^<file\s+path="([^"]+)"/;
17
+ /**
18
+ * Build a lookup: for each line index, find the file path it belongs to
19
+ * by scanning for `<file path="...">` tags.
20
+ */
21
+ function buildFilePathIndex(lines) {
22
+ const index = new Array(lines.length).fill('');
23
+ let currentPath = '';
24
+ for (let i = 0; i < lines.length; i++) {
25
+ const tagMatch = lines[i].match(FILE_TAG_REGEX);
26
+ if (tagMatch) {
27
+ currentPath = tagMatch[1];
28
+ }
29
+ index[i] = currentPath;
30
+ }
31
+ return index;
32
+ }
16
33
  function searchInLines(lines, pattern, ignoreCase) {
17
34
  const regex = createRegexPattern(pattern, ignoreCase);
35
+ const filePathIndex = buildFilePathIndex(lines);
18
36
  const matches = [];
19
37
  for (let i = 0; i < lines.length; i++) {
20
38
  const line = lines[i];
@@ -24,6 +42,7 @@ function searchInLines(lines, pattern, ignoreCase) {
24
42
  lineNumber: i + 1,
25
43
  line,
26
44
  matchedText: match[0],
45
+ filePath: filePathIndex[i] || null,
27
46
  });
28
47
  }
29
48
  }
@@ -35,10 +54,18 @@ function formatSearchResults(lines, matches, beforeLines, afterLines) {
35
54
  }
36
55
  const resultLines = [];
37
56
  const addedLines = new Set();
57
+ let lastFilePath = null;
38
58
  for (const match of matches) {
39
59
  const start = Math.max(0, match.lineNumber - 1 - beforeLines);
40
60
  const end = Math.min(lines.length - 1, match.lineNumber - 1 + afterLines);
41
- if (resultLines.length > 0 && start > Math.min(...addedLines) + 1) {
61
+ // Add file path header when entering a different file
62
+ if (match.filePath && match.filePath !== lastFilePath) {
63
+ if (resultLines.length > 0)
64
+ resultLines.push('');
65
+ resultLines.push(`=== ${match.filePath} ===`);
66
+ lastFilePath = match.filePath;
67
+ }
68
+ else if (resultLines.length > 0 && start > Math.min(...addedLines) + 1) {
42
69
  resultLines.push('--');
43
70
  }
44
71
  for (let i = start; i <= end; i++) {
@@ -78,3 +105,57 @@ export function getRepomixFile(xml, filePath) {
78
105
  return null;
79
106
  return match[1];
80
107
  }
108
+ /**
109
+ * Extract the directory structure from repomix XML.
110
+ * Returns an indented tree filtered by optional path and depth.
111
+ */
112
+ export function getRepomixDirectoryStructure(xml, options) {
113
+ const match = xml.match(/<directory_structure>\n?([\s\S]*?)\n?<\/directory_structure>/);
114
+ if (!match?.[1])
115
+ return null;
116
+ const fullTree = match[1];
117
+ const lines = fullTree.split('\n');
118
+ const targetPath = options?.path?.replace(/\/$/, '') ?? '';
119
+ const maxDepth = options?.depth ?? 1;
120
+ const getDepth = (line) => {
121
+ const leadingSpaces = line.match(/^( *)/)?.[1].length ?? 0;
122
+ return leadingSpaces / 2;
123
+ };
124
+ // Build full paths from indented tree using a stack
125
+ const pathStack = [];
126
+ if (!targetPath) {
127
+ return lines
128
+ .filter(line => line.trim() && getDepth(line) < maxDepth)
129
+ .join('\n');
130
+ }
131
+ // Find the subtree starting at targetPath
132
+ let inSubtree = false;
133
+ let subtreeBaseDepth = 0;
134
+ const result = [];
135
+ for (const line of lines) {
136
+ const trimmed = line.trim();
137
+ if (!trimmed)
138
+ continue;
139
+ const depth = getDepth(line);
140
+ // Maintain path stack: trim to current depth, then push
141
+ pathStack.length = depth;
142
+ pathStack.push(trimmed.replace(/\/$/, ''));
143
+ const fullPath = pathStack.join('/');
144
+ if (!inSubtree) {
145
+ if (fullPath === targetPath) {
146
+ inSubtree = true;
147
+ subtreeBaseDepth = depth + 1;
148
+ result.push(trimmed);
149
+ }
150
+ continue;
151
+ }
152
+ if (depth < subtreeBaseDepth) {
153
+ break;
154
+ }
155
+ const relativeDepth = depth - subtreeBaseDepth;
156
+ if (relativeDepth < maxDepth) {
157
+ result.push(' '.repeat(relativeDepth) + trimmed);
158
+ }
159
+ }
160
+ return result.length > 0 ? result.join('\n') : null;
161
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { searchRepomix, getRepomixFile } from './repomix-search.js';
2
+ import { searchRepomix, getRepomixFile, getRepomixDirectoryStructure } from './repomix-search.js';
3
3
  const SAMPLE_XML = `<file path="src/index.ts">
4
4
  import { App } from './app';
5
5
 
@@ -25,12 +25,46 @@ export function login(username: string, password: string) {
25
25
  });
26
26
  }
27
27
  </file>`;
28
+ const SAMPLE_XML_WITH_STRUCTURE = `<directory_structure>
29
+ app/
30
+ components/
31
+ Header.vue
32
+ Footer.vue
33
+ pages/
34
+ index.vue
35
+ about.vue
36
+ middleware/
37
+ auth.ts
38
+ config/
39
+ app.config.ts
40
+ package.json
41
+ README.md
42
+ </directory_structure>
43
+
44
+ <files>
45
+ <file path="app/components/Header.vue">
46
+ <template><header></header></template>
47
+ </file>
48
+ </files>`;
28
49
  describe('searchRepomix', () => {
29
50
  it('returns matching lines with file context', () => {
30
51
  const results = searchRepomix(SAMPLE_XML, 'auth');
31
52
  expect(results.matches.length).toBeGreaterThan(0);
32
53
  expect(results.matches.some(m => m.line.includes('auth'))).toBe(true);
33
54
  });
55
+ it('enriches each match with its file path', () => {
56
+ const results = searchRepomix(SAMPLE_XML, 'auth');
57
+ const readmeMatch = results.matches.find(m => m.line.includes('authentication'));
58
+ expect(readmeMatch?.filePath).toBe('README.md');
59
+ const loginMatch = results.matches.find(m => m.line.includes('/api/auth/login'));
60
+ expect(loginMatch?.filePath).toBe('src/auth/login.ts');
61
+ });
62
+ it('includes file path headers in formatted output when file changes', () => {
63
+ const results = searchRepomix(SAMPLE_XML, 'auth');
64
+ const output = results.formattedOutput.join('\n');
65
+ expect(output).toContain('=== README.md ===');
66
+ expect(output).toContain('=== src/auth/login.ts ===');
67
+ });
34
68
  it('returns empty matches for non-existent pattern', () => {
35
69
  const results = searchRepomix(SAMPLE_XML, 'nonexistent_xyz_pattern');
36
70
  expect(results.matches).toHaveLength(0);
@@ -73,3 +107,36 @@ describe('getRepomixFile', () => {
73
107
  expect(content).toContain("import { App }");
74
108
  });
75
109
  });
110
+ describe('getRepomixDirectoryStructure', () => {
111
+ it('returns top-level entries by default (depth 1)', () => {
112
+ const result = getRepomixDirectoryStructure(SAMPLE_XML_WITH_STRUCTURE);
113
+ expect(result).toContain('app/');
114
+ expect(result).toContain('config/');
115
+ expect(result).toContain('package.json');
116
+ expect(result).not.toContain('Header.vue');
117
+ expect(result).not.toContain('pages/');
118
+ });
119
+ it('returns deeper entries when depth is increased', () => {
120
+ const result = getRepomixDirectoryStructure(SAMPLE_XML_WITH_STRUCTURE, { depth: 2 });
121
+ expect(result).toContain('app/');
122
+ expect(result).toContain('components/');
123
+ expect(result).toContain('pages/');
124
+ expect(result).not.toContain('Header.vue');
125
+ });
126
+ it('returns a subtree when path is specified', () => {
127
+ const result = getRepomixDirectoryStructure(SAMPLE_XML_WITH_STRUCTURE, { path: 'app/components', depth: 1 });
128
+ expect(result).toContain('components/');
129
+ expect(result).toContain('Header.vue');
130
+ expect(result).toContain('Footer.vue');
131
+ expect(result).not.toContain('pages/');
132
+ expect(result).not.toContain('config/');
133
+ });
134
+ it('returns null when directory_structure section is missing', () => {
135
+ const result = getRepomixDirectoryStructure('<file path="test.ts">code</file>');
136
+ expect(result).toBeNull();
137
+ });
138
+ it('returns null when path does not exist in tree', () => {
139
+ const result = getRepomixDirectoryStructure(SAMPLE_XML_WITH_STRUCTURE, { path: 'nonexistent' });
140
+ expect(result).toBeNull();
141
+ });
142
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubblecommerce/overmind-core",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Shared infrastructure package for the Overmind AI agent system",
6
6
  "main": "./dist/src/index.js",