Skip to content

Commit d3c3abb

Browse files
committed
Merge branch 'main' into feat/transaction-search-docs-and-deploy-note
2 parents ae57d09 + 5e64ea3 commit d3c3abb

File tree

16 files changed

+588
-456
lines changed

16 files changed

+588
-456
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Slack Open PRs Notification
2+
3+
on:
4+
schedule:
5+
- cron: '0 13 * * *' # 8:00 AM EST (13:00 UTC)
6+
workflow_dispatch:
7+
8+
permissions:
9+
pull-requests: read
10+
11+
jobs:
12+
notify-slack:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Get open PRs
16+
id: open-prs
17+
uses: actions/github-script@v8
18+
with:
19+
script: |
20+
const { data: prs } = await github.rest.pulls.list({
21+
owner: context.repo.owner,
22+
repo: context.repo.repo,
23+
state: 'open',
24+
});
25+
26+
const count = prs.length;
27+
28+
// Format each PR with plain text and bare URL (Slack auto-links URLs)
29+
const prList = prs.map(pr =>
30+
`• #${pr.number} - ${pr.title} (by ${pr.user.login})\n ${pr.html_url}`
31+
).join('\n');
32+
33+
core.setOutput('count', count);
34+
35+
// Use GITHUB_OUTPUT delimiter for multiline support
36+
const fs = require('fs');
37+
fs.appendFileSync(
38+
process.env.GITHUB_OUTPUT,
39+
`pr_list<<PRLIST_EOF\n${prList}\nPRLIST_EOF\n`
40+
);
41+
42+
- name: Send open PRs summary to Slack
43+
uses: slackapi/slack-github-action@v2.1.1
44+
with:
45+
webhook: ${{ secrets.SLACK_OPEN_PRS_WEBHOOK_URL }}
46+
webhook-type: webhook-trigger
47+
payload: |
48+
pr_count: "${{ steps.open-prs.outputs.count }}"
49+
pr_list: ${{ toJSON(steps.open-prs.outputs.pr_list) }}
50+
repository: "${{ github.repository }}"
51+
repository_url: "https://github.com/${{ github.repository }}/pulls"

package-lock.json

Lines changed: 323 additions & 418 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,13 @@
106106
"aws-cdk-lib": "^2.240.0",
107107
"constructs": "^10.4.4",
108108
"esbuild": "^0.27.2",
109-
"eslint": "^9.39.3",
109+
"eslint": "^10.0.3",
110110
"eslint-config-prettier": "^10.1.8",
111111
"eslint-import-resolver-typescript": "^4.4.4",
112112
"eslint-plugin-import": "^2.32.0",
113113
"eslint-plugin-react": "^7.37.5",
114114
"eslint-plugin-react-hooks": "^7.0.1",
115-
"eslint-plugin-react-refresh": "^0.4.26",
115+
"eslint-plugin-react-refresh": "^0.5.2",
116116
"eslint-plugin-security": "^4.0.0",
117117
"husky": "^9.1.7",
118118
"ink-testing-library": "^4.0.0",

src/cli/commands/add/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface AddGatewayOptions {
3535
agentClientId?: string;
3636
agentClientSecret?: string;
3737
agents?: string;
38+
semanticSearch?: boolean;
3839
json?: boolean;
3940
}
4041

src/cli/primitives/GatewayPrimitive.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface AddGatewayOptions {
2626
agentClientId?: string;
2727
agentClientSecret?: string;
2828
agents?: string;
29+
enableSemanticSearch?: boolean;
2930
}
3031

3132
/**
@@ -156,6 +157,7 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
156157
.option('--agent-client-id <id>', 'Agent OAuth client ID')
157158
.option('--agent-client-secret <secret>', 'Agent OAuth client secret')
158159
.option('--agents <agents>', 'Comma-separated agent names')
160+
.option('--no-semantic-search', 'Disable semantic search for tool discovery')
159161
.option('--json', 'Output as JSON')
160162
.action(async (rawOptions: Record<string, string | boolean | undefined>) => {
161163
const cliOptions = rawOptions as unknown as CLIAddGatewayOptions;
@@ -186,6 +188,7 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
186188
agentClientId: cliOptions.agentClientId,
187189
agentClientSecret: cliOptions.agentClientSecret,
188190
agents: cliOptions.agents,
191+
enableSemanticSearch: cliOptions.semanticSearch !== false,
189192
});
190193

191194
if (cliOptions.json) {
@@ -282,6 +285,7 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
282285
description: options.description ?? `Gateway for ${options.name}`,
283286
authorizerType: options.authorizerType,
284287
jwtConfig: undefined,
288+
enableSemanticSearch: options.enableSemanticSearch ?? true,
285289
};
286290

287291
if (options.authorizerType === 'CUSTOM_JWT' && options.discoveryUrl) {
@@ -348,6 +352,7 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
348352
targets: movedTargets,
349353
authorizerType: config.authorizerType,
350354
authorizerConfiguration: this.buildAuthorizerConfiguration(config),
355+
enableSemanticSearch: config.enableSemanticSearch,
351356
};
352357

353358
mcpSpec.agentCoreGateways.push(gateway);

src/cli/tui/hooks/__tests__/useMultiSelectNavigation.test.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMultiSelectNavigation } from '../useMultiSelectNavigation.js';
22
import { Text } from 'ink';
33
import { render } from 'ink-testing-library';
4-
import React from 'react';
4+
import React, { useImperativeHandle } from 'react';
55
import { afterEach, describe, expect, it, vi } from 'vitest';
66

77
const UP_ARROW = '\x1B[A';
@@ -34,13 +34,15 @@ function Harness({
3434
isActive,
3535
textInputActive,
3636
requireSelection,
37+
initialSelectedIds,
3738
}: {
3839
testItems?: Item[];
3940
onConfirm?: (ids: string[]) => void;
4041
onExit?: () => void;
4142
isActive?: boolean;
4243
textInputActive?: boolean;
4344
requireSelection?: boolean;
45+
initialSelectedIds?: string[];
4446
}) {
4547
const { cursorIndex, selectedIds } = useMultiSelectNavigation({
4648
items: testItems,
@@ -50,6 +52,7 @@ function Harness({
5052
isActive,
5153
textInputActive,
5254
requireSelection,
55+
initialSelectedIds,
5356
});
5457
return (
5558
<Text>
@@ -58,6 +61,27 @@ function Harness({
5861
);
5962
}
6063

64+
interface ResetHarnessHandle {
65+
reset: () => void;
66+
}
67+
68+
const ResetHarness = React.forwardRef<ResetHarnessHandle, { initialSelectedIds?: string[] }>(
69+
({ initialSelectedIds }, ref) => {
70+
const { cursorIndex, selectedIds, reset } = useMultiSelectNavigation({
71+
items,
72+
getId,
73+
initialSelectedIds,
74+
});
75+
useImperativeHandle(ref, () => ({ reset }));
76+
return (
77+
<Text>
78+
cursor:{cursorIndex} selected:{Array.from(selectedIds).sort().join(',')}
79+
</Text>
80+
);
81+
}
82+
);
83+
ResetHarness.displayName = 'ResetHarness';
84+
6185
describe('useMultiSelectNavigation', () => {
6286
it('starts with cursorIndex=0 and empty selectedIds', () => {
6387
const { lastFrame } = render(<Harness />);
@@ -217,4 +241,43 @@ describe('useMultiSelectNavigation', () => {
217241
await delay();
218242
expect(onExit).not.toHaveBeenCalled();
219243
});
244+
245+
it('initialSelectedIds pre-selects items', () => {
246+
const { lastFrame } = render(<Harness initialSelectedIds={['1', '3']} />);
247+
expect(lastFrame()).toContain('cursor:0');
248+
expect(lastFrame()).toContain('selected:1,3');
249+
});
250+
251+
it('initialSelectedIds items can be toggled off', async () => {
252+
const { lastFrame, stdin } = render(<Harness initialSelectedIds={['1']} />);
253+
await delay();
254+
expect(lastFrame()).toContain('selected:1');
255+
256+
// Cursor is at 0 (item id '1'), press Space to toggle it off
257+
stdin.write(SPACE);
258+
await delay();
259+
expect(lastFrame()).not.toMatch(/selected:\S/);
260+
});
261+
262+
it('reset restores initialSelectedIds', async () => {
263+
const ref = React.createRef<ResetHarnessHandle>();
264+
const { lastFrame, stdin } = render(<ResetHarness ref={ref} initialSelectedIds={['2']} />);
265+
await delay();
266+
expect(lastFrame()).toContain('selected:2');
267+
268+
// Move cursor to item '2' (index 1) and toggle it off
269+
stdin.write(DOWN_ARROW);
270+
await delay();
271+
stdin.write(SPACE);
272+
await delay();
273+
expect(lastFrame()).not.toMatch(/selected:\S/);
274+
275+
// Trigger reset to restore initialSelectedIds
276+
React.act(() => {
277+
ref.current!.reset();
278+
});
279+
await delay();
280+
expect(lastFrame()).toContain('selected:2');
281+
expect(lastFrame()).toContain('cursor:0');
282+
});
220283
});

src/cli/tui/hooks/useCreateMcp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function useCreateGateway() {
2828
allowedScopes: config.jwtConfig?.allowedScopes?.join(','),
2929
agentClientId: config.jwtConfig?.agentClientId,
3030
agentClientSecret: config.jwtConfig?.agentClientSecret,
31+
enableSemanticSearch: config.enableSemanticSearch,
3132
});
3233
if (!addResult.success) {
3334
throw new Error(addResult.error ?? 'Failed to create gateway');

src/cli/tui/hooks/useMultiSelectNavigation.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface UseMultiSelectNavigationOptions<T> {
1616
textInputActive?: boolean;
1717
/** Whether to require at least one selection before confirm (default: false) */
1818
requireSelection?: boolean;
19+
/** Initial set of selected item IDs (default: empty set) */
20+
initialSelectedIds?: string[];
1921
}
2022

2123
interface UseMultiSelectNavigationResult {
@@ -56,9 +58,10 @@ export function useMultiSelectNavigation<T>({
5658
isActive = true,
5759
textInputActive = false,
5860
requireSelection = false,
61+
initialSelectedIds,
5962
}: UseMultiSelectNavigationOptions<T>): UseMultiSelectNavigationResult {
6063
const [cursorIndex, setCursorIndex] = useState(0);
61-
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
64+
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set(initialSelectedIds ?? []));
6265

6366
const toggleSelection = useCallback(() => {
6467
const item = items[cursorIndex];
@@ -77,8 +80,8 @@ export function useMultiSelectNavigation<T>({
7780

7881
const reset = useCallback(() => {
7982
setCursorIndex(0);
80-
setSelectedIds(new Set());
81-
}, []);
83+
setSelectedIds(new Set(initialSelectedIds ?? []));
84+
}, [initialSelectedIds]);
8285

8386
useInput(
8487
(input, key) => {

src/cli/tui/screens/mcp/AddGatewayScreen.tsx

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { HELP_TEXT } from '../../constants';
1515
import { useListNavigation, useMultiSelectNavigation } from '../../hooks';
1616
import { generateUniqueName } from '../../utils';
1717
import type { AddGatewayConfig } from './types';
18-
import { AUTHORIZER_TYPE_OPTIONS, GATEWAY_STEP_LABELS } from './types';
18+
import { AUTHORIZER_TYPE_OPTIONS, GATEWAY_STEP_LABELS, SEMANTIC_SEARCH_ITEM_ID } from './types';
1919
import { useAddGatewayWizard } from './useAddGatewayWizard';
2020
import { Box, Text } from 'ink';
2121
import React, { useMemo, useState } from 'react';
@@ -27,6 +27,8 @@ interface AddGatewayScreenProps {
2727
unassignedTargets: string[];
2828
}
2929

30+
const INITIAL_ADVANCED_SELECTED = [SEMANTIC_SEARCH_ITEM_ID];
31+
3032
export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassignedTargets }: AddGatewayScreenProps) {
3133
const wizard = useAddGatewayWizard(unassignedTargets.length);
3234

@@ -48,10 +50,16 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
4850
[]
4951
);
5052

53+
const advancedConfigItems: SelectableItem[] = useMemo(
54+
() => [{ id: SEMANTIC_SEARCH_ITEM_ID, title: 'Semantic Search' }],
55+
[]
56+
);
57+
5158
const isNameStep = wizard.step === 'name';
5259
const isAuthorizerStep = wizard.step === 'authorizer';
5360
const isJwtConfigStep = wizard.step === 'jwt-config';
5461
const isIncludeTargetsStep = wizard.step === 'include-targets';
62+
const isAdvancedConfigStep = wizard.step === 'advanced-config';
5563
const isConfirmStep = wizard.step === 'confirm';
5664

5765
const authorizerNav = useListNavigation({
@@ -70,6 +78,17 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
7078
requireSelection: false,
7179
});
7280

81+
const advancedNav = useMultiSelectNavigation({
82+
items: advancedConfigItems,
83+
getId: item => item.id,
84+
initialSelectedIds: INITIAL_ADVANCED_SELECTED,
85+
onConfirm: selectedIds =>
86+
wizard.setAdvancedConfig({ enableSemanticSearch: selectedIds.includes(SEMANTIC_SEARCH_ITEM_ID) }),
87+
onExit: () => wizard.goBack(),
88+
isActive: isAdvancedConfigStep,
89+
requireSelection: false,
90+
});
91+
7392
useListNavigation({
7493
items: [{ id: 'confirm', title: 'Confirm' }],
7594
onSelect: () => onComplete(wizard.config),
@@ -136,13 +155,14 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
136155
}
137156
};
138157

139-
const helpText = isIncludeTargetsStep
140-
? 'Space toggle · Enter confirm · Esc back'
141-
: isConfirmStep
142-
? HELP_TEXT.CONFIRM_CANCEL
143-
: isAuthorizerStep
144-
? HELP_TEXT.NAVIGATE_SELECT
145-
: HELP_TEXT.TEXT_INPUT;
158+
const helpText =
159+
isIncludeTargetsStep || isAdvancedConfigStep
160+
? 'Space toggle · Enter confirm · Esc back'
161+
: isConfirmStep
162+
? HELP_TEXT.CONFIRM_CANCEL
163+
: isAuthorizerStep
164+
? HELP_TEXT.NAVIGATE_SELECT
165+
: HELP_TEXT.TEXT_INPUT;
146166

147167
const headerContent = <StepIndicator steps={wizard.steps} currentStep={wizard.step} labels={GATEWAY_STEP_LABELS} />;
148168

@@ -202,6 +222,30 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
202222
<Text dimColor>No unassigned targets available. Press Enter to continue.</Text>
203223
))}
204224

225+
{isAdvancedConfigStep && (
226+
<Box flexDirection="column">
227+
<Text bold>Advanced Configuration</Text>
228+
<Text dimColor>Toggle options with Space, press Enter to continue</Text>
229+
<Box marginTop={1} flexDirection="column">
230+
{advancedConfigItems.map((item, idx) => {
231+
const isCursor = idx === advancedNav.cursorIndex;
232+
const isChecked = advancedNav.selectedIds.has(item.id);
233+
const checkbox = isChecked ? '[✓]' : '[ ]';
234+
return (
235+
<Box key={item.id}>
236+
<Text wrap="truncate">
237+
<Text color={isCursor ? 'cyan' : undefined}>{isCursor ? '❯' : ' '} </Text>
238+
<Text color={isChecked ? 'green' : undefined}>{checkbox} </Text>
239+
<Text color={isCursor ? 'cyan' : undefined}>{item.title}</Text>
240+
</Text>
241+
<Text dimColor> {isChecked ? 'Enabled' : 'Disabled'}</Text>
242+
</Box>
243+
);
244+
})}
245+
</Box>
246+
</Box>
247+
)}
248+
205249
{isConfirmStep && (
206250
<ConfirmReview
207251
fields={[
@@ -228,6 +272,7 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
228272
? wizard.config.selectedTargets.join(', ')
229273
: '(none)',
230274
},
275+
{ label: 'Semantic Search', value: wizard.config.enableSemanticSearch ? 'Enabled' : 'Disabled' },
231276
]}
232277
/>
233278
)}

0 commit comments

Comments
 (0)