@meetopenbot/linear 0.0.1 → 0.0.3
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.
- package/README.md +1 -1
- package/dist/api.d.ts +79 -2
- package/dist/api.js +76 -12
- package/dist/config.d.ts +2 -0
- package/dist/config.js +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -3
- package/dist/oauth.js +14 -5
- package/dist/tools.js +45 -69
- package/package.json +1 -1
- package/src/config.ts +4 -2
- package/src/index.ts +4 -2
- package/src/oauth.ts +14 -5
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Create a personal API key at **Linear → Settings → API** and either put it i
|
|
|
54
54
|
|
|
55
55
|
| Field | Description | Default |
|
|
56
56
|
| --- | --- | --- |
|
|
57
|
-
| `clientId` | Linear OAuth application Client ID |
|
|
57
|
+
| `clientId` | Linear OAuth application Client ID (overrides OpenBot's managed application) | OpenBot managed client ID |
|
|
58
58
|
| `clientSecret` | OAuth Client Secret (optional; PKCE used when omitted) | — |
|
|
59
59
|
| `apiKey` | Personal API key (skips OAuth entirely) | — |
|
|
60
60
|
| `oauthPort` | Local port for the OAuth callback server | `4137` |
|
package/dist/api.d.ts
CHANGED
|
@@ -1,8 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Thin Linear GraphQL client over fetch. We use raw GraphQL instead of
|
|
3
|
+
* @linear/sdk so list queries resolve relations (state, assignee, team) in a
|
|
4
|
+
* single request and the plugin stays dependency-light.
|
|
3
5
|
*/
|
|
4
6
|
export declare const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
|
|
5
|
-
export
|
|
7
|
+
export type LinearAuth = {
|
|
8
|
+
kind: 'apiKey';
|
|
9
|
+
token: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'oauth';
|
|
12
|
+
token: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class LinearApiError extends Error {
|
|
15
|
+
readonly status?: number | undefined;
|
|
16
|
+
constructor(message: string, status?: number | undefined);
|
|
17
|
+
}
|
|
18
|
+
export declare function linearRequest<T = Record<string, unknown>>(auth: LinearAuth, query: string, variables?: Record<string, unknown>): Promise<T>;
|
|
19
|
+
export declare const ISSUE_FIELDS = "\n id\n identifier\n title\n description\n priority\n priorityLabel\n url\n createdAt\n updatedAt\n dueDate\n state { id name type }\n assignee { id name displayName }\n team { id key name }\n project { id name }\n labels { nodes { id name } }\n";
|
|
20
|
+
export interface LinearIssue {
|
|
21
|
+
id: string;
|
|
22
|
+
identifier: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description?: string | null;
|
|
25
|
+
priority: number;
|
|
26
|
+
priorityLabel: string;
|
|
27
|
+
url: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
dueDate?: string | null;
|
|
31
|
+
state?: {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
type: string;
|
|
35
|
+
} | null;
|
|
36
|
+
assignee?: {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
displayName: string;
|
|
40
|
+
} | null;
|
|
41
|
+
team?: {
|
|
42
|
+
id: string;
|
|
43
|
+
key: string;
|
|
44
|
+
name: string;
|
|
45
|
+
} | null;
|
|
46
|
+
project?: {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
} | null;
|
|
50
|
+
labels?: {
|
|
51
|
+
nodes: Array<{
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
}>;
|
|
55
|
+
} | null;
|
|
56
|
+
}
|
|
57
|
+
/** Compact issue representation returned to the model/tool caller. */
|
|
58
|
+
export declare function formatIssue(issue: LinearIssue): {
|
|
59
|
+
id: string;
|
|
60
|
+
identifier: string;
|
|
61
|
+
title: string;
|
|
62
|
+
description: string | undefined;
|
|
63
|
+
state: string | undefined;
|
|
64
|
+
stateType: string | undefined;
|
|
65
|
+
priority: string;
|
|
66
|
+
assignee: string | undefined;
|
|
67
|
+
team: string | undefined;
|
|
68
|
+
project: string | undefined;
|
|
69
|
+
labels: string[] | undefined;
|
|
70
|
+
dueDate: string | undefined;
|
|
71
|
+
url: string;
|
|
72
|
+
updatedAt: string;
|
|
73
|
+
};
|
|
74
|
+
export declare function fetchViewer(auth: LinearAuth): Promise<{
|
|
6
75
|
viewer: {
|
|
7
76
|
id: string;
|
|
8
77
|
name: string;
|
|
@@ -15,3 +84,11 @@ export declare function fetchViewer(accessToken: string): Promise<{
|
|
|
15
84
|
urlKey: string;
|
|
16
85
|
};
|
|
17
86
|
}>;
|
|
87
|
+
/** Resolve a team by UUID, key (e.g. "ENG"), or exact name. */
|
|
88
|
+
export declare function resolveTeam(auth: LinearAuth, team: string): Promise<{
|
|
89
|
+
id: string;
|
|
90
|
+
key: string;
|
|
91
|
+
name: string;
|
|
92
|
+
} | {
|
|
93
|
+
id: string;
|
|
94
|
+
}>;
|
package/dist/api.js
CHANGED
|
@@ -1,29 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Thin Linear GraphQL client over fetch. We use raw GraphQL instead of
|
|
3
|
+
* @linear/sdk so list queries resolve relations (state, assignee, team) in a
|
|
4
|
+
* single request and the plugin stays dependency-light.
|
|
3
5
|
*/
|
|
4
|
-
export const LINEAR_GRAPHQL_URL =
|
|
5
|
-
|
|
6
|
+
export const LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql';
|
|
7
|
+
export class LinearApiError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
constructor(message, status) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.name = 'LinearApiError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function linearRequest(auth, query, variables) {
|
|
6
16
|
const response = await fetch(LINEAR_GRAPHQL_URL, {
|
|
7
|
-
method:
|
|
17
|
+
method: 'POST',
|
|
8
18
|
headers: {
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
// Personal API keys are sent bare; OAuth tokens use the Bearer scheme.
|
|
21
|
+
Authorization: auth.kind === 'apiKey' ? auth.token : `Bearer ${auth.token}`,
|
|
11
22
|
},
|
|
12
23
|
body: JSON.stringify({ query, variables }),
|
|
13
24
|
});
|
|
14
25
|
if (!response.ok) {
|
|
15
|
-
const body = await response.text().catch(() =>
|
|
16
|
-
|
|
26
|
+
const body = await response.text().catch(() => '');
|
|
27
|
+
if (response.status === 401) {
|
|
28
|
+
throw new LinearApiError('Linear rejected the credentials (401). Reconnect with `linear_connect` or update the API key.', 401);
|
|
29
|
+
}
|
|
30
|
+
throw new LinearApiError(`Linear API request failed (${response.status}): ${body.slice(0, 500)}`, response.status);
|
|
17
31
|
}
|
|
18
32
|
const payload = (await response.json());
|
|
19
33
|
if (payload.errors?.length) {
|
|
20
|
-
throw new
|
|
34
|
+
throw new LinearApiError(payload.errors.map((e) => e.message).join('; '));
|
|
21
35
|
}
|
|
22
36
|
if (!payload.data) {
|
|
23
|
-
throw new
|
|
37
|
+
throw new LinearApiError('Linear API returned no data.');
|
|
24
38
|
}
|
|
25
39
|
return payload.data;
|
|
26
40
|
}
|
|
27
|
-
export
|
|
28
|
-
|
|
41
|
+
export const ISSUE_FIELDS = `
|
|
42
|
+
id
|
|
43
|
+
identifier
|
|
44
|
+
title
|
|
45
|
+
description
|
|
46
|
+
priority
|
|
47
|
+
priorityLabel
|
|
48
|
+
url
|
|
49
|
+
createdAt
|
|
50
|
+
updatedAt
|
|
51
|
+
dueDate
|
|
52
|
+
state { id name type }
|
|
53
|
+
assignee { id name displayName }
|
|
54
|
+
team { id key name }
|
|
55
|
+
project { id name }
|
|
56
|
+
labels { nodes { id name } }
|
|
57
|
+
`;
|
|
58
|
+
/** Compact issue representation returned to the model/tool caller. */
|
|
59
|
+
export function formatIssue(issue) {
|
|
60
|
+
return {
|
|
61
|
+
id: issue.id,
|
|
62
|
+
identifier: issue.identifier,
|
|
63
|
+
title: issue.title,
|
|
64
|
+
description: issue.description ?? undefined,
|
|
65
|
+
state: issue.state?.name,
|
|
66
|
+
stateType: issue.state?.type,
|
|
67
|
+
priority: issue.priorityLabel,
|
|
68
|
+
assignee: issue.assignee?.displayName ?? issue.assignee?.name,
|
|
69
|
+
team: issue.team?.key,
|
|
70
|
+
project: issue.project?.name,
|
|
71
|
+
labels: issue.labels?.nodes.map((l) => l.name),
|
|
72
|
+
dueDate: issue.dueDate ?? undefined,
|
|
73
|
+
url: issue.url,
|
|
74
|
+
updatedAt: issue.updatedAt,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function fetchViewer(auth) {
|
|
78
|
+
const data = await linearRequest(auth, `query { viewer { id name displayName email } organization { id name urlKey } }`);
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
/** Resolve a team by UUID, key (e.g. "ENG"), or exact name. */
|
|
82
|
+
export async function resolveTeam(auth, team) {
|
|
83
|
+
const uuidLike = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
84
|
+
if (uuidLike.test(team))
|
|
85
|
+
return { id: team };
|
|
86
|
+
const data = await linearRequest(auth, `query { teams(first: 100) { nodes { id key name } } }`);
|
|
87
|
+
const match = data.teams.nodes.find((t) => t.key.toLowerCase() === team.toLowerCase() || t.name.toLowerCase() === team.toLowerCase());
|
|
88
|
+
if (!match) {
|
|
89
|
+
const available = data.teams.nodes.map((t) => t.key).join(', ');
|
|
90
|
+
throw new LinearApiError(`No Linear team matches "${team}". Available team keys: ${available}`);
|
|
91
|
+
}
|
|
92
|
+
return match;
|
|
29
93
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export declare const VAR_ACCESS_TOKEN = "LINEAR_ACCESS_TOKEN";
|
|
|
5
5
|
export declare const VAR_REFRESH_TOKEN = "LINEAR_REFRESH_TOKEN";
|
|
6
6
|
export declare const VAR_TOKEN_EXPIRES_AT = "LINEAR_TOKEN_EXPIRES_AT";
|
|
7
7
|
export declare const VAR_CLIENT_ID = "LINEAR_CLIENT_ID";
|
|
8
|
+
export declare const GO_BACK_TO_OPENBOT_URL = "https://openbot.one/settings/agents";
|
|
9
|
+
export declare const LINEAR_OPENBOT_CLIENT_ID = "1263fc2dbbf08efa7b2c4b1b2b2582be";
|
|
8
10
|
export type LinearPluginConfig = {
|
|
9
11
|
clientId?: string;
|
|
10
12
|
clientSecret?: string;
|
package/dist/config.js
CHANGED
|
@@ -4,6 +4,8 @@ export const VAR_ACCESS_TOKEN = "LINEAR_ACCESS_TOKEN";
|
|
|
4
4
|
export const VAR_REFRESH_TOKEN = "LINEAR_REFRESH_TOKEN";
|
|
5
5
|
export const VAR_TOKEN_EXPIRES_AT = "LINEAR_TOKEN_EXPIRES_AT";
|
|
6
6
|
export const VAR_CLIENT_ID = "LINEAR_CLIENT_ID";
|
|
7
|
+
export const GO_BACK_TO_OPENBOT_URL = "https://openbot.one/settings/agents";
|
|
8
|
+
export const LINEAR_OPENBOT_CLIENT_ID = "1263fc2dbbf08efa7b2c4b1b2b2582be";
|
|
7
9
|
const REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
8
10
|
function variableValue(variables, key) {
|
|
9
11
|
const entry = variables[key];
|
|
@@ -15,7 +17,7 @@ export function readLinearConfig(config) {
|
|
|
15
17
|
return {
|
|
16
18
|
clientId: typeof config.clientId === "string" && config.clientId.trim()
|
|
17
19
|
? config.clientId.trim()
|
|
18
|
-
:
|
|
20
|
+
: LINEAR_OPENBOT_CLIENT_ID,
|
|
19
21
|
clientSecret: typeof config.clientSecret === "string" && config.clientSecret.trim()
|
|
20
22
|
? config.clientSecret.trim()
|
|
21
23
|
: undefined,
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agentOutput, definePlugin, shouldHandleInvoke, toolResult, uiWidget, webhookHttpResponse, } from "@meetopenbot/plugin-sdk";
|
|
2
|
-
import { clearTokens, formatMissingCredentials, readLinearConfig, resolveLinearCredentials, resolveWebhookBaseUrl, saveTokens, } from "./config.js";
|
|
2
|
+
import { clearTokens, formatMissingCredentials, LINEAR_OPENBOT_CLIENT_ID, readLinearConfig, resolveLinearCredentials, resolveWebhookBaseUrl, saveTokens, } from "./config.js";
|
|
3
3
|
import { runLinearAgent } from "./linear-agent.js";
|
|
4
4
|
import { buildIssuesListWidget, isAssignedIssuesPrompt, isListIssuesPrompt, issuesListTitle, } from "./linear-issues.js";
|
|
5
5
|
import { OAUTH_WEBHOOK_PROVIDER, buildOAuthRedirectUri, fetchViewer, handleWebhookOAuthCallback, startOAuthFlow, startWebhookOAuthFlow, } from "./oauth.js";
|
|
@@ -47,7 +47,7 @@ function* yieldOAuthAuthorizeWidget(agentId, authorizeUrl, modeHint, options) {
|
|
|
47
47
|
id: OPEN_URL_ACTION_ID,
|
|
48
48
|
label: "Connect Linear",
|
|
49
49
|
variant: "primary",
|
|
50
|
-
value: { url: authorizeUrl
|
|
50
|
+
value: { url: authorizeUrl },
|
|
51
51
|
},
|
|
52
52
|
],
|
|
53
53
|
},
|
|
@@ -170,7 +170,8 @@ const linearPluginConfigSchema = {
|
|
|
170
170
|
properties: {
|
|
171
171
|
clientId: {
|
|
172
172
|
type: "string",
|
|
173
|
-
description: "Linear OAuth application Client ID
|
|
173
|
+
description: "Linear OAuth application Client ID. Defaults to OpenBot's managed Linear application.",
|
|
174
|
+
default: LINEAR_OPENBOT_CLIENT_ID,
|
|
174
175
|
},
|
|
175
176
|
clientSecret: {
|
|
176
177
|
type: "string",
|
package/dist/oauth.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createHash, randomBytes } from "node:crypto";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
|
+
import { GO_BACK_TO_OPENBOT_URL } from "./config.js";
|
|
10
11
|
import { clearPendingOAuthSession, loadPendingOAuthSession, savePendingOAuthSession, } from "./oauth-pending.js";
|
|
11
12
|
export const LINEAR_AUTHORIZE_URL = "https://linear.app/oauth/authorize";
|
|
12
13
|
export const LINEAR_TOKEN_URL = "https://api.linear.app/oauth/token";
|
|
@@ -31,19 +32,27 @@ export function oauthHtmlPage(title, body, ok) {
|
|
|
31
32
|
<meta charset="utf-8" />
|
|
32
33
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
33
34
|
<title>${title}</title>
|
|
35
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
36
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
37
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
34
38
|
<style>
|
|
35
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #101012; color: #ededef; }
|
|
36
|
-
.card { text-align: center; padding: 48px
|
|
37
|
-
.icon { font-size:
|
|
38
|
-
|
|
39
|
+
body { font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #101012; color: #ededef; }
|
|
40
|
+
.card { text-align: center; padding: 40px 48px; border-radius: 14px; background: #1b1b1f; border: 1px solid #2a2a30; max-width: 420px; }
|
|
41
|
+
.icon { height: 28px; margin-bottom: 18px; font-size: 28px; font-weight: 500; line-height: 28px; }
|
|
42
|
+
.success { color: #4ade80; }
|
|
43
|
+
h1 { font-size: 20px; font-weight: 600; margin: 0 0 8px; }
|
|
39
44
|
p { color: #9b9ba3; margin: 0; line-height: 1.5; }
|
|
45
|
+
.back-link { margin: 20px 0 0; font-size: 14px; }
|
|
46
|
+
.back-link a { color: #ededef; text-decoration: none; }
|
|
47
|
+
.back-link a:hover { text-decoration: underline; }
|
|
40
48
|
</style>
|
|
41
49
|
</head>
|
|
42
50
|
<body>
|
|
43
51
|
<div class="card">
|
|
44
|
-
<div class="icon">${ok ? "
|
|
52
|
+
<div class="icon${ok ? " success" : ""}">${ok ? "✓" : "⚠"}</div>
|
|
45
53
|
<h1>${title}</h1>
|
|
46
54
|
<p>${body}</p>
|
|
55
|
+
<p class="back-link">Go back to <a href="${GO_BACK_TO_OPENBOT_URL}">OpenBot</a></p>
|
|
47
56
|
</div>
|
|
48
57
|
</body>
|
|
49
58
|
</html>`;
|
package/dist/tools.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* model plus a human-readable `output` summary, and optionally a UI widget
|
|
4
4
|
* spec rendered in the OpenBot client.
|
|
5
5
|
*/
|
|
6
|
+
import { z } from 'zod';
|
|
6
7
|
import { ISSUE_FIELDS, LinearApiError, fetchViewer, formatIssue, linearRequest, resolveTeam, } from './api.js';
|
|
7
8
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
9
|
async function resolveIssueId(auth, idOrIdentifier) {
|
|
@@ -42,7 +43,9 @@ export const linearTools = {
|
|
|
42
43
|
linear_status: {
|
|
43
44
|
definition: {
|
|
44
45
|
description: 'Check whether Linear is connected and which user/workspace the credentials belong to.',
|
|
45
|
-
inputSchema
|
|
46
|
+
// The openbot runtime plugin passes inputSchema straight to the AI SDK,
|
|
47
|
+
// which requires Zod schemas (plain JSON Schema objects fail).
|
|
48
|
+
inputSchema: z.object({}),
|
|
46
49
|
},
|
|
47
50
|
run: async (auth) => {
|
|
48
51
|
const { viewer, organization } = await fetchViewer(auth);
|
|
@@ -55,7 +58,7 @@ export const linearTools = {
|
|
|
55
58
|
list_teams: {
|
|
56
59
|
definition: {
|
|
57
60
|
description: 'List Linear teams, including their workflow states (useful for setting issue status).',
|
|
58
|
-
inputSchema:
|
|
61
|
+
inputSchema: z.object({}),
|
|
59
62
|
},
|
|
60
63
|
run: async (auth) => {
|
|
61
64
|
const data = await linearRequest(auth, `query { teams(first: 100) { nodes { id key name states { nodes { id name type } } } } }`);
|
|
@@ -69,19 +72,15 @@ export const linearTools = {
|
|
|
69
72
|
list_issues: {
|
|
70
73
|
definition: {
|
|
71
74
|
description: 'List Linear issues, optionally filtered by team (key, name, or id), assignee ("me" or a user id), and state type.',
|
|
72
|
-
inputSchema: {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
},
|
|
82
|
-
limit: { type: 'number', description: 'Max issues to return (default 25)' },
|
|
83
|
-
},
|
|
84
|
-
},
|
|
75
|
+
inputSchema: z.object({
|
|
76
|
+
team: z.string().optional().describe('Team key (e.g. "ENG"), name, or id'),
|
|
77
|
+
assignee: z.string().optional().describe('"me" for the connected user, or a Linear user id'),
|
|
78
|
+
stateType: z
|
|
79
|
+
.enum(['triage', 'backlog', 'unstarted', 'started', 'completed', 'canceled'])
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('Filter by workflow state type'),
|
|
82
|
+
limit: z.number().optional().describe('Max issues to return (default 25)'),
|
|
83
|
+
}),
|
|
85
84
|
},
|
|
86
85
|
run: async (auth, args) => {
|
|
87
86
|
const { team, assignee, stateType, limit } = args;
|
|
@@ -111,14 +110,10 @@ export const linearTools = {
|
|
|
111
110
|
search_issues: {
|
|
112
111
|
definition: {
|
|
113
112
|
description: 'Full-text search Linear issues by keyword.',
|
|
114
|
-
inputSchema: {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
limit: { type: 'number', description: 'Max results (default 10)' },
|
|
119
|
-
},
|
|
120
|
-
required: ['query'],
|
|
121
|
-
},
|
|
113
|
+
inputSchema: z.object({
|
|
114
|
+
query: z.string().describe('Search term'),
|
|
115
|
+
limit: z.number().optional().describe('Max results (default 10)'),
|
|
116
|
+
}),
|
|
122
117
|
},
|
|
123
118
|
run: async (auth, args) => {
|
|
124
119
|
const { query, limit } = args;
|
|
@@ -134,13 +129,9 @@ export const linearTools = {
|
|
|
134
129
|
get_issue: {
|
|
135
130
|
definition: {
|
|
136
131
|
description: 'Get full details of a Linear issue by identifier (e.g. "ENG-123") or id, including comments.',
|
|
137
|
-
inputSchema: {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
issue: { type: 'string', description: 'Issue identifier (e.g. "ENG-123") or Linear issue id' },
|
|
141
|
-
},
|
|
142
|
-
required: ['issue'],
|
|
143
|
-
},
|
|
132
|
+
inputSchema: z.object({
|
|
133
|
+
issue: z.string().describe('Issue identifier (e.g. "ENG-123") or Linear issue id'),
|
|
134
|
+
}),
|
|
144
135
|
},
|
|
145
136
|
run: async (auth, args) => {
|
|
146
137
|
const { issue: issueRef } = args;
|
|
@@ -161,18 +152,14 @@ export const linearTools = {
|
|
|
161
152
|
create_issue: {
|
|
162
153
|
definition: {
|
|
163
154
|
description: 'Create a new Linear issue.',
|
|
164
|
-
inputSchema: {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
stateId: { type: 'string', description: 'Workflow state id (see list_teams)' },
|
|
173
|
-
},
|
|
174
|
-
required: ['team', 'title'],
|
|
175
|
-
},
|
|
155
|
+
inputSchema: z.object({
|
|
156
|
+
team: z.string().describe('Team key (e.g. "ENG"), name, or id'),
|
|
157
|
+
title: z.string().describe('Issue title'),
|
|
158
|
+
description: z.string().optional().describe('Issue description (markdown)'),
|
|
159
|
+
priority: z.number().optional().describe('0 none, 1 urgent, 2 high, 3 medium, 4 low'),
|
|
160
|
+
assignee: z.string().optional().describe('"me" or a Linear user id'),
|
|
161
|
+
stateId: z.string().optional().describe('Workflow state id (see list_teams)'),
|
|
162
|
+
}),
|
|
176
163
|
},
|
|
177
164
|
run: async (auth, args) => {
|
|
178
165
|
const { team, title, description, priority, assignee, stateId } = args;
|
|
@@ -191,18 +178,14 @@ export const linearTools = {
|
|
|
191
178
|
update_issue: {
|
|
192
179
|
definition: {
|
|
193
180
|
description: 'Update a Linear issue (title, description, priority, state, assignee).',
|
|
194
|
-
inputSchema: {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
stateId: { type: 'string', description: 'Workflow state id (see list_teams)' },
|
|
203
|
-
},
|
|
204
|
-
required: ['issue'],
|
|
205
|
-
},
|
|
181
|
+
inputSchema: z.object({
|
|
182
|
+
issue: z.string().describe('Issue identifier (e.g. "ENG-123") or id'),
|
|
183
|
+
title: z.string().optional().describe('New title'),
|
|
184
|
+
description: z.string().optional().describe('New description (markdown)'),
|
|
185
|
+
priority: z.number().optional().describe('0 none, 1 urgent, 2 high, 3 medium, 4 low'),
|
|
186
|
+
assignee: z.string().optional().describe('"me", a Linear user id, or "none" to unassign'),
|
|
187
|
+
stateId: z.string().optional().describe('Workflow state id (see list_teams)'),
|
|
188
|
+
}),
|
|
206
189
|
},
|
|
207
190
|
run: async (auth, args) => {
|
|
208
191
|
const { issue: issueRef, title, description, priority, assignee, stateId } = args;
|
|
@@ -223,14 +206,10 @@ export const linearTools = {
|
|
|
223
206
|
create_comment: {
|
|
224
207
|
definition: {
|
|
225
208
|
description: 'Add a comment to a Linear issue.',
|
|
226
|
-
inputSchema: {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
body: { type: 'string', description: 'Comment body (markdown)' },
|
|
231
|
-
},
|
|
232
|
-
required: ['issue', 'body'],
|
|
233
|
-
},
|
|
209
|
+
inputSchema: z.object({
|
|
210
|
+
issue: z.string().describe('Issue identifier (e.g. "ENG-123") or id'),
|
|
211
|
+
body: z.string().describe('Comment body (markdown)'),
|
|
212
|
+
}),
|
|
234
213
|
},
|
|
235
214
|
run: async (auth, args) => {
|
|
236
215
|
const { issue: issueRef, body } = args;
|
|
@@ -245,12 +224,9 @@ export const linearTools = {
|
|
|
245
224
|
list_projects: {
|
|
246
225
|
definition: {
|
|
247
226
|
description: 'List Linear projects.',
|
|
248
|
-
inputSchema: {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
limit: { type: 'number', description: 'Max projects to return (default 25)' },
|
|
252
|
-
},
|
|
253
|
-
},
|
|
227
|
+
inputSchema: z.object({
|
|
228
|
+
limit: z.number().optional().describe('Max projects to return (default 25)'),
|
|
229
|
+
}),
|
|
254
230
|
},
|
|
255
231
|
run: async (auth, args) => {
|
|
256
232
|
const { limit } = args;
|
|
@@ -276,7 +252,7 @@ export const linearTools = {
|
|
|
276
252
|
list_users: {
|
|
277
253
|
definition: {
|
|
278
254
|
description: 'List members of the Linear workspace (useful for resolving assignee ids).',
|
|
279
|
-
inputSchema:
|
|
255
|
+
inputSchema: z.object({}),
|
|
280
256
|
},
|
|
281
257
|
run: async (auth) => {
|
|
282
258
|
const data = await linearRequest(auth, `query { users(first: 100) { nodes { id name displayName email active } } }`);
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -6,6 +6,8 @@ export const VAR_ACCESS_TOKEN = "LINEAR_ACCESS_TOKEN";
|
|
|
6
6
|
export const VAR_REFRESH_TOKEN = "LINEAR_REFRESH_TOKEN";
|
|
7
7
|
export const VAR_TOKEN_EXPIRES_AT = "LINEAR_TOKEN_EXPIRES_AT";
|
|
8
8
|
export const VAR_CLIENT_ID = "LINEAR_CLIENT_ID";
|
|
9
|
+
export const GO_BACK_TO_OPENBOT_URL = "https://openbot.one/settings/agents";
|
|
10
|
+
export const LINEAR_OPENBOT_CLIENT_ID = "1263fc2dbbf08efa7b2c4b1b2b2582be";
|
|
9
11
|
|
|
10
12
|
const REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
11
13
|
|
|
@@ -47,7 +49,7 @@ export function readLinearConfig(
|
|
|
47
49
|
clientId:
|
|
48
50
|
typeof config.clientId === "string" && config.clientId.trim()
|
|
49
51
|
? config.clientId.trim()
|
|
50
|
-
:
|
|
52
|
+
: LINEAR_OPENBOT_CLIENT_ID,
|
|
51
53
|
clientSecret:
|
|
52
54
|
typeof config.clientSecret === "string" && config.clientSecret.trim()
|
|
53
55
|
? config.clientSecret.trim()
|
|
@@ -114,7 +116,7 @@ export async function saveTokens(
|
|
|
114
116
|
|
|
115
117
|
export async function clearTokens(storage: Storage): Promise<void> {
|
|
116
118
|
for (const key of [VAR_ACCESS_TOKEN, VAR_REFRESH_TOKEN, VAR_TOKEN_EXPIRES_AT]) {
|
|
117
|
-
await storage.deleteVariable({ key }).catch(() => {});
|
|
119
|
+
await storage.deleteVariable({ key }).catch(() => { });
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
clearTokens,
|
|
18
18
|
formatMissingCredentials,
|
|
19
|
+
LINEAR_OPENBOT_CLIENT_ID,
|
|
19
20
|
readLinearConfig,
|
|
20
21
|
resolveLinearCredentials,
|
|
21
22
|
resolveWebhookBaseUrl,
|
|
@@ -113,7 +114,7 @@ function* yieldOAuthAuthorizeWidget(
|
|
|
113
114
|
id: OPEN_URL_ACTION_ID,
|
|
114
115
|
label: "Connect Linear",
|
|
115
116
|
variant: "primary",
|
|
116
|
-
value: { url: authorizeUrl
|
|
117
|
+
value: { url: authorizeUrl },
|
|
117
118
|
},
|
|
118
119
|
],
|
|
119
120
|
},
|
|
@@ -273,7 +274,8 @@ const linearPluginConfigSchema = {
|
|
|
273
274
|
clientId: {
|
|
274
275
|
type: "string",
|
|
275
276
|
description:
|
|
276
|
-
"Linear OAuth application Client ID
|
|
277
|
+
"Linear OAuth application Client ID. Defaults to OpenBot's managed Linear application.",
|
|
278
|
+
default: LINEAR_OPENBOT_CLIENT_ID,
|
|
277
279
|
},
|
|
278
280
|
clientSecret: {
|
|
279
281
|
type: "string",
|
package/src/oauth.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
10
10
|
import { createServer, type Server } from "node:http";
|
|
11
11
|
import type { Storage } from "@meetopenbot/plugin-sdk";
|
|
12
|
+
import { GO_BACK_TO_OPENBOT_URL } from "./config.js";
|
|
12
13
|
import {
|
|
13
14
|
clearPendingOAuthSession,
|
|
14
15
|
loadPendingOAuthSession,
|
|
@@ -84,19 +85,27 @@ export function oauthHtmlPage(title: string, body: string, ok: boolean): string
|
|
|
84
85
|
<meta charset="utf-8" />
|
|
85
86
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
86
87
|
<title>${title}</title>
|
|
88
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
89
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
90
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
87
91
|
<style>
|
|
88
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #101012; color: #ededef; }
|
|
89
|
-
.card { text-align: center; padding: 48px
|
|
90
|
-
.icon { font-size:
|
|
91
|
-
|
|
92
|
+
body { font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #101012; color: #ededef; }
|
|
93
|
+
.card { text-align: center; padding: 40px 48px; border-radius: 14px; background: #1b1b1f; border: 1px solid #2a2a30; max-width: 420px; }
|
|
94
|
+
.icon { height: 28px; margin-bottom: 18px; font-size: 28px; font-weight: 500; line-height: 28px; }
|
|
95
|
+
.success { color: #4ade80; }
|
|
96
|
+
h1 { font-size: 20px; font-weight: 600; margin: 0 0 8px; }
|
|
92
97
|
p { color: #9b9ba3; margin: 0; line-height: 1.5; }
|
|
98
|
+
.back-link { margin: 20px 0 0; font-size: 14px; }
|
|
99
|
+
.back-link a { color: #ededef; text-decoration: none; }
|
|
100
|
+
.back-link a:hover { text-decoration: underline; }
|
|
93
101
|
</style>
|
|
94
102
|
</head>
|
|
95
103
|
<body>
|
|
96
104
|
<div class="card">
|
|
97
|
-
<div class="icon">${ok ? "
|
|
105
|
+
<div class="icon${ok ? " success" : ""}">${ok ? "✓" : "⚠"}</div>
|
|
98
106
|
<h1>${title}</h1>
|
|
99
107
|
<p>${body}</p>
|
|
108
|
+
<p class="back-link">Go back to <a href="${GO_BACK_TO_OPENBOT_URL}">OpenBot</a></p>
|
|
100
109
|
</div>
|
|
101
110
|
</body>
|
|
102
111
|
</html>`;
|