Skip to content

Commit 7238cb9

Browse files
committed
fix(search): add GitHub commit search and accept native-only live queries
1 parent 75fbc72 commit 7238cb9

11 files changed

Lines changed: 216 additions & 30 deletions

‎apps/sim/lib/api/contracts/mothership-assistant-tools.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,12 @@ describe('Assistant execution contracts', () => {
7373
topK: 20,
7474
})
7575
expect(TOOL_CATALOG.read_document?.parameters).toMatchObject({ required: ['documentId'] })
76+
const nativeQueries = [{ provider: 'github', query: 'author:@me', kind: 'commits' }]
77+
expect(searchWorkspaceInputSchema.parse({ nativeQueries })).toMatchObject({ query: '' })
78+
expect(searchWorkspaceInputSchema.safeParse({}).success).toBe(false)
79+
expect(
80+
searchWorkspaceInputSchema.safeParse({ nativeQueries: [{ provider: 'github', query: '' }] })
81+
.success
82+
).toBe(false)
7683
})
7784
})

‎apps/sim/lib/api/contracts/mothership-assistant-tools.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export const nativeSearchQuerySchema = z
1010
provider: liveSearchProviderSchema,
1111
query: z.string().trim().max(2000),
1212
accountId: z.string().min(1).max(200).optional(),
13-
kind: z.enum(['issues', 'code', 'repositories', 'merge_requests', 'wiki']).optional(),
13+
kind: z
14+
.enum(['issues', 'code', 'repositories', 'commits', 'merge_requests', 'wiki'])
15+
.optional(),
1416
project: z.string().min(1).max(300).optional(),
1517
cursor: z.string().max(4000).optional(),
1618
termClauses: z.array(z.string().max(500)).max(10).optional(),
@@ -115,7 +117,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
115117
nativeQueries: nativeSearchQueriesSchema
116118
.optional()
117119
.describe(
118-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
120+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
119121
),
120122
query: z
121123
.string()
@@ -138,6 +140,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
138140
.superRefine((input, context) => {
139141
if (
140142
!input.query &&
143+
!input.nativeQueries?.some((query) => query.query) &&
141144
!input.startDate &&
142145
!input.endDate &&
143146
!input.modifiedAfter &&
@@ -146,7 +149,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
146149
context.addIssue({
147150
code: 'custom',
148151
path: ['query'],
149-
message: 'Supply search terms or a date bound.',
152+
message: 'Supply search terms, a native query, or a date bound.',
150153
})
151154
if (
152155
input.startDate &&

‎apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export const nativeSearchQuerySchema = z
2222
provider: liveSearchProviderSchema,
2323
query: z.string().trim().max(2000),
2424
accountId: z.string().min(1).max(200).optional(),
25-
kind: z.enum(['issues', 'code', 'repositories', 'merge_requests', 'wiki']).optional(),
25+
kind: z
26+
.enum(['issues', 'code', 'repositories', 'commits', 'merge_requests', 'wiki'])
27+
.optional(),
2628
project: z.string().min(1).max(300).optional(),
2729
cursor: z.string().max(4000).optional(),
2830
termClauses: z.array(z.string().max(500)).max(10).optional(),
@@ -127,7 +129,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
127129
nativeQueries: nativeSearchQueriesSchema
128130
.optional()
129131
.describe(
130-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
132+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
131133
),
132134
query: z
133135
.string()
@@ -150,6 +152,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
150152
.superRefine((input, context) => {
151153
if (
152154
!input.query &&
155+
!input.nativeQueries?.some((query) => query.query) &&
153156
!input.startDate &&
154157
!input.endDate &&
155158
!input.modifiedAfter &&
@@ -158,7 +161,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
158161
context.addIssue({
159162
code: 'custom',
160163
path: ['query'],
161-
message: 'Supply search terms or a date bound.',
164+
message: 'Supply search terms, a native query, or a date bound.',
162165
})
163166
if (
164167
input.startDate &&

‎apps/sim/lib/mothership/generated/tool-catalog-v1.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6041,7 +6041,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
60416041
},
60426042
nativeQueries: {
60436043
description:
6044-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
6044+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
60456045
minItems: 1,
60466046
maxItems: 9,
60476047
type: 'array',
@@ -6066,7 +6066,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
60666066
accountId: { type: 'string', minLength: 1, maxLength: 200 },
60676067
kind: {
60686068
type: 'string',
6069-
enum: ['issues', 'code', 'repositories', 'merge_requests', 'wiki'],
6069+
enum: ['issues', 'code', 'repositories', 'commits', 'merge_requests', 'wiki'],
60706070
},
60716071
project: { type: 'string', minLength: 1, maxLength: 300 },
60726072
cursor: { type: 'string', maxLength: 4000 },

‎apps/sim/lib/mothership/generated/tool-schemas-v1.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5972,7 +5972,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
59725972
},
59735973
nativeQueries: {
59745974
description:
5975-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
5975+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
59765976
minItems: 1,
59775977
maxItems: 9,
59785978
type: 'array',
@@ -6004,7 +6004,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
60046004
},
60056005
kind: {
60066006
type: 'string',
6007-
enum: ['issues', 'code', 'repositories', 'merge_requests', 'wiki'],
6007+
enum: ['issues', 'code', 'repositories', 'commits', 'merge_requests', 'wiki'],
60086008
},
60096009
project: {
60106010
type: 'string',

‎apps/sim/lib/sim-search/live/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Member mode only. Search uses the connected user's Slack real-time search grant
5858

5959
### GitHub
6060

61-
Member mode searches issues, code, and repositories permitted by the connected token. Explicit repository/organization/user qualifiers narrow the user's query. Default discovery is bounded to up to 100 affiliated repositories, sent as `repo:` qualifiers in at most four batches per search kind. Code batches stay under code search's 1,000-byte query limit, issue batches are larger, and a long query that cannot fit every repository reports the searched subset. Date bounds become one `updated:start..end` range, since GitHub ORs repeated qualifiers. Provider pagination/search caps still apply. REST code search returns no file dates and accepts no date qualifier, so date-filtered searches cover issues and pull requests only.
61+
Member mode searches issues, code, repositories, and commits permitted by the connected token. Commits are searched only with an explicit `commits` kind; they cover each repository's default branch, and a commit read lists up to 100 changed files. Explicit repository/organization/user qualifiers narrow the user's query. Default discovery is bounded to up to 100 affiliated repositories, sent as `repo:` qualifiers in at most four batches per search kind. Code batches stay under code search's 1,000-byte query limit, issue batches are larger, and a long query that cannot fit every repository reports the searched subset. Date bounds become one `updated:start..end` range (`author-date:` for commits), since GitHub ORs repeated qualifiers. Provider pagination/search caps still apply. REST code search returns no file dates and accepts no date qualifier, so date-filtered searches cover issues and pull requests only.
6262

6363
In service mode, an administrator connects a GitHub App installation and selects repositories one by one in Sources. Each source pins the provider-verified repository ID and may narrow code files by directory and extension. Search queries the member's own GitHub connection with `repo:` qualifiers drawn only from active sources. For each candidate, Sim checks that the current App installation still covers that repository, mints a repository-scoped read token, and compares repository and owner IDs returned under both the App and member tokens. It then checks the per-repository code filters. Reads use the member token and repeat these checks. A personal repository outside the selected sources is never searched, even if the member can access it. GitHub REST code search covers the default branch; live Sources therefore do not offer a branch setting.
6464

@@ -124,7 +124,7 @@ Self-managed GitLab is resolved from the saved source's validated host/project i
124124
| Slack | `POST /api/assistant.search.context` | `conversations.replies` or `files.info` preview | Member only; Slack enforces the connected user's grant |
125125
| Jira | `POST /ex/jira/{cloudId}/rest/api/3/search/jql` | `/rest/api/3/issue/{key}` under that cloud site | Member only |
126126
| Confluence | `/ex/confluence/{cloudId}/wiki/rest/api/search` with CQL | `/wiki/rest/api/content/{id}` | Same site, spaces, current type/status/labels, source readability |
127-
| GitHub | `/search/issues`, `/search/code`, `/search/repositories` | Issue, repository, or contents endpoint for returned kind | Added repositories; installation coverage/stable IDs and code filters |
127+
| GitHub | `/search/issues`, `/search/code`, `/search/repositories`, `/search/commits` | Issue, repository, commit, or contents endpoint for returned kind | Added repositories; installation coverage/stable IDs and code filters |
128128
| GitLab | Configured `/api/v4/projects/{project}/search`, or supported date listing | Project issue/MR/wiki/file endpoint | Current request-local admin ACL evidence or saved CSV grants, plus content filters |
129129
| Coda | Personal MCP `search`; REST `/apis/v1/docs` title-search compatibility | MCP read allowlist; REST compatibility document/page reads | Selected parent doc and current source-token visibility; optional Enterprise org membership |
130130

‎apps/sim/lib/sim-search/live/application.test.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,22 @@ describe('authorized live retrieval', () => {
510510
expect(result.retrieval.status).toBe('partial')
511511
expect(result.live?.accounts[0]).toMatchObject({ status: 'partial', nextCursor: 'next' })
512512
})
513+
it('searches with only a native query and still rejects a search with neither', async () => {
514+
const nativeQueries = [{ provider: 'google_drive' as const, query: "name contains 'Q4'" }]
515+
await searchLiveKnowledge.execute({ principal, input: { ...input, query: '', nativeQueries } })
516+
expect(mocks.search).toHaveBeenCalledExactlyOnceWith(
517+
expect.anything(),
518+
expect.anything(),
519+
expect.objectContaining({ native: expect.objectContaining(nativeQueries[0]) })
520+
)
521+
await expect(
522+
searchLiveKnowledge.execute({
523+
principal,
524+
input: { ...input, query: '', nativeQueries: [{ provider: 'google_drive', query: '' }] },
525+
})
526+
).rejects.toThrow()
527+
expect(mocks.search).toHaveBeenCalledOnce()
528+
})
513529
it('rejects invalid dates before resolving provider credentials', async () => {
514530
await expect(
515531
searchLiveKnowledge.execute({

‎apps/sim/lib/sim-search/live/application.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,13 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
279279
const userId = requirePrincipalSubjectUserId(principal)
280280
if (input.organizationId) await requireOrganizationSearchAvailable(input.organizationId)
281281
input.signal?.throwIfAborted()
282+
const queries = input.nativeQueries
283+
? nativeSearchQueriesSchema.parse(input.nativeQueries)
284+
: undefined
282285
if (
283-
(!input.query.trim() && !hasDateBounds(input.filters)) ||
286+
(!input.query.trim() &&
287+
!hasDateBounds(input.filters) &&
288+
!queries?.some((query) => query.query)) ||
284289
input.query.length > 2000 ||
285290
!Number.isInteger(input.topK) ||
286291
input.topK < 1 ||
@@ -296,9 +301,6 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
296301
Date.parse(filters.startDate) >= Date.parse(filters.endDate)
297302
)
298303
throw new OrchestrationError('validation', 'endDate must be after startDate')
299-
const queries = input.nativeQueries
300-
? nativeSearchQueriesSchema.parse(input.nativeQueries)
301-
: undefined
302304
if (queries?.some((query) => !query.query) && !hasDateBounds(filters))
303305
throw new OrchestrationError('validation', 'Empty native queries require a date bound')
304306
const searchSignal = input.signal

‎apps/sim/lib/sim-search/live/github.ts‎

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,28 @@ import type {
1313
NativeSearchInput,
1414
} from '@/lib/sim-search/live/types'
1515

16+
/** A commit as a document: its message is the content and its author date the timeline. */
17+
function commitDocument(
18+
row: Record<string, unknown>,
19+
container = string(object(row.repository).full_name)
20+
): NativeDocument {
21+
const commit = object(row.commit)
22+
const message = string(commit.message)
23+
const authored = Date.parse(string(object(commit.author).date))
24+
return {
25+
id: string(row.sha),
26+
container,
27+
kind: 'commits',
28+
title: `${container} · ${message.split('\n')[0] || string(row.sha).slice(0, 7)}`,
29+
url: string(row.html_url),
30+
content: message,
31+
...(Number.isFinite(authored) ? { modifiedAt: new Date(authored).toISOString() } : {}),
32+
author: string(object(row.author).login) || string(object(commit.author).name),
33+
}
34+
}
35+
1636
function githubDocument(row: Record<string, unknown>, kind: string): NativeDocument {
37+
if (kind === 'commits') return commitDocument(row)
1738
const repository = object(row.repository)
1839
const repositoryUrl = string(row.repository_url)
1940
const container =
@@ -59,17 +80,31 @@ const GITHUB_ISSUE_QUERY_BYTES = 3800
5980
/** Batches per kind; each code batch spends one of GitHub's ten code searches per minute. */
6081
const GITHUB_MAX_REPOSITORY_BATCHES = 4
6182

62-
type GitHubKind = 'issues' | 'code' | 'repositories'
83+
/** Changed files listed when reading a commit, GitHub's default page size. */
84+
const GITHUB_COMMIT_FILES = 30
85+
86+
const GITHUB_KINDS = ['issues', 'code', 'repositories', 'commits'] as const
87+
type GitHubKind = (typeof GITHUB_KINDS)[number]
88+
const isGitHubKind = (kind: string | undefined): kind is GitHubKind =>
89+
GITHUB_KINDS.some((supported) => supported === kind)
90+
/**
91+
* The date each kind filters and sorts by: issues by last update, commits by author date. Code
92+
* and repository search take no date qualifier.
93+
*/
94+
const GITHUB_DATE_FIELD: Partial<Record<GitHubKind, 'updated' | 'author-date'>> = {
95+
issues: 'updated',
96+
commits: 'author-date',
97+
}
6398
const CODE_EXCLUDED_BY_DATES = 'Code has no dates and is excluded from date-filtered searches.'
6499

65100
/** Code has no file dates, so a date-bounded search covers issues and pull requests only. */
66101
function githubKinds(input: NativeSearchInput): GitHubKind[] {
67102
const kind = input.native?.kind
68-
if (kind === 'issues' || kind === 'code' || kind === 'repositories') return [kind]
103+
if (isGitHubKind(kind)) return [kind]
69104
if (kind)
70105
throw new NativeSearchError(
71106
'unavailable',
72-
'GitHub search supports issues, code, or repositories.'
107+
`GitHub search supports these kinds: ${GITHUB_KINDS.join(', ')}.`
73108
)
74109
return hasDateBounds(input.filters) ? ['issues'] : ['issues', 'code']
75110
}
@@ -240,17 +275,17 @@ export async function searchGitHub(
240275
)
241276
const dates = nativeDateBounds(input)
242277
const text = nativeText(input)
243-
/** GitHub ORs repeated qualifiers, so both bounds must share one `updated:` range. */
244-
const updated =
278+
/** GitHub ORs repeated qualifiers, so both bounds share one range qualifier. */
279+
const dateField = GITHUB_DATE_FIELD[kind]
280+
const dateRange =
245281
dates.start && dates.end
246-
? `updated:${dates.start}..${dates.end}`
282+
? `${dateField}:${dates.start}..${dates.end}`
247283
: dates.start
248-
? `updated:>=${dates.start}`
284+
? `${dateField}:>=${dates.start}`
249285
: dates.end
250-
? `updated:<=${dates.end}`
286+
? `${dateField}:<=${dates.end}`
251287
: ''
252-
const datedQuery =
253-
kind === 'issues' ? [groupGitHubText(text), updated].filter(Boolean).join(' ') : text
288+
const datedQuery = dateField ? [groupGitHubText(text), dateRange].filter(Boolean).join(' ') : text
254289
if (githubTextLength(text) > GITHUB_TEXT_CHARACTERS)
255290
throw new NativeSearchError(
256291
'unavailable',
@@ -266,8 +301,8 @@ export async function searchGitHub(
266301
q: hasDateBounds(input.filters) ? datedQuery : text,
267302
per_page: String(input.limit),
268303
page,
269-
...(kind === 'issues' && dateSortDirection(input.filters)
270-
? { sort: 'updated', order: dateSortDirection(input.filters) }
304+
...(dateField && dateSortDirection(input.filters)
305+
? { sort: dateField, order: dateSortDirection(input.filters) }
271306
: {}),
272307
},
273308
})
@@ -333,6 +368,33 @@ export async function readGitHub(
333368
const row = object(await client.json(path))
334369
return githubDocument(row, 'repositories')
335370
}
371+
if (kind === 'commits') {
372+
if (!/^[0-9a-f]{7,40}$/i.test(id))
373+
throw new NativeSearchError('unavailable', 'Invalid GitHub commit reference.')
374+
/** Each changed file carries its patch, so the page bounds the response size. */
375+
const row = object(
376+
await client.json(`${path}/commits/${id}`, {
377+
query: { per_page: String(GITHUB_COMMIT_FILES) },
378+
})
379+
)
380+
const files = array(row.files).map(
381+
(file) =>
382+
`${string(file.status)} ${string(file.filename)} (+${string(file.additions)} -${string(file.deletions)})`
383+
)
384+
const document = commitDocument(row, repository)
385+
return {
386+
...document,
387+
content: [
388+
document.content,
389+
files.length ? `Files changed:\n${files.join('\n')}` : '',
390+
files.length === GITHUB_COMMIT_FILES
391+
? `At most ${GITHUB_COMMIT_FILES} changed files are listed.`
392+
: '',
393+
]
394+
.filter(Boolean)
395+
.join('\n\n'),
396+
}
397+
}
336398
if (!/^\d+$/.test(id))
337399
throw new NativeSearchError('unavailable', 'Invalid GitHub issue reference.')
338400
return githubDocument(object(await client.json(`${path}/issues/${id}`)), 'issues')

0 commit comments

Comments
 (0)