@meetopenbot/linear 0.0.3 → 0.0.4
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 +17 -60
- package/dist/cloud-mode.js +10 -0
- package/dist/config.js +26 -109
- package/dist/credits-auth.js +53 -0
- package/dist/index.js +60 -262
- package/dist/linear-agent.js +142 -61
- package/dist/linear-mcp.js +9 -21
- package/dist/mcp-errors.js +40 -0
- package/dist/mcp-tool-args.js +38 -0
- package/dist/model-registry.js +40 -0
- package/dist/model.js +24 -0
- package/dist/thread-context.js +72 -0
- package/package.json +12 -23
- package/dist/api.d.ts +0 -94
- package/dist/api.js +0 -93
- package/dist/auth.d.ts +0 -33
- package/dist/auth.js +0 -91
- package/dist/config.d.ts +0 -41
- package/dist/index.d.ts +0 -54
- package/dist/linear-agent.d.ts +0 -14
- package/dist/linear-issues.d.ts +0 -33
- package/dist/linear-issues.js +0 -136
- package/dist/linear-mcp.d.ts +0 -4
- package/dist/oauth-pending.d.ts +0 -13
- package/dist/oauth-pending.js +0 -40
- package/dist/oauth.d.ts +0 -95
- package/dist/oauth.js +0 -381
- package/dist/tools.d.ts +0 -17
- package/dist/tools.js +0 -266
- package/src/config.ts +0 -230
- package/src/index.ts +0 -451
- package/src/linear-agent.ts +0 -107
- package/src/linear-issues.ts +0 -188
- package/src/linear-mcp.ts +0 -33
- package/src/oauth-pending.ts +0 -68
- package/src/oauth.ts +0 -572
package/src/linear-agent.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
|
-
import { generateText, stepCountIs } from "ai";
|
|
3
|
-
import type { LinearIssue } from "./linear-issues.js";
|
|
4
|
-
import { extractIssuesFromToolResults } from "./linear-issues.js";
|
|
5
|
-
import { createLinearMcpClient } from "./linear-mcp.js";
|
|
6
|
-
|
|
7
|
-
export type RunLinearAgentArgs = {
|
|
8
|
-
prompt: string;
|
|
9
|
-
openaiApiKey: string;
|
|
10
|
-
accessToken: string;
|
|
11
|
-
model?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type LinearAgentResult = {
|
|
15
|
-
text: string;
|
|
16
|
-
issues: LinearIssue[];
|
|
17
|
-
usedTools: boolean;
|
|
18
|
-
toolErrors: string[];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const SYSTEM_PROMPT = `You are the OpenBot Linear specialist agent.
|
|
22
|
-
You MUST use the provided Linear MCP tools for every request about issues, projects, teams, or comments.
|
|
23
|
-
Never guess or invent Linear data. If a tool returns no results, say so explicitly.
|
|
24
|
-
For listing or searching issues, call linear_get_user first when the user asks for issues assigned to them, then call linear_search_issues with the correct filters.
|
|
25
|
-
Summarize tool results clearly in plain text. Be concise and friendly.`;
|
|
26
|
-
|
|
27
|
-
function extractToolErrors(
|
|
28
|
-
toolResults: Array<{ toolName: string; output: unknown }>,
|
|
29
|
-
): string[] {
|
|
30
|
-
const errors: string[] = [];
|
|
31
|
-
|
|
32
|
-
for (const toolResult of toolResults) {
|
|
33
|
-
const output = toolResult.output;
|
|
34
|
-
if (!output || typeof output !== "object") continue;
|
|
35
|
-
|
|
36
|
-
const record = output as Record<string, unknown>;
|
|
37
|
-
if (typeof record.error === "string") {
|
|
38
|
-
errors.push(`${toolResult.toolName}: ${record.error}`);
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (typeof record.isError === "boolean" && record.isError) {
|
|
43
|
-
const text =
|
|
44
|
-
typeof record.text === "string"
|
|
45
|
-
? record.text
|
|
46
|
-
: JSON.stringify(record).slice(0, 300);
|
|
47
|
-
errors.push(`${toolResult.toolName}: ${text}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return errors;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function runLinearAgent(
|
|
55
|
-
args: RunLinearAgentArgs,
|
|
56
|
-
): Promise<LinearAgentResult> {
|
|
57
|
-
const openai = createOpenAI({ apiKey: args.openaiApiKey });
|
|
58
|
-
const mcpClient = await createLinearMcpClient({
|
|
59
|
-
accessToken: args.accessToken,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const tools = await mcpClient.tools();
|
|
64
|
-
if (Object.keys(tools).length === 0) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
"Linear MCP server returned no tools. Check that mcp-server-linear is installed and reachable.",
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const result = await generateText({
|
|
71
|
-
model: openai(args.model ?? "gpt-4o-mini"),
|
|
72
|
-
system: SYSTEM_PROMPT,
|
|
73
|
-
prompt: args.prompt,
|
|
74
|
-
tools,
|
|
75
|
-
stopWhen: stepCountIs(10),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const toolResults = result.steps.flatMap((step) =>
|
|
79
|
-
step.toolResults.map((toolResult) => ({
|
|
80
|
-
toolName: toolResult.toolName,
|
|
81
|
-
output: toolResult.output,
|
|
82
|
-
})),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const issues = extractIssuesFromToolResults(toolResults);
|
|
86
|
-
const toolErrors = extractToolErrors(toolResults);
|
|
87
|
-
const text = result.text.trim();
|
|
88
|
-
|
|
89
|
-
if (!text && toolErrors.length > 0) {
|
|
90
|
-
return {
|
|
91
|
-
text: `Linear tool error: ${toolErrors[0]}`,
|
|
92
|
-
issues,
|
|
93
|
-
usedTools: toolResults.length > 0,
|
|
94
|
-
toolErrors,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
text: text || (issues.length > 0 ? `Found ${issues.length} issue(s).` : "Done."),
|
|
100
|
-
issues,
|
|
101
|
-
usedTools: toolResults.length > 0,
|
|
102
|
-
toolErrors,
|
|
103
|
-
};
|
|
104
|
-
} finally {
|
|
105
|
-
await mcpClient.close();
|
|
106
|
-
}
|
|
107
|
-
}
|
package/src/linear-issues.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import type { RenderUIWidgetData } from "@meetopenbot/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
export type LinearIssue = {
|
|
4
|
-
id: string;
|
|
5
|
-
identifier: string;
|
|
6
|
-
title: string;
|
|
7
|
-
url: string;
|
|
8
|
-
state?: { name: string; type: string };
|
|
9
|
-
assignee?: { name: string };
|
|
10
|
-
team?: { name: string; key: string };
|
|
11
|
-
project?: { name: string };
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const OPEN_URL_ACTION_ID = "open_url";
|
|
15
|
-
export const ISSUES_LIST_WIDGET_ID = "linear-issues-list";
|
|
16
|
-
|
|
17
|
-
type LinearStateType =
|
|
18
|
-
| "backlog"
|
|
19
|
-
| "unstarted"
|
|
20
|
-
| "started"
|
|
21
|
-
| "completed"
|
|
22
|
-
| "canceled"
|
|
23
|
-
| string;
|
|
24
|
-
|
|
25
|
-
function mapStateStatus(
|
|
26
|
-
stateType?: LinearStateType,
|
|
27
|
-
): "pending" | "in_progress" | "done" | "cancelled" | undefined {
|
|
28
|
-
switch (stateType) {
|
|
29
|
-
case "started":
|
|
30
|
-
return "in_progress";
|
|
31
|
-
case "completed":
|
|
32
|
-
return "done";
|
|
33
|
-
case "canceled":
|
|
34
|
-
return "cancelled";
|
|
35
|
-
case "backlog":
|
|
36
|
-
case "unstarted":
|
|
37
|
-
return "pending";
|
|
38
|
-
default:
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function buildIssuesListWidget(
|
|
44
|
-
issues: LinearIssue[],
|
|
45
|
-
options?: { title?: string; description?: string },
|
|
46
|
-
): RenderUIWidgetData {
|
|
47
|
-
return {
|
|
48
|
-
kind: "list",
|
|
49
|
-
widgetId: ISSUES_LIST_WIDGET_ID,
|
|
50
|
-
title: options?.title ?? "Linear issues",
|
|
51
|
-
description:
|
|
52
|
-
options?.description ??
|
|
53
|
-
(issues.length === 0
|
|
54
|
-
? "No issues matched this query."
|
|
55
|
-
: `${issues.length} issue${issues.length === 1 ? "" : "s"}`),
|
|
56
|
-
items: issues.map((issue) => {
|
|
57
|
-
const details = [
|
|
58
|
-
issue.state?.name,
|
|
59
|
-
issue.assignee?.name ? `Assignee: ${issue.assignee.name}` : undefined,
|
|
60
|
-
issue.team?.name,
|
|
61
|
-
issue.project?.name,
|
|
62
|
-
]
|
|
63
|
-
.filter(Boolean)
|
|
64
|
-
.join(" · ");
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
id: issue.id,
|
|
68
|
-
label: `${issue.identifier}: ${issue.title}`,
|
|
69
|
-
description: details || undefined,
|
|
70
|
-
badge: issue.state?.name,
|
|
71
|
-
status: mapStateStatus(issue.state?.type),
|
|
72
|
-
actions: issue.url
|
|
73
|
-
? [
|
|
74
|
-
{
|
|
75
|
-
id: OPEN_URL_ACTION_ID,
|
|
76
|
-
label: "Open",
|
|
77
|
-
variant: "secondary" as const,
|
|
78
|
-
value: { url: issue.url, target: "_blank" },
|
|
79
|
-
},
|
|
80
|
-
]
|
|
81
|
-
: undefined,
|
|
82
|
-
metadata: {
|
|
83
|
-
identifier: issue.identifier,
|
|
84
|
-
url: issue.url,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function extractTextPayload(value: unknown): string | undefined {
|
|
92
|
-
if (typeof value === "string") return value;
|
|
93
|
-
if (!value || typeof value !== "object") return undefined;
|
|
94
|
-
|
|
95
|
-
const record = value as Record<string, unknown>;
|
|
96
|
-
if (typeof record.text === "string") return record.text;
|
|
97
|
-
|
|
98
|
-
if (Array.isArray(record.content)) {
|
|
99
|
-
for (const part of record.content) {
|
|
100
|
-
if (
|
|
101
|
-
part &&
|
|
102
|
-
typeof part === "object" &&
|
|
103
|
-
(part as { type?: string }).type === "text" &&
|
|
104
|
-
typeof (part as { text?: string }).text === "string"
|
|
105
|
-
) {
|
|
106
|
-
return (part as { text: string }).text;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return undefined;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function parseIssuesPayload(payload: unknown): LinearIssue[] {
|
|
115
|
-
if (!payload || typeof payload !== "object") return [];
|
|
116
|
-
|
|
117
|
-
const root = payload as Record<string, unknown>;
|
|
118
|
-
const issuesNode =
|
|
119
|
-
(root.issues as { nodes?: unknown[] } | undefined) ??
|
|
120
|
-
((root.data as Record<string, unknown> | undefined)?.issues as
|
|
121
|
-
| { nodes?: unknown[] }
|
|
122
|
-
| undefined);
|
|
123
|
-
|
|
124
|
-
if (!issuesNode?.nodes || !Array.isArray(issuesNode.nodes)) return [];
|
|
125
|
-
|
|
126
|
-
return issuesNode.nodes
|
|
127
|
-
.filter((node): node is LinearIssue => {
|
|
128
|
-
if (!node || typeof node !== "object") return false;
|
|
129
|
-
const issue = node as LinearIssue;
|
|
130
|
-
return (
|
|
131
|
-
typeof issue.id === "string" &&
|
|
132
|
-
typeof issue.identifier === "string" &&
|
|
133
|
-
typeof issue.title === "string"
|
|
134
|
-
);
|
|
135
|
-
})
|
|
136
|
-
.map((issue) => ({
|
|
137
|
-
...issue,
|
|
138
|
-
url: typeof issue.url === "string" ? issue.url : "",
|
|
139
|
-
}));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function extractIssuesFromToolResults(
|
|
143
|
-
toolResults: Array<{ toolName: string; output: unknown }>,
|
|
144
|
-
): LinearIssue[] {
|
|
145
|
-
const byId = new Map<string, LinearIssue>();
|
|
146
|
-
|
|
147
|
-
for (const toolResult of toolResults) {
|
|
148
|
-
if (!toolResult.toolName.includes("search_issues")) continue;
|
|
149
|
-
|
|
150
|
-
const text = extractTextPayload(toolResult.output);
|
|
151
|
-
if (!text) continue;
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
for (const issue of parseIssuesPayload(JSON.parse(text))) {
|
|
155
|
-
byId.set(issue.id, issue);
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
// Ignore malformed tool payloads.
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return [...byId.values()];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function isListIssuesPrompt(message: string): boolean {
|
|
166
|
-
const normalized = message.trim().toLowerCase();
|
|
167
|
-
if (!normalized) return false;
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
/\b(list|show|get|fetch|what are|available)\b/.test(normalized) &&
|
|
171
|
-
/\bissues?\b/.test(normalized)
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function isAssignedIssuesPrompt(message: string): boolean {
|
|
176
|
-
const normalized = message.trim().toLowerCase();
|
|
177
|
-
return (
|
|
178
|
-
/\b(assigned to me|my issues|issues for me|issues assigned)\b/.test(
|
|
179
|
-
normalized,
|
|
180
|
-
) || /\bissues?\s+assigned\b/.test(normalized)
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function issuesListTitle(prompt: string): string {
|
|
185
|
-
return isAssignedIssuesPrompt(prompt)
|
|
186
|
-
? "Issues assigned to you"
|
|
187
|
-
: "Linear issues";
|
|
188
|
-
}
|
package/src/linear-mcp.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { createMCPClient } from "@ai-sdk/mcp";
|
|
3
|
-
import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
|
|
4
|
-
|
|
5
|
-
export type LinearMcpClientArgs = {
|
|
6
|
-
accessToken: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
|
-
|
|
11
|
-
function resolveMcpServerLinearLaunch(): { command: string; args: string[] } {
|
|
12
|
-
try {
|
|
13
|
-
const entry = require.resolve("mcp-server-linear");
|
|
14
|
-
return { command: process.execPath, args: [entry] };
|
|
15
|
-
} catch {
|
|
16
|
-
return { command: "npx", args: ["-y", "mcp-server-linear"] };
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function createLinearMcpClient(args: LinearMcpClientArgs) {
|
|
21
|
-
const launch = resolveMcpServerLinearLaunch();
|
|
22
|
-
|
|
23
|
-
return createMCPClient({
|
|
24
|
-
transport: new Experimental_StdioMCPTransport({
|
|
25
|
-
command: launch.command,
|
|
26
|
-
args: launch.args,
|
|
27
|
-
env: {
|
|
28
|
-
...process.env,
|
|
29
|
-
LINEAR_ACCESS_TOKEN: args.accessToken,
|
|
30
|
-
},
|
|
31
|
-
}),
|
|
32
|
-
});
|
|
33
|
-
}
|
package/src/oauth-pending.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Storage } from "@meetopenbot/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
export const VAR_OAUTH_PENDING = "LINEAR_OAUTH_PENDING";
|
|
4
|
-
|
|
5
|
-
export interface PendingOAuthSession {
|
|
6
|
-
state: string;
|
|
7
|
-
codeVerifier: string;
|
|
8
|
-
clientId: string;
|
|
9
|
-
clientSecret?: string;
|
|
10
|
-
redirectUri: string;
|
|
11
|
-
expiresAt: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type VariableValue = string | { value: string; secret: boolean } | undefined;
|
|
15
|
-
|
|
16
|
-
function variableValue(
|
|
17
|
-
variables: Record<string, VariableValue>,
|
|
18
|
-
key: string,
|
|
19
|
-
): string | undefined {
|
|
20
|
-
const entry = variables[key];
|
|
21
|
-
if (typeof entry === "string") return entry || undefined;
|
|
22
|
-
return entry?.value || undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function savePendingOAuthSession(
|
|
26
|
-
storage: Storage,
|
|
27
|
-
session: PendingOAuthSession,
|
|
28
|
-
): Promise<void> {
|
|
29
|
-
await storage.createVariable({
|
|
30
|
-
key: VAR_OAUTH_PENDING,
|
|
31
|
-
value: JSON.stringify(session),
|
|
32
|
-
secret: true,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function loadPendingOAuthSession(
|
|
37
|
-
storage: Storage,
|
|
38
|
-
): Promise<PendingOAuthSession | null> {
|
|
39
|
-
const variables = (await storage.getVariables().catch(() => ({}))) as Record<
|
|
40
|
-
string,
|
|
41
|
-
VariableValue
|
|
42
|
-
>;
|
|
43
|
-
const raw =
|
|
44
|
-
variableValue(variables, VAR_OAUTH_PENDING) ??
|
|
45
|
-
process.env[VAR_OAUTH_PENDING];
|
|
46
|
-
if (!raw) return null;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const session = JSON.parse(raw) as PendingOAuthSession;
|
|
50
|
-
if (
|
|
51
|
-
typeof session.state !== "string" ||
|
|
52
|
-
typeof session.codeVerifier !== "string" ||
|
|
53
|
-
typeof session.clientId !== "string" ||
|
|
54
|
-
typeof session.redirectUri !== "string" ||
|
|
55
|
-
typeof session.expiresAt !== "number"
|
|
56
|
-
) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
if (Date.now() > session.expiresAt) return null;
|
|
60
|
-
return session;
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function clearPendingOAuthSession(storage: Storage): Promise<void> {
|
|
67
|
-
await storage.deleteVariable({ key: VAR_OAUTH_PENDING }).catch(() => {});
|
|
68
|
-
}
|