@hubblecommerce/overmind-core 0.1.4 → 0.1.6

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 (29) hide show
  1. package/dist/src/integrations/confluence/confluence.client.test.d.ts +2 -0
  2. package/dist/src/integrations/confluence/confluence.client.test.d.ts.map +1 -0
  3. package/dist/src/integrations/confluence/confluence.client.test.js +320 -0
  4. package/dist/src/integrations/gitlab/gitlab.client.d.ts +8 -0
  5. package/dist/src/integrations/gitlab/gitlab.client.d.ts.map +1 -1
  6. package/dist/src/integrations/gitlab/gitlab.client.js +20 -0
  7. package/dist/src/integrations/gitlab/gitlab.client.test.d.ts +2 -0
  8. package/dist/src/integrations/gitlab/gitlab.client.test.d.ts.map +1 -0
  9. package/dist/src/integrations/gitlab/gitlab.client.test.js +62 -0
  10. package/dist/src/integrations/jira/jira.client.test.d.ts +2 -0
  11. package/dist/src/integrations/jira/jira.client.test.d.ts.map +1 -0
  12. package/dist/src/integrations/jira/jira.client.test.js +171 -0
  13. package/dist/src/llm/anthropic-retry-wrapper.test.d.ts +2 -0
  14. package/dist/src/llm/anthropic-retry-wrapper.test.d.ts.map +1 -0
  15. package/dist/src/llm/anthropic-retry-wrapper.test.js +125 -0
  16. package/dist/src/processors/confluence-document.processor.test.d.ts +2 -0
  17. package/dist/src/processors/confluence-document.processor.test.d.ts.map +1 -0
  18. package/dist/src/processors/confluence-document.processor.test.js +202 -0
  19. package/dist/src/processors/confluence-html-parser.test.d.ts +2 -0
  20. package/dist/src/processors/confluence-html-parser.test.d.ts.map +1 -0
  21. package/dist/src/processors/confluence-html-parser.test.js +214 -0
  22. package/dist/src/tools/confluence-search.tool.d.ts +6 -6
  23. package/dist/src/utils/repomix-search.d.ts +31 -0
  24. package/dist/src/utils/repomix-search.d.ts.map +1 -0
  25. package/dist/src/utils/repomix-search.js +80 -0
  26. package/dist/src/utils/repomix-search.test.d.ts +2 -0
  27. package/dist/src/utils/repomix-search.test.d.ts.map +1 -0
  28. package/dist/src/utils/repomix-search.test.js +75 -0
  29. package/package.json +11 -2
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { invokeWithRetry, RateLimitError } from './anthropic-retry-wrapper.js';
3
+ function make429Error(overrides = {}) {
4
+ const err = new Error('rate limit exceeded');
5
+ err.status = 429;
6
+ return Object.assign(err, overrides);
7
+ }
8
+ describe('invokeWithRetry', () => {
9
+ beforeEach(() => { vi.useFakeTimers(); });
10
+ afterEach(() => { vi.useRealTimers(); });
11
+ it('returns result on first attempt', async () => {
12
+ const agent = { invoke: vi.fn().mockResolvedValue('result') };
13
+ expect(await invokeWithRetry(agent, 'input')).toBe('result');
14
+ expect(agent.invoke).toHaveBeenCalledOnce();
15
+ });
16
+ it('retries after 429 and returns result on second attempt', async () => {
17
+ const agent = { invoke: vi.fn() };
18
+ agent.invoke
19
+ .mockRejectedValueOnce(make429Error())
20
+ .mockResolvedValueOnce('result');
21
+ const promise = invokeWithRetry(agent, 'input');
22
+ await vi.runAllTimersAsync();
23
+ expect(await promise).toBe('result');
24
+ expect(agent.invoke).toHaveBeenCalledTimes(2);
25
+ });
26
+ it('throws RateLimitError after exhausting all retries', async () => {
27
+ const agent = { invoke: vi.fn().mockRejectedValue(make429Error()) };
28
+ const promise = invokeWithRetry(agent, 'input', 3);
29
+ // Attach rejection handler before running timers to avoid unhandled rejection warnings
30
+ const assertion = expect(promise).rejects.toBeInstanceOf(RateLimitError);
31
+ await vi.runAllTimersAsync();
32
+ await assertion;
33
+ expect(agent.invoke).toHaveBeenCalledTimes(3);
34
+ });
35
+ it('throws immediately on non-429 error without retrying', async () => {
36
+ const agent = { invoke: vi.fn().mockRejectedValue(new Error('Internal server error')) };
37
+ await expect(invokeWithRetry(agent, 'input')).rejects.toThrow('Internal server error');
38
+ expect(agent.invoke).toHaveBeenCalledOnce();
39
+ });
40
+ it('detects 429 via message containing "rate limit"', async () => {
41
+ const err = new Error('Claude rate limit reached');
42
+ const agent = { invoke: vi.fn() };
43
+ agent.invoke.mockRejectedValueOnce(err).mockResolvedValueOnce('ok');
44
+ const promise = invokeWithRetry(agent, 'input');
45
+ await vi.runAllTimersAsync();
46
+ expect(await promise).toBe('ok');
47
+ });
48
+ it('detects 429 via message containing "too many requests"', async () => {
49
+ const err = new Error('Too Many Requests from this endpoint');
50
+ const agent = { invoke: vi.fn() };
51
+ agent.invoke.mockRejectedValueOnce(err).mockResolvedValueOnce('ok');
52
+ const promise = invokeWithRetry(agent, 'input');
53
+ await vi.runAllTimersAsync();
54
+ expect(await promise).toBe('ok');
55
+ });
56
+ it('uses custom maxRetries count', async () => {
57
+ const agent = { invoke: vi.fn().mockRejectedValue(make429Error()) };
58
+ const promise = invokeWithRetry(agent, 'input', 2);
59
+ const assertion = expect(promise).rejects.toBeInstanceOf(RateLimitError);
60
+ await vi.runAllTimersAsync();
61
+ await assertion;
62
+ expect(agent.invoke).toHaveBeenCalledTimes(2);
63
+ });
64
+ });
65
+ describe('RateLimitError', () => {
66
+ it('is instanceof Error', () => {
67
+ expect(new RateLimitError('msg', 60)).toBeInstanceOf(Error);
68
+ });
69
+ it('has name RateLimitError', () => {
70
+ expect(new RateLimitError('msg', 60).name).toBe('RateLimitError');
71
+ });
72
+ it('stores retryAfterSeconds', () => {
73
+ expect(new RateLimitError('msg', 30).retryAfterSeconds).toBe(30);
74
+ });
75
+ it('stores null retryAfterSeconds', () => {
76
+ expect(new RateLimitError('msg', null).retryAfterSeconds).toBeNull();
77
+ });
78
+ });
79
+ describe('extractRetryAfter (tested via RateLimitError on exhaustion)', () => {
80
+ beforeEach(() => { vi.useFakeTimers(); });
81
+ afterEach(() => { vi.useRealTimers(); });
82
+ async function exhaustAndGetError(err) {
83
+ const agent = { invoke: vi.fn().mockRejectedValue(err) };
84
+ const promise = invokeWithRetry(agent, 'input', 3);
85
+ // Attach handler before running timers to prevent unhandled rejection warnings
86
+ let caughtError;
87
+ promise.catch((e) => { caughtError = e; });
88
+ await vi.runAllTimersAsync();
89
+ return caughtError;
90
+ }
91
+ it('reads retry-after from error.headers', async () => {
92
+ const err = make429Error({ headers: { 'retry-after': '45' } });
93
+ const result = await exhaustAndGetError(err);
94
+ expect(result.retryAfterSeconds).toBe(45);
95
+ });
96
+ it('reads retry-after from error.response.headers as plain object', async () => {
97
+ const err = make429Error({ response: { headers: { 'retry-after': '30' } } });
98
+ const result = await exhaustAndGetError(err);
99
+ expect(result.retryAfterSeconds).toBe(30);
100
+ });
101
+ it('reads retry-after from error.response.headers.get() (fetch Headers interface)', async () => {
102
+ const err = make429Error({
103
+ response: {
104
+ headers: { get: (key) => key === 'retry-after' ? '20' : undefined },
105
+ },
106
+ });
107
+ const result = await exhaustAndGetError(err);
108
+ expect(result.retryAfterSeconds).toBe(20);
109
+ });
110
+ it('reads retry-after from error.error.headers (nested shape)', async () => {
111
+ const err = make429Error({ error: { headers: { 'retry-after': '15' } } });
112
+ const result = await exhaustAndGetError(err);
113
+ expect(result.retryAfterSeconds).toBe(15);
114
+ });
115
+ it('returns null retryAfterSeconds when no header present', async () => {
116
+ const err = make429Error();
117
+ const result = await exhaustAndGetError(err);
118
+ expect(result.retryAfterSeconds).toBeNull();
119
+ });
120
+ it('returns null retryAfterSeconds when retry-after is not a valid integer', async () => {
121
+ const err = make429Error({ headers: { 'retry-after': 'not-a-number' } });
122
+ const result = await exhaustAndGetError(err);
123
+ expect(result.retryAfterSeconds).toBeNull();
124
+ });
125
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=confluence-document.processor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confluence-document.processor.test.d.ts","sourceRoot":"","sources":["../../../src/processors/confluence-document.processor.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,202 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { splitMarkdownBySections, processConfluencePage } from './confluence-document.processor.js';
3
+ function makePage(overrides) {
4
+ const { html, ...rest } = overrides;
5
+ return {
6
+ id: 'page1',
7
+ status: 'current',
8
+ title: 'Test Page',
9
+ spaceId: 'SPACE1',
10
+ authorId: 'author1',
11
+ createdAt: '2024-01-01T00:00:00.000Z',
12
+ version: {
13
+ createdAt: '2024-01-02T00:00:00.000Z',
14
+ message: '',
15
+ number: 1,
16
+ minorEdit: false,
17
+ authorId: 'author1',
18
+ },
19
+ body: {
20
+ storage: {
21
+ value: html ?? '<p>Default content</p>',
22
+ representation: 'storage',
23
+ },
24
+ },
25
+ _links: { webui: '/spaces/TEST/pages/page1/Test+Page' },
26
+ ...rest,
27
+ };
28
+ }
29
+ describe('splitMarkdownBySections', () => {
30
+ it('returns a single chunk with undefined sectionTitle when no H2 headings', () => {
31
+ const chunks = splitMarkdownBySections('Some content without headings');
32
+ expect(chunks).toHaveLength(1);
33
+ expect(chunks[0].sectionTitle).toBeUndefined();
34
+ expect(chunks[0].sectionIndex).toBe(0);
35
+ expect(chunks[0].content).toBe('Some content without headings');
36
+ });
37
+ it('returns N chunks for N H2 headings', () => {
38
+ const md = '## First\nContent A\n## Second\nContent B\n## Third\nContent C';
39
+ expect(splitMarkdownBySections(md)).toHaveLength(3);
40
+ });
41
+ it('chunk sectionTitle matches the H2 heading text', () => {
42
+ const md = '## My Heading\nSome text';
43
+ const [chunk] = splitMarkdownBySections(md);
44
+ expect(chunk.sectionTitle).toBe('My Heading');
45
+ });
46
+ it('sectionIndex is 0-based and sequential', () => {
47
+ const md = '## A\ntext\n## B\ntext\n## C\ntext';
48
+ const chunks = splitMarkdownBySections(md);
49
+ expect(chunks.map(c => c.sectionIndex)).toEqual([0, 1, 2]);
50
+ });
51
+ it('each chunk content starts with the ## heading line', () => {
52
+ const md = '## Section One\nContent here';
53
+ const [chunk] = splitMarkdownBySections(md);
54
+ expect(chunk.content).toMatch(/^## Section One/);
55
+ });
56
+ it('H3 and H4 headings do not split sections', () => {
57
+ const md = '## Section\n### Sub A\n#### Sub B\nContent';
58
+ expect(splitMarkdownBySections(md)).toHaveLength(1);
59
+ });
60
+ it('trims whitespace from chunk content', () => {
61
+ const md = '## Title\n\n Content \n\n';
62
+ const [chunk] = splitMarkdownBySections(md);
63
+ expect(chunk.content).toBe(chunk.content.trim());
64
+ });
65
+ it('returns empty string content as a single chunk', () => {
66
+ const chunks = splitMarkdownBySections('');
67
+ expect(chunks).toHaveLength(1);
68
+ expect(chunks[0].content).toBe('');
69
+ });
70
+ });
71
+ describe('processConfluencePage', () => {
72
+ const BASE_URL = 'https://example.atlassian.net';
73
+ const SPACE_KEY = 'TEST';
74
+ it('returns empty array when page has no body content', () => {
75
+ const page = makePage({ body: undefined });
76
+ expect(processConfluencePage(page, SPACE_KEY, BASE_URL)).toEqual([]);
77
+ });
78
+ it('returns empty array when page body value is empty string', () => {
79
+ const page = makePage({ html: '' });
80
+ expect(processConfluencePage(page, SPACE_KEY, BASE_URL)).toHaveLength(0);
81
+ });
82
+ it('document_id uses format confluence:{spaceKey}:{pageId}_section_{index}', () => {
83
+ const page = makePage({ id: 'abc123', html: '<p>Hello</p>' });
84
+ const [doc] = processConfluencePage(page, 'PROJ', BASE_URL);
85
+ expect(doc.metadata.document_id).toBe('confluence:PROJ:abc123_section_0');
86
+ });
87
+ it('strips trailing slash from baseUrl when constructing page_url', () => {
88
+ const page = makePage({ html: '<p>Hello</p>' });
89
+ const [doc] = processConfluencePage(page, SPACE_KEY, 'https://example.atlassian.net/');
90
+ // Trailing slash stripped: result should be /wiki/spaces/... not //wiki/spaces/...
91
+ expect(doc.metadata.page_url).not.toContain('//wiki');
92
+ expect(doc.metadata.page_url).toContain('https://example.atlassian.net/wiki');
93
+ });
94
+ it('page_url includes the page webui path', () => {
95
+ const page = makePage({ html: '<p>Hello</p>' });
96
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL);
97
+ expect(doc.metadata.page_url).toContain(page._links.webui);
98
+ });
99
+ it('chunk_type is full_page when there are no H2 headings', () => {
100
+ const page = makePage({ html: '<p>Simple content with no headings</p>' });
101
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL);
102
+ expect(doc.metadata.chunk_type).toBe('full_page');
103
+ });
104
+ it('chunk_type is section when H2 headings are present', () => {
105
+ const page = makePage({ html: '<h2>Section</h2><p>Content</p>' });
106
+ const docs = processConfluencePage(page, SPACE_KEY, BASE_URL);
107
+ expect(docs[0].metadata.chunk_type).toBe('section');
108
+ });
109
+ it('page with two H2s produces two documents', () => {
110
+ const page = makePage({ html: '<h2>Part A</h2><p>First</p><h2>Part B</h2><p>Second</p>' });
111
+ expect(processConfluencePage(page, SPACE_KEY, BASE_URL)).toHaveLength(2);
112
+ });
113
+ it('metadata.source is always "confluence"', () => {
114
+ const page = makePage({ html: '<p>Hello</p>' });
115
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL);
116
+ expect(doc.metadata.source).toBe('confluence');
117
+ });
118
+ it('metadata.document_type is always "confluence_page"', () => {
119
+ const page = makePage({ html: '<p>Hello</p>' });
120
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL);
121
+ expect(doc.metadata.document_type).toBe('confluence_page');
122
+ });
123
+ it('passes labels through to metadata', () => {
124
+ const page = makePage({ html: '<p>Hello</p>' });
125
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, ['faq', 'active']);
126
+ expect(doc.metadata.labels).toEqual(['faq', 'active']);
127
+ });
128
+ describe('label metadata extraction', () => {
129
+ it('maps "project-info" label to content_type', () => {
130
+ const page = makePage({ html: '<p>Hello</p>' });
131
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, ['project-info']);
132
+ expect(doc.metadata.content_type).toBe('project-info');
133
+ });
134
+ it('maps "faq" label to content_type', () => {
135
+ const page = makePage({ html: '<p>Hello</p>' });
136
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, ['faq']);
137
+ expect(doc.metadata.content_type).toBe('faq');
138
+ });
139
+ it('maps "customer-acme" label to customer field', () => {
140
+ const page = makePage({ html: '<p>Hello</p>' });
141
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, ['customer-acme']);
142
+ expect(doc.metadata.customer).toBe('customer-acme');
143
+ });
144
+ it('maps "active" label to category', () => {
145
+ const page = makePage({ html: '<p>Hello</p>' });
146
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, ['active']);
147
+ expect(doc.metadata.category).toBe('active');
148
+ });
149
+ it('maps "archived" label to category', () => {
150
+ const page = makePage({ html: '<p>Hello</p>' });
151
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, ['archived']);
152
+ expect(doc.metadata.category).toBe('archived');
153
+ });
154
+ it('remaining labels go to keywords', () => {
155
+ const page = makePage({ html: '<p>Hello</p>' });
156
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, ['shopware', 'v6', 'backend']);
157
+ expect(doc.metadata.keywords).toEqual(['shopware', 'v6', 'backend']);
158
+ });
159
+ it('categorized labels are NOT duplicated in keywords', () => {
160
+ const page = makePage({ html: '<p>Hello</p>' });
161
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, ['faq', 'active', 'customer-xyz', 'shopware']);
162
+ expect(doc.metadata.keywords).toEqual(['shopware']);
163
+ expect(doc.metadata.keywords).not.toContain('faq');
164
+ expect(doc.metadata.keywords).not.toContain('active');
165
+ expect(doc.metadata.keywords).not.toContain('customer-xyz');
166
+ });
167
+ it('empty labels result in undefined content_type, customer, category and empty keywords', () => {
168
+ const page = makePage({ html: '<p>Hello</p>' });
169
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL, []);
170
+ expect(doc.metadata.content_type).toBeUndefined();
171
+ expect(doc.metadata.customer).toBeUndefined();
172
+ expect(doc.metadata.category).toBeUndefined();
173
+ expect(doc.metadata.keywords).toEqual([]);
174
+ });
175
+ });
176
+ describe('content analysis', () => {
177
+ it('has_code_blocks is true when content contains triple backtick', () => {
178
+ // Use a Confluence code macro so the HTML parser produces backticks
179
+ const html = `<ac:structured-macro ac:name="code">
180
+ <ac:plain-text-body>const x = 1;</ac:plain-text-body>
181
+ </ac:structured-macro>`;
182
+ const page = makePage({ html });
183
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL);
184
+ expect(doc.metadata.has_code_blocks).toBe(true);
185
+ });
186
+ it('has_code_blocks is false when content has no code blocks', () => {
187
+ const page = makePage({ html: '<p>Plain text only</p>' });
188
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL);
189
+ expect(doc.metadata.has_code_blocks).toBe(false);
190
+ });
191
+ it('has_links is true when content contains a markdown link', () => {
192
+ const page = makePage({ html: '<a href="https://example.com">click</a>' });
193
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL);
194
+ expect(doc.metadata.has_links).toBe(true);
195
+ });
196
+ it('word_count reflects number of words in content', () => {
197
+ const page = makePage({ html: '<p>one two three four five</p>' });
198
+ const [doc] = processConfluencePage(page, SPACE_KEY, BASE_URL);
199
+ expect(doc.metadata.word_count).toBe(5);
200
+ });
201
+ });
202
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=confluence-html-parser.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confluence-html-parser.test.d.ts","sourceRoot":"","sources":["../../../src/processors/confluence-html-parser.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,214 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseConfluenceHtml } from './confluence-html-parser.js';
3
+ describe('parseConfluenceHtml', () => {
4
+ describe('basic HTML conversion', () => {
5
+ it('converts h2 to ## heading', () => {
6
+ expect(parseConfluenceHtml('<h2>My Section</h2>')).toBe('## My Section');
7
+ });
8
+ it('converts h1 to # heading', () => {
9
+ expect(parseConfluenceHtml('<h1>Title</h1>')).toBe('# Title');
10
+ });
11
+ it('converts strong to **bold**', () => {
12
+ expect(parseConfluenceHtml('<p><strong>bold text</strong></p>')).toContain('**bold text**');
13
+ });
14
+ it('converts inline code to backtick', () => {
15
+ expect(parseConfluenceHtml('<p><code>myFunc()</code></p>')).toContain('`myFunc()`');
16
+ });
17
+ it('converts anchor tag to markdown link', () => {
18
+ const result = parseConfluenceHtml('<a href="https://example.com">click here</a>');
19
+ expect(result).toContain('[click here](https://example.com)');
20
+ });
21
+ });
22
+ describe('whitespace cleanup', () => {
23
+ it('collapses 3+ consecutive newlines to 2', () => {
24
+ const html = '<p>First</p>\n\n\n\n<p>Second</p>';
25
+ const result = parseConfluenceHtml(html);
26
+ expect(result).not.toMatch(/\n{3,}/);
27
+ });
28
+ it('removes trailing spaces from lines', () => {
29
+ const result = parseConfluenceHtml('<p>Hello world</p>');
30
+ const lines = result.split('\n');
31
+ for (const line of lines) {
32
+ expect(line).toBe(line.trimEnd());
33
+ }
34
+ });
35
+ it('trims leading and trailing whitespace', () => {
36
+ const result = parseConfluenceHtml('<p>Hello</p>');
37
+ expect(result).toBe(result.trim());
38
+ });
39
+ });
40
+ describe('Confluence code macro', () => {
41
+ it('renders code macro as fenced code block', () => {
42
+ const html = `<ac:structured-macro ac:name="code">
43
+ <ac:plain-text-body>const x = 1;</ac:plain-text-body>
44
+ </ac:structured-macro>`;
45
+ const result = parseConfluenceHtml(html);
46
+ expect(result).toContain('```');
47
+ expect(result).toContain('const x = 1;');
48
+ });
49
+ it('includes language when parameter is present', () => {
50
+ const html = `<ac:structured-macro ac:name="code">
51
+ <ac:parameter ac:name="language">javascript</ac:parameter>
52
+ <ac:plain-text-body>const x = 1;</ac:plain-text-body>
53
+ </ac:structured-macro>`;
54
+ const result = parseConfluenceHtml(html);
55
+ expect(result).toContain('```javascript');
56
+ });
57
+ });
58
+ describe('Confluence panel macros', () => {
59
+ it('renders info macro as blockquote with INFO label', () => {
60
+ const html = `<ac:structured-macro ac:name="info">
61
+ <ac:rich-text-body><p>Important info</p></ac:rich-text-body>
62
+ </ac:structured-macro>`;
63
+ const result = parseConfluenceHtml(html);
64
+ expect(result).toContain('> **INFO**');
65
+ });
66
+ it('renders warning macro as blockquote with WARNING label', () => {
67
+ const html = `<ac:structured-macro ac:name="warning">
68
+ <ac:rich-text-body><p>Be careful</p></ac:rich-text-body>
69
+ </ac:structured-macro>`;
70
+ const result = parseConfluenceHtml(html);
71
+ expect(result).toContain('> **WARNING**');
72
+ });
73
+ it('renders tip macro as blockquote with TIP label', () => {
74
+ const html = `<ac:structured-macro ac:name="tip">
75
+ <ac:rich-text-body><p>A helpful tip</p></ac:rich-text-body>
76
+ </ac:structured-macro>`;
77
+ const result = parseConfluenceHtml(html);
78
+ expect(result).toContain('> **TIP**');
79
+ });
80
+ it('renders note macro as blockquote with NOTE label', () => {
81
+ const html = `<ac:structured-macro ac:name="note">
82
+ <ac:rich-text-body><p>Take note</p></ac:rich-text-body>
83
+ </ac:structured-macro>`;
84
+ const result = parseConfluenceHtml(html);
85
+ expect(result).toContain('> **NOTE**');
86
+ });
87
+ });
88
+ describe('Confluence TOC macro', () => {
89
+ it('renders toc macro as Table of Contents text', () => {
90
+ // Real Confluence TOC macros include parameters — an empty element is
91
+ // not matched by the filter in some DOM parsers
92
+ const html = `<ac:structured-macro ac:name="toc">
93
+ <ac:parameter ac:name="maxLevel">3</ac:parameter>
94
+ </ac:structured-macro>`;
95
+ const result = parseConfluenceHtml(html);
96
+ expect(result).toContain('**Table of Contents**');
97
+ });
98
+ });
99
+ describe('Confluence JIRA macro', () => {
100
+ it('renders jira macro with key as JIRA Issue reference', () => {
101
+ const html = `<ac:structured-macro ac:name="jira">
102
+ <ac:parameter ac:name="key">PROJ-123</ac:parameter>
103
+ </ac:structured-macro>`;
104
+ const result = parseConfluenceHtml(html);
105
+ expect(result).toContain('**JIRA Issue:** PROJ-123');
106
+ });
107
+ it('renders empty string when jira macro has no key', () => {
108
+ const html = `<ac:structured-macro ac:name="jira"></ac:structured-macro>`;
109
+ const result = parseConfluenceHtml(html);
110
+ expect(result).not.toContain('JIRA Issue');
111
+ });
112
+ });
113
+ describe('Confluence status macro', () => {
114
+ it('renders status macro as **[title]**', () => {
115
+ const html = `<ac:structured-macro ac:name="status">
116
+ <ac:parameter ac:name="title">IN PROGRESS</ac:parameter>
117
+ </ac:structured-macro>`;
118
+ const result = parseConfluenceHtml(html);
119
+ expect(result).toContain('**[IN PROGRESS]**');
120
+ });
121
+ it('appends color in parentheses when colour parameter is present', () => {
122
+ const html = `<ac:structured-macro ac:name="status">
123
+ <ac:parameter ac:name="title">DONE</ac:parameter>
124
+ <ac:parameter ac:name="colour">Green</ac:parameter>
125
+ </ac:structured-macro>`;
126
+ const result = parseConfluenceHtml(html);
127
+ expect(result).toContain('(Green)');
128
+ });
129
+ });
130
+ describe('Confluence anchor macro', () => {
131
+ it('renders anchor macro as named anchor tag', () => {
132
+ const html = `<ac:structured-macro ac:name="anchor">
133
+ <ac:parameter>my-anchor</ac:parameter>
134
+ </ac:structured-macro>`;
135
+ const result = parseConfluenceHtml(html);
136
+ expect(result).toContain('<a name="my-anchor"></a>');
137
+ });
138
+ it('renders empty string when anchor has no parameter text', () => {
139
+ const html = `<ac:structured-macro ac:name="anchor">
140
+ <ac:parameter></ac:parameter>
141
+ </ac:structured-macro>`;
142
+ const result = parseConfluenceHtml(html);
143
+ expect(result).not.toContain('<a name=');
144
+ });
145
+ });
146
+ describe('Confluence excerpt macro', () => {
147
+ it('returns rich-text-body content for excerpt macro', () => {
148
+ const html = `<ac:structured-macro ac:name="excerpt">
149
+ <ac:rich-text-body><p>Excerpt content</p></ac:rich-text-body>
150
+ </ac:structured-macro>`;
151
+ const result = parseConfluenceHtml(html);
152
+ expect(result).toContain('Excerpt content');
153
+ });
154
+ });
155
+ describe('Confluence expand macro', () => {
156
+ it('renders expand macro with title and body', () => {
157
+ const html = `<ac:structured-macro ac:name="expand">
158
+ <ac:parameter ac:name="title">Click to expand</ac:parameter>
159
+ <ac:rich-text-body><p>Hidden content</p></ac:rich-text-body>
160
+ </ac:structured-macro>`;
161
+ const result = parseConfluenceHtml(html);
162
+ expect(result).toContain('**Click to expand**');
163
+ expect(result).toContain('Hidden content');
164
+ });
165
+ it('uses "Details" as default title when no title parameter', () => {
166
+ const html = `<ac:structured-macro ac:name="expand">
167
+ <ac:rich-text-body><p>Some content</p></ac:rich-text-body>
168
+ </ac:structured-macro>`;
169
+ const result = parseConfluenceHtml(html);
170
+ expect(result).toContain('**Details**');
171
+ });
172
+ });
173
+ describe('Confluence quote macro', () => {
174
+ it('renders quote macro as blockquote', () => {
175
+ const html = `<ac:structured-macro ac:name="quote">
176
+ <ac:rich-text-body><p>A famous quote</p></ac:rich-text-body>
177
+ </ac:structured-macro>`;
178
+ const result = parseConfluenceHtml(html);
179
+ expect(result).toContain('> ');
180
+ expect(result).toContain('A famous quote');
181
+ });
182
+ });
183
+ describe('Confluence link macro', () => {
184
+ it('renders ac:link with ri:page as markdown link using content-title', () => {
185
+ const html = `<ac:link><ri:page ri:content-title="My Page"/><ac:plain-text-link-body>My Page</ac:plain-text-link-body></ac:link>`;
186
+ const result = parseConfluenceHtml(html);
187
+ expect(result).toContain('[My Page](My Page)');
188
+ });
189
+ it('renders ac:link without href as plain text', () => {
190
+ const html = `<ac:link><ac:plain-text-link-body>Just text</ac:plain-text-link-body></ac:link>`;
191
+ const result = parseConfluenceHtml(html);
192
+ expect(result).toContain('Just text');
193
+ });
194
+ });
195
+ describe('Confluence unknown macro', () => {
196
+ it('falls back to text content for unknown macros', () => {
197
+ const html = `<ac:structured-macro ac:name="unknown-macro">
198
+ <ac:rich-text-body><p>Fallback text</p></ac:rich-text-body>
199
+ </ac:structured-macro>`;
200
+ const result = parseConfluenceHtml(html);
201
+ expect(result).toContain('Fallback text');
202
+ });
203
+ });
204
+ describe('Confluence JIRA macro with server', () => {
205
+ it('includes server name when present', () => {
206
+ const html = `<ac:structured-macro ac:name="jira">
207
+ <ac:parameter ac:name="key">PROJ-456</ac:parameter>
208
+ <ac:parameter ac:name="server">My Jira</ac:parameter>
209
+ </ac:structured-macro>`;
210
+ const result = parseConfluenceHtml(html);
211
+ expect(result).toContain('(My Jira)');
212
+ });
213
+ });
214
+ });
@@ -22,7 +22,7 @@ export declare function createConfluenceAgentTool(vectorStore: VectorStoreProvid
22
22
  parent_page_id: z.ZodOptional<z.ZodString>;
23
23
  }, "strip", z.ZodTypeAny, {
24
24
  keywords?: string[] | undefined;
25
- content_type?: "template" | "project-info" | "faq" | "guide" | "decision" | undefined;
25
+ content_type?: "project-info" | "template" | "faq" | "guide" | "decision" | undefined;
26
26
  customer?: string | undefined;
27
27
  category?: "archived" | "active" | "maintenance" | "legacy" | undefined;
28
28
  space_key?: string | undefined;
@@ -36,7 +36,7 @@ export declare function createConfluenceAgentTool(vectorStore: VectorStoreProvid
36
36
  parent_page_id?: string | undefined;
37
37
  }, {
38
38
  keywords?: string[] | undefined;
39
- content_type?: "template" | "project-info" | "faq" | "guide" | "decision" | undefined;
39
+ content_type?: "project-info" | "template" | "faq" | "guide" | "decision" | undefined;
40
40
  customer?: string | undefined;
41
41
  category?: "archived" | "active" | "maintenance" | "legacy" | undefined;
42
42
  space_key?: string | undefined;
@@ -53,7 +53,7 @@ export declare function createConfluenceAgentTool(vectorStore: VectorStoreProvid
53
53
  query: string;
54
54
  filters?: {
55
55
  keywords?: string[] | undefined;
56
- content_type?: "template" | "project-info" | "faq" | "guide" | "decision" | undefined;
56
+ content_type?: "project-info" | "template" | "faq" | "guide" | "decision" | undefined;
57
57
  customer?: string | undefined;
58
58
  category?: "archived" | "active" | "maintenance" | "legacy" | undefined;
59
59
  space_key?: string | undefined;
@@ -70,7 +70,7 @@ export declare function createConfluenceAgentTool(vectorStore: VectorStoreProvid
70
70
  query: string;
71
71
  filters?: {
72
72
  keywords?: string[] | undefined;
73
- content_type?: "template" | "project-info" | "faq" | "guide" | "decision" | undefined;
73
+ content_type?: "project-info" | "template" | "faq" | "guide" | "decision" | undefined;
74
74
  customer?: string | undefined;
75
75
  category?: "archived" | "active" | "maintenance" | "legacy" | undefined;
76
76
  space_key?: string | undefined;
@@ -87,7 +87,7 @@ export declare function createConfluenceAgentTool(vectorStore: VectorStoreProvid
87
87
  query: string;
88
88
  filters?: {
89
89
  keywords?: string[] | undefined;
90
- content_type?: "template" | "project-info" | "faq" | "guide" | "decision" | undefined;
90
+ content_type?: "project-info" | "template" | "faq" | "guide" | "decision" | undefined;
91
91
  customer?: string | undefined;
92
92
  category?: "archived" | "active" | "maintenance" | "legacy" | undefined;
93
93
  space_key?: string | undefined;
@@ -104,7 +104,7 @@ export declare function createConfluenceAgentTool(vectorStore: VectorStoreProvid
104
104
  query: string;
105
105
  filters?: {
106
106
  keywords?: string[] | undefined;
107
- content_type?: "template" | "project-info" | "faq" | "guide" | "decision" | undefined;
107
+ content_type?: "project-info" | "template" | "faq" | "guide" | "decision" | undefined;
108
108
  customer?: string | undefined;
109
109
  category?: "archived" | "active" | "maintenance" | "legacy" | undefined;
110
110
  space_key?: string | undefined;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Repomix search utilities — pure string/regex operations, no filesystem dependency.
3
+ * Core search logic adapted from repomix (https://github.com/yamadashy/repomix)
4
+ * src/mcp/tools/grepRepomixOutputTool.ts
5
+ */
6
+ interface SearchMatch {
7
+ lineNumber: number;
8
+ line: string;
9
+ matchedText: string;
10
+ }
11
+ interface SearchResult {
12
+ matches: SearchMatch[];
13
+ formattedOutput: string[];
14
+ }
15
+ interface SearchOptions {
16
+ ignoreCase?: boolean;
17
+ contextLines?: number;
18
+ }
19
+ /**
20
+ * Search a repomix XML string for lines matching a pattern.
21
+ * Returns matching lines with optional context.
22
+ */
23
+ export declare function searchRepomix(xml: string, pattern: string, options?: SearchOptions): SearchResult;
24
+ /**
25
+ * Extract the content of a specific file block from repomix XML.
26
+ * Looks for `<file path="filePath">...</file>` and returns the inner content.
27
+ * Returns null if the file is not found.
28
+ */
29
+ export declare function getRepomixFile(xml: string, filePath: string): string | null;
30
+ export {};
31
+ //# sourceMappingURL=repomix-search.d.ts.map
@@ -0,0 +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"}