Skip to content

Commit 88d8a1b

Browse files
feat(tools): added linear tools/block (#439)
* feat(linear): add Linear Issue Reader and Writer tools with types * chore(tools): register Linear tools in global tool registry * feat(icons): add LinearIcon for Linear block * feat(blocks): register Linear block in global block registry * feat(linear): implement OAuth integration for Linear block * feat(linear): add dynamic team and project selectors for Linear block * feat(linear): add backend API endpoints for teams and projects * feat(linear): update UI components for Linear selectors and modal * refactor(linear): update create/read issue tools and types * chore(linear): update block config for Linear integration * fix(auth): update auth and oauth logic for Linear * minor fix * improvement[linear]: require teamId and projectId for all tools and types * style[lint]: fix code style and lint errors * chore(linear): install @linear/sdk package * fix[linear]: address greptile-apps feedback for type safety and error handling * fix[linear]: handle teams API response errors * modified icon, added docs --------- Co-authored-by: sriram2k4 <sriramthehacker01@gmail.com>
1 parent 50cbc89 commit 88d8a1b

File tree

21 files changed

+925
-1
lines changed

21 files changed

+925
-1
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
title: Linear
3+
description: Read and create issues in Linear
4+
---
5+
6+
import { BlockInfoCard } from "@/components/ui/block-info-card"
7+
8+
<BlockInfoCard
9+
type="linear"
10+
color="#5E6AD2"
11+
icon={true}
12+
iconSvg={`<svg className="block-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 100 100"><path fill="#fff" d="M1.22541 61.5228c-.2225-.9485.90748-1.5459 1.59638-.857L39.3342 97.1782c.6889.6889.0915 1.8189-.857 1.5964C20.0515 94.4522 5.54779 79.9485 1.22541 61.5228ZM.00189135 46.8891c-.01764375.2833.08887215.5599.28957165.7606L52.3503 99.7085c.2007.2007.4773.3075.7606.2896 2.3692-.1476 4.6938-.46 6.9624-.9259.7645-.157 1.0301-1.0963.4782-1.6481L2.57595 39.4485c-.55186-.5519-1.49117-.2863-1.648174.4782-.465915 2.2686-.77832 4.5932-.92588465 6.9624ZM4.21093 29.7054c-.16649.3738-.08169.8106.20765 1.1l64.77602 64.776c.2894.2894.7262.3742 1.1.2077 1.7861-.7956 3.5171-1.6927 5.1855-2.684.5521-.328.6373-1.0867.1832-1.5407L8.43566 24.3367c-.45409-.4541-1.21271-.3689-1.54074.1832-.99132 1.6684-1.88843 3.3994-2.68399 5.1855ZM12.6587 18.074c-.3701-.3701-.393-.9637-.0443-1.3541C21.7795 6.45931 35.1114 0 49.9519 0 77.5927 0 100 22.4073 100 50.0481c0 14.8405-6.4593 28.1724-16.7199 37.3375-.3903.3487-.984.3258-1.3542-.0443L12.6587 18.074Z"/></svg>`}
13+
/>
14+
15+
{/* MANUAL-CONTENT-START:intro */}
16+
[Linear](https://linear.app) is a leading project management and issue tracking platform that helps teams plan, track, and manage their work effectively. As a modern project management tool, Linear has become increasingly popular among software development teams and project management professionals for its streamlined interface and powerful features.
17+
18+
Linear provides a comprehensive set of tools for managing complex projects through its flexible and customizable workflow system. With its robust API and integration capabilities, Linear enables teams to streamline their development processes and maintain clear visibility of project progress.
19+
20+
Key features of Linear include:
21+
22+
- Agile Project Management: Support for Scrum and Kanban methodologies with customizable boards and workflows
23+
- Issue Tracking: Sophisticated tracking system for bugs, stories, epics, and tasks with detailed reporting
24+
- Workflow Automation: Powerful automation rules to streamline repetitive tasks and processes
25+
- Advanced Search: Complex filtering and reporting capabilities for efficient issue management
26+
27+
In Sim Studio, the Linear integration allows your agents to seamlessly interact with your project management workflow. This creates opportunities for automated issue creation, updates, and tracking as part of your AI workflows. The integration enables agents to read existing issues and create new ones programmatically, facilitating automated project management tasks and ensuring that important information is properly tracked and documented. By connecting Sim Studio with Linear, you can build intelligent agents that maintain project visibility while automating routine project management tasks, enhancing team productivity and ensuring consistent project tracking.
28+
{/* MANUAL-CONTENT-END */}
29+
30+
31+
## Usage Instructions
32+
33+
Integrate with Linear to fetch, filter, and create issues directly from your workflow.
34+
35+
36+
37+
## Tools
38+
39+
### `linear_read_issues`
40+
41+
Fetch and filter issues from Linear
42+
43+
#### Input
44+
45+
| Parameter | Type | Required | Description |
46+
| --------- | ---- | -------- | ----------- |
47+
| `teamId` | string | Yes | Linear team ID |
48+
| `projectId` | string | Yes | Linear project ID |
49+
50+
#### Output
51+
52+
| Parameter | Type |
53+
| --------- | ---- |
54+
| `issues` | string |
55+
56+
### `linear_create_issue`
57+
58+
Create a new issue in Linear
59+
60+
#### Input
61+
62+
| Parameter | Type | Required | Description |
63+
| --------- | ---- | -------- | ----------- |
64+
| `teamId` | string | Yes | Linear team ID |
65+
| `projectId` | string | Yes | Linear project ID |
66+
| `title` | string | Yes | Issue title |
67+
| `description` | string | No | Issue description |
68+
69+
#### Output
70+
71+
| Parameter | Type |
72+
| --------- | ---- |
73+
| `issue` | string |
74+
| `title` | string |
75+
| `description` | string |
76+
| `state` | string |
77+
| `teamId` | string |
78+
| `projectId` | string |
79+
80+
81+
82+
## Block Configuration
83+
84+
### Input
85+
86+
| Parameter | Type | Required | Description |
87+
| --------- | ---- | -------- | ----------- |
88+
| `operation` | string | Yes | Operation |
89+
90+
91+
92+
### Outputs
93+
94+
| Output | Type | Description |
95+
| ------ | ---- | ----------- |
96+
| `response` | object | Output from response |
97+
|`issues` | json | issues of the response |
98+
|`issue` | json | issue of the response |
99+
100+
101+
## Notes
102+
103+
- Category: `tools`
104+
- Type: `linear`

apps/docs/content/docs/tools/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"image_generator",
2222
"jina",
2323
"jira",
24+
"linear",
2425
"linkup",
2526
"mem0",
2627
"memory",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Project } from '@linear/sdk'
2+
import { LinearClient } from '@linear/sdk'
3+
import { NextResponse } from 'next/server'
4+
import { getSession } from '@/lib/auth'
5+
import { createLogger } from '@/lib/logs/console-logger'
6+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
7+
8+
export const dynamic = 'force-dynamic'
9+
10+
const logger = createLogger('LinearProjects')
11+
12+
export async function POST(request: Request) {
13+
try {
14+
const session = await getSession()
15+
const body = await request.json()
16+
const { credential, teamId, workflowId } = body
17+
18+
if (!credential || !teamId) {
19+
logger.error('Missing credential or teamId in request')
20+
return NextResponse.json({ error: 'Credential and teamId are required' }, { status: 400 })
21+
}
22+
23+
const userId = session?.user?.id || ''
24+
if (!userId) {
25+
logger.error('No user ID found in session')
26+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
27+
}
28+
29+
const accessToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId)
30+
if (!accessToken) {
31+
logger.error('Failed to get access token', { credentialId: credential, userId })
32+
return NextResponse.json(
33+
{
34+
error: 'Could not retrieve access token',
35+
authRequired: true,
36+
},
37+
{ status: 401 }
38+
)
39+
}
40+
41+
const linearClient = new LinearClient({ accessToken })
42+
let projects = []
43+
44+
const team = await linearClient.team(teamId)
45+
const projectsResult = await team.projects()
46+
projects = projectsResult.nodes.map((project: Project) => ({
47+
id: project.id,
48+
name: project.name,
49+
}))
50+
51+
if (projects.length === 0) {
52+
logger.info('No projects found for team', { teamId })
53+
}
54+
55+
return NextResponse.json({ projects })
56+
} catch (error) {
57+
logger.error('Error processing Linear projects request:', error)
58+
return NextResponse.json(
59+
{ error: 'Failed to retrieve Linear projects', details: (error as Error).message },
60+
{ status: 500 }
61+
)
62+
}
63+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { Team } from '@linear/sdk'
2+
import { LinearClient } from '@linear/sdk'
3+
import { NextResponse } from 'next/server'
4+
import { getSession } from '@/lib/auth'
5+
import { createLogger } from '@/lib/logs/console-logger'
6+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
7+
8+
export const dynamic = 'force-dynamic'
9+
10+
const logger = createLogger('LinearTeams')
11+
12+
export async function POST(request: Request) {
13+
try {
14+
const session = await getSession()
15+
const body = await request.json()
16+
const { credential, workflowId } = body
17+
18+
if (!credential) {
19+
logger.error('Missing credential in request')
20+
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
21+
}
22+
23+
const userId = session?.user?.id || ''
24+
if (!userId) {
25+
logger.error('No user ID found in session')
26+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
27+
}
28+
29+
const accessToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId)
30+
if (!accessToken) {
31+
logger.error('Failed to get access token', { credentialId: credential, userId })
32+
return NextResponse.json(
33+
{
34+
error: 'Could not retrieve access token',
35+
authRequired: true,
36+
},
37+
{ status: 401 }
38+
)
39+
}
40+
41+
const linearClient = new LinearClient({ accessToken })
42+
const teamsResult = await linearClient.teams()
43+
const teams = teamsResult.nodes.map((team: Team) => ({
44+
id: team.id,
45+
name: team.name,
46+
}))
47+
48+
return NextResponse.json({ teams })
49+
} catch (error) {
50+
logger.error('Error processing Linear teams request:', error)
51+
return NextResponse.json(
52+
{ error: 'Failed to retrieve Linear teams', details: (error as Error).message },
53+
{ status: 500 }
54+
)
55+
}
56+
}

apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
106106
'messages.read': 'Read your Discord messages',
107107
guilds: 'Read your Discord guilds',
108108
'guilds.members.read': 'Read your Discord guild members',
109+
read: 'Read access to your Linear workspace',
110+
write: 'Write access to your Linear workspace',
109111
}
110112

111113
// Convert OAuth scope to user-friendly description
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useEffect, useState } from 'react'
2+
import {
3+
Select,
4+
SelectContent,
5+
SelectItem,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@/components/ui/select'
9+
10+
export interface LinearProjectInfo {
11+
id: string
12+
name: string
13+
}
14+
15+
interface LinearProjectSelectorProps {
16+
value: string
17+
onChange: (projectId: string, projectInfo?: LinearProjectInfo) => void
18+
credential: string
19+
teamId: string
20+
label?: string
21+
disabled?: boolean
22+
}
23+
24+
export function LinearProjectSelector({
25+
value,
26+
onChange,
27+
credential,
28+
teamId,
29+
label = 'Select Linear project',
30+
disabled = false,
31+
}: LinearProjectSelectorProps) {
32+
const [projects, setProjects] = useState<LinearProjectInfo[]>([])
33+
const [loading, setLoading] = useState(false)
34+
const [error, setError] = useState<string | null>(null)
35+
36+
useEffect(() => {
37+
if (!credential || !teamId) return
38+
const controller = new AbortController()
39+
setLoading(true)
40+
fetch('/api/tools/linear/projects', {
41+
method: 'POST',
42+
headers: { 'Content-Type': 'application/json' },
43+
body: JSON.stringify({ credential, teamId }),
44+
signal: controller.signal,
45+
})
46+
.then(async (res) => {
47+
if (!res.ok) {
48+
const errorText = await res.text()
49+
throw new Error(`HTTP error! status: ${res.status} - ${errorText}`)
50+
}
51+
return res.json()
52+
})
53+
.then((data) => {
54+
if (data.error) {
55+
setError(data.error)
56+
setProjects([])
57+
} else {
58+
setProjects(data.projects)
59+
}
60+
})
61+
.catch((err) => {
62+
if (err.name === 'AbortError') return
63+
setError(err.message)
64+
setProjects([])
65+
})
66+
.finally(() => setLoading(false))
67+
return () => controller.abort()
68+
}, [credential, teamId])
69+
70+
return (
71+
<Select
72+
value={value}
73+
onValueChange={(projectId) => {
74+
const projectInfo = projects.find((p) => p.id === projectId)
75+
onChange(projectId, projectInfo)
76+
}}
77+
disabled={disabled || loading || !credential || !teamId}
78+
>
79+
<SelectTrigger className='w-full'>
80+
<SelectValue placeholder={loading ? 'Loading projects...' : label} />
81+
</SelectTrigger>
82+
<SelectContent>
83+
{projects.map((project) => (
84+
<SelectItem key={project.id} value={project.id}>
85+
{project.name}
86+
</SelectItem>
87+
))}
88+
{error && <div className='px-2 py-1 text-red-500'>{error}</div>}
89+
</SelectContent>
90+
</Select>
91+
)
92+
}

0 commit comments

Comments
 (0)