@calltelemetry/openclaw-linear 0.8.5 → 0.8.7
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 +276 -1
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
- package/prompts.yaml +24 -12
- package/src/__test__/fixtures/webhook-payloads.ts +9 -2
- package/src/__test__/webhook-scenarios.test.ts +150 -0
- package/src/infra/doctor.test.ts +3 -3
- package/src/pipeline/guidance.test.ts +222 -0
- package/src/pipeline/guidance.ts +156 -0
- package/src/pipeline/pipeline.ts +23 -2
- package/src/pipeline/webhook.ts +102 -27
- package/src/tools/linear-issues-tool.test.ts +453 -0
- package/src/tools/linear-issues-tool.ts +338 -0
- package/src/tools/tools.test.ts +36 -7
- package/src/tools/tools.ts +9 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { jsonResult } from "openclaw/plugin-sdk";
|
|
3
|
+
import { LinearAgentApi, resolveLinearToken } from "../api/linear-api.js";
|
|
4
|
+
|
|
5
|
+
type Action = "read" | "create" | "update" | "comment" | "list_states" | "list_labels";
|
|
6
|
+
|
|
7
|
+
interface ToolParams {
|
|
8
|
+
action: Action;
|
|
9
|
+
issueId?: string;
|
|
10
|
+
parentIssueId?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
priority?: number;
|
|
13
|
+
estimate?: number;
|
|
14
|
+
labels?: string[];
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
body?: string;
|
|
18
|
+
teamId?: string;
|
|
19
|
+
projectId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildApi(pluginConfig?: Record<string, unknown>): LinearAgentApi {
|
|
23
|
+
const tokenInfo = resolveLinearToken(pluginConfig);
|
|
24
|
+
if (!tokenInfo.accessToken) {
|
|
25
|
+
throw new Error("No Linear access token configured. Run: openclaw openclaw-linear auth");
|
|
26
|
+
}
|
|
27
|
+
return new LinearAgentApi(tokenInfo.accessToken, {
|
|
28
|
+
refreshToken: tokenInfo.refreshToken,
|
|
29
|
+
expiresAt: tokenInfo.expiresAt,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Action handlers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
async function handleRead(api: LinearAgentApi, params: ToolParams) {
|
|
38
|
+
if (!params.issueId) {
|
|
39
|
+
return jsonResult({ error: "issueId is required for action='read'" });
|
|
40
|
+
}
|
|
41
|
+
const issue = await api.getIssueDetails(params.issueId);
|
|
42
|
+
return jsonResult({
|
|
43
|
+
identifier: issue.identifier,
|
|
44
|
+
title: issue.title,
|
|
45
|
+
description: issue.description,
|
|
46
|
+
status: issue.state.name,
|
|
47
|
+
statusType: issue.state.type,
|
|
48
|
+
assignee: issue.assignee?.name ?? null,
|
|
49
|
+
creator: issue.creator?.name ?? null,
|
|
50
|
+
estimate: issue.estimate,
|
|
51
|
+
team: { id: issue.team.id, name: issue.team.name },
|
|
52
|
+
labels: issue.labels.nodes.map((l) => l.name),
|
|
53
|
+
project: issue.project ? { id: issue.project.id, name: issue.project.name } : null,
|
|
54
|
+
parent: issue.parent ? { id: issue.parent.id, identifier: issue.parent.identifier } : null,
|
|
55
|
+
relations: issue.relations.nodes.map((r) => ({
|
|
56
|
+
type: r.type,
|
|
57
|
+
identifier: r.relatedIssue.identifier,
|
|
58
|
+
title: r.relatedIssue.title,
|
|
59
|
+
})),
|
|
60
|
+
recentComments: issue.comments.nodes.map((c) => ({
|
|
61
|
+
author: c.user?.name ?? "Unknown",
|
|
62
|
+
body: c.body.slice(0, 500),
|
|
63
|
+
createdAt: c.createdAt,
|
|
64
|
+
})),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleCreate(api: LinearAgentApi, params: ToolParams) {
|
|
69
|
+
if (!params.title) {
|
|
70
|
+
return jsonResult({ error: "title is required for action='create'" });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolve teamId: explicit param, or derive from parent issue
|
|
74
|
+
let teamId = params.teamId;
|
|
75
|
+
let projectId = params.projectId;
|
|
76
|
+
|
|
77
|
+
if (params.parentIssueId) {
|
|
78
|
+
// Fetch parent to get teamId and projectId
|
|
79
|
+
const parent = await api.getIssueDetails(params.parentIssueId);
|
|
80
|
+
teamId = teamId ?? parent.team.id;
|
|
81
|
+
projectId = projectId ?? parent.project?.id ?? undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!teamId) {
|
|
85
|
+
return jsonResult({
|
|
86
|
+
error: "teamId is required for action='create'. Provide it directly, or provide parentIssueId to inherit from parent.",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const input: Record<string, unknown> = {
|
|
91
|
+
teamId,
|
|
92
|
+
title: params.title,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (params.description) input.description = params.description;
|
|
96
|
+
if (params.parentIssueId) input.parentId = params.parentIssueId;
|
|
97
|
+
if (projectId) input.projectId = projectId;
|
|
98
|
+
if (params.priority != null) input.priority = params.priority;
|
|
99
|
+
if (params.estimate != null) input.estimate = params.estimate;
|
|
100
|
+
|
|
101
|
+
// Resolve label names → labelIds
|
|
102
|
+
if (params.labels) {
|
|
103
|
+
const teamLabels = await api.getTeamLabels(teamId);
|
|
104
|
+
const resolvedIds: string[] = [];
|
|
105
|
+
const unmatched: string[] = [];
|
|
106
|
+
for (const name of params.labels) {
|
|
107
|
+
const match = teamLabels.find((l) => l.name.toLowerCase() === name.toLowerCase());
|
|
108
|
+
if (match) {
|
|
109
|
+
resolvedIds.push(match.id);
|
|
110
|
+
} else {
|
|
111
|
+
unmatched.push(name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (unmatched.length > 0) {
|
|
115
|
+
const available = teamLabels.map((l) => l.name).join(", ");
|
|
116
|
+
return jsonResult({
|
|
117
|
+
error: `Labels not found: ${unmatched.join(", ")}. Available: ${available}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
input.labelIds = resolvedIds;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await api.createIssue(input as any);
|
|
124
|
+
return jsonResult({
|
|
125
|
+
success: true,
|
|
126
|
+
id: result.id,
|
|
127
|
+
identifier: result.identifier,
|
|
128
|
+
parentIssueId: params.parentIssueId ?? null,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function handleUpdate(api: LinearAgentApi, params: ToolParams) {
|
|
133
|
+
if (!params.issueId) {
|
|
134
|
+
return jsonResult({ error: "issueId is required for action='update'" });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const hasFields = params.status || params.priority != null || params.estimate != null || params.labels || params.title;
|
|
138
|
+
if (!hasFields) {
|
|
139
|
+
return jsonResult({ error: "At least one field (status, priority, estimate, labels, title) is required for action='update'" });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fetch issue to get teamId for name-to-ID resolution
|
|
143
|
+
const issue = await api.getIssueDetails(params.issueId);
|
|
144
|
+
const teamId = issue.team.id;
|
|
145
|
+
const updateInput: Record<string, unknown> = {};
|
|
146
|
+
const changes: string[] = [];
|
|
147
|
+
|
|
148
|
+
// Resolve status name → stateId
|
|
149
|
+
if (params.status) {
|
|
150
|
+
const states = await api.getTeamStates(teamId);
|
|
151
|
+
const match = states.find((s) => s.name.toLowerCase() === params.status!.toLowerCase());
|
|
152
|
+
if (!match) {
|
|
153
|
+
const available = states.map((s) => `${s.name} (${s.type})`).join(", ");
|
|
154
|
+
return jsonResult({
|
|
155
|
+
error: `Status "${params.status}" not found. Available states: ${available}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
updateInput.stateId = match.id;
|
|
159
|
+
changes.push(`status → ${match.name}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Resolve label names → labelIds
|
|
163
|
+
if (params.labels) {
|
|
164
|
+
const teamLabels = await api.getTeamLabels(teamId);
|
|
165
|
+
const resolvedIds: string[] = [];
|
|
166
|
+
const unmatched: string[] = [];
|
|
167
|
+
for (const name of params.labels) {
|
|
168
|
+
const match = teamLabels.find((l) => l.name.toLowerCase() === name.toLowerCase());
|
|
169
|
+
if (match) {
|
|
170
|
+
resolvedIds.push(match.id);
|
|
171
|
+
} else {
|
|
172
|
+
unmatched.push(name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (unmatched.length > 0) {
|
|
176
|
+
const available = teamLabels.map((l) => l.name).join(", ");
|
|
177
|
+
return jsonResult({
|
|
178
|
+
error: `Labels not found: ${unmatched.join(", ")}. Available: ${available}`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
updateInput.labelIds = resolvedIds;
|
|
182
|
+
changes.push(`labels → [${params.labels.join(", ")}]`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (params.priority != null) {
|
|
186
|
+
updateInput.priority = params.priority;
|
|
187
|
+
changes.push(`priority → ${params.priority}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (params.estimate != null) {
|
|
191
|
+
updateInput.estimate = params.estimate;
|
|
192
|
+
changes.push(`estimate → ${params.estimate}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (params.title) {
|
|
196
|
+
updateInput.title = params.title;
|
|
197
|
+
changes.push(`title → "${params.title}"`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const success = await api.updateIssueExtended(params.issueId, updateInput);
|
|
201
|
+
return jsonResult({
|
|
202
|
+
success,
|
|
203
|
+
issueId: issue.identifier,
|
|
204
|
+
changes,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function handleComment(api: LinearAgentApi, params: ToolParams) {
|
|
209
|
+
if (!params.issueId) {
|
|
210
|
+
return jsonResult({ error: "issueId is required for action='comment'" });
|
|
211
|
+
}
|
|
212
|
+
if (!params.body) {
|
|
213
|
+
return jsonResult({ error: "body is required for action='comment'" });
|
|
214
|
+
}
|
|
215
|
+
const commentId = await api.createComment(params.issueId, params.body);
|
|
216
|
+
return jsonResult({ success: true, commentId });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function handleListStates(api: LinearAgentApi, params: ToolParams) {
|
|
220
|
+
if (!params.teamId) {
|
|
221
|
+
return jsonResult({ error: "teamId is required for action='list_states'" });
|
|
222
|
+
}
|
|
223
|
+
const states = await api.getTeamStates(params.teamId);
|
|
224
|
+
return jsonResult({ states });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function handleListLabels(api: LinearAgentApi, params: ToolParams) {
|
|
228
|
+
if (!params.teamId) {
|
|
229
|
+
return jsonResult({ error: "teamId is required for action='list_labels'" });
|
|
230
|
+
}
|
|
231
|
+
const labels = await api.getTeamLabels(params.teamId);
|
|
232
|
+
return jsonResult({ labels });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Tool factory
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
export function createLinearIssuesTool(
|
|
240
|
+
api: OpenClawPluginApi,
|
|
241
|
+
): AnyAgentTool {
|
|
242
|
+
const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
name: "linear_issues",
|
|
246
|
+
label: "Linear Issues",
|
|
247
|
+
description:
|
|
248
|
+
"Read, create, update, and manage Linear issues directly via API. " +
|
|
249
|
+
"Actions: read (get issue details), create (create issue or sub-issue), " +
|
|
250
|
+
"update (change status/priority/labels/estimate/title), " +
|
|
251
|
+
"comment (post a comment), list_states (get workflow states for a team), " +
|
|
252
|
+
"list_labels (get labels for a team). " +
|
|
253
|
+
"Use action='create' with parentIssueId to create sub-issues for granular work breakdown. " +
|
|
254
|
+
"Status and label names are resolved to IDs automatically.",
|
|
255
|
+
parameters: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
action: {
|
|
259
|
+
type: "string",
|
|
260
|
+
enum: ["read", "create", "update", "comment", "list_states", "list_labels"],
|
|
261
|
+
description: "The action to perform.",
|
|
262
|
+
},
|
|
263
|
+
issueId: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: "Issue identifier (e.g. 'ENG-123') or UUID. Required for read, update, comment.",
|
|
266
|
+
},
|
|
267
|
+
parentIssueId: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: "Parent issue identifier or UUID. Used with action=create to make a sub-issue. The new issue inherits teamId and projectId from the parent.",
|
|
270
|
+
},
|
|
271
|
+
description: {
|
|
272
|
+
type: "string",
|
|
273
|
+
description: "Issue description with acceptance criteria. Used with action=create.",
|
|
274
|
+
},
|
|
275
|
+
projectId: {
|
|
276
|
+
type: "string",
|
|
277
|
+
description: "Project UUID. Used with action=create to add the issue to a project.",
|
|
278
|
+
},
|
|
279
|
+
status: {
|
|
280
|
+
type: "string",
|
|
281
|
+
description: "New status name (e.g. 'In Progress', 'Done', 'Backlog'). Used with action=update.",
|
|
282
|
+
},
|
|
283
|
+
priority: {
|
|
284
|
+
type: "number",
|
|
285
|
+
description: "Priority level 0-4 (0=none, 1=urgent, 2=high, 3=medium, 4=low). Used with action=create and action=update.",
|
|
286
|
+
},
|
|
287
|
+
estimate: {
|
|
288
|
+
type: "number",
|
|
289
|
+
description: "Story point estimate. Used with action=create and action=update.",
|
|
290
|
+
},
|
|
291
|
+
labels: {
|
|
292
|
+
type: "array",
|
|
293
|
+
items: { type: "string" },
|
|
294
|
+
description: "Label names to set on the issue. Used with action=create and action=update.",
|
|
295
|
+
},
|
|
296
|
+
title: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "Issue title. Required for action=create. Used with action=update to rename.",
|
|
299
|
+
},
|
|
300
|
+
body: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "Comment body text. Required for action=comment.",
|
|
303
|
+
},
|
|
304
|
+
teamId: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "Team ID. Required for list_states, list_labels, and action=create (unless parentIssueId is provided). Get it from a read action first.",
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
required: ["action"],
|
|
310
|
+
},
|
|
311
|
+
execute: async (_toolCallId: string, params: ToolParams) => {
|
|
312
|
+
try {
|
|
313
|
+
const linearApi = buildApi(pluginConfig);
|
|
314
|
+
|
|
315
|
+
switch (params.action) {
|
|
316
|
+
case "read":
|
|
317
|
+
return await handleRead(linearApi, params);
|
|
318
|
+
case "create":
|
|
319
|
+
return await handleCreate(linearApi, params);
|
|
320
|
+
case "update":
|
|
321
|
+
return await handleUpdate(linearApi, params);
|
|
322
|
+
case "comment":
|
|
323
|
+
return await handleComment(linearApi, params);
|
|
324
|
+
case "list_states":
|
|
325
|
+
return await handleListStates(linearApi, params);
|
|
326
|
+
case "list_labels":
|
|
327
|
+
return await handleListLabels(linearApi, params);
|
|
328
|
+
default:
|
|
329
|
+
return jsonResult({ error: `Unknown action: ${params.action}. Valid: read, create, update, comment, list_states, list_labels` });
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
332
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
333
|
+
api.logger.error(`linear_issues tool error: ${message}`);
|
|
334
|
+
return jsonResult({ error: message });
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
} as unknown as AnyAgentTool;
|
|
338
|
+
}
|
package/src/tools/tools.test.ts
CHANGED
|
@@ -17,9 +17,14 @@ vi.mock("./orchestration-tools.js", () => ({
|
|
|
17
17
|
]),
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
+
vi.mock("./linear-issues-tool.js", () => ({
|
|
21
|
+
createLinearIssuesTool: vi.fn(() => ({ name: "linear_issues", execute: vi.fn() })),
|
|
22
|
+
}));
|
|
23
|
+
|
|
20
24
|
import { createLinearTools } from "./tools.js";
|
|
21
25
|
import { createCodeTool } from "./code-tool.js";
|
|
22
26
|
import { createOrchestrationTools } from "./orchestration-tools.js";
|
|
27
|
+
import { createLinearIssuesTool } from "./linear-issues-tool.js";
|
|
23
28
|
|
|
24
29
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
25
30
|
|
|
@@ -38,15 +43,16 @@ function makeApi(pluginConfig?: Record<string, unknown>) {
|
|
|
38
43
|
// ── Tests ──────────────────────────────────────────────────────────
|
|
39
44
|
|
|
40
45
|
describe("createLinearTools", () => {
|
|
41
|
-
it("returns code_run, spawn_agent, and
|
|
46
|
+
it("returns code_run, spawn_agent, ask_agent, and linear_issues tools", () => {
|
|
42
47
|
const api = makeApi();
|
|
43
48
|
const tools = createLinearTools(api, {});
|
|
44
49
|
|
|
45
|
-
expect(tools).toHaveLength(
|
|
50
|
+
expect(tools).toHaveLength(4);
|
|
46
51
|
const names = tools.map((t: any) => t.name);
|
|
47
52
|
expect(names).toContain("code_run");
|
|
48
53
|
expect(names).toContain("spawn_agent");
|
|
49
54
|
expect(names).toContain("ask_agent");
|
|
55
|
+
expect(names).toContain("linear_issues");
|
|
50
56
|
});
|
|
51
57
|
|
|
52
58
|
it("includes orchestration tools by default", () => {
|
|
@@ -61,8 +67,10 @@ describe("createLinearTools", () => {
|
|
|
61
67
|
const api = makeApi({ enableOrchestration: false });
|
|
62
68
|
const tools = createLinearTools(api, {});
|
|
63
69
|
|
|
64
|
-
expect(tools).toHaveLength(
|
|
65
|
-
|
|
70
|
+
expect(tools).toHaveLength(2);
|
|
71
|
+
const names = tools.map((t: any) => t.name);
|
|
72
|
+
expect(names).toContain("code_run");
|
|
73
|
+
expect(names).toContain("linear_issues");
|
|
66
74
|
expect(createOrchestrationTools).not.toHaveBeenCalled();
|
|
67
75
|
});
|
|
68
76
|
|
|
@@ -74,10 +82,11 @@ describe("createLinearTools", () => {
|
|
|
74
82
|
const api = makeApi();
|
|
75
83
|
const tools = createLinearTools(api, {});
|
|
76
84
|
|
|
77
|
-
expect(tools).toHaveLength(
|
|
85
|
+
expect(tools).toHaveLength(3);
|
|
78
86
|
const names = tools.map((t: any) => t.name);
|
|
79
87
|
expect(names).toContain("spawn_agent");
|
|
80
88
|
expect(names).toContain("ask_agent");
|
|
89
|
+
expect(names).toContain("linear_issues");
|
|
81
90
|
expect(api.logger.warn).toHaveBeenCalledWith(
|
|
82
91
|
expect.stringContaining("code_run tool not available"),
|
|
83
92
|
);
|
|
@@ -91,10 +100,30 @@ describe("createLinearTools", () => {
|
|
|
91
100
|
const api = makeApi();
|
|
92
101
|
const tools = createLinearTools(api, {});
|
|
93
102
|
|
|
94
|
-
expect(tools).toHaveLength(
|
|
95
|
-
|
|
103
|
+
expect(tools).toHaveLength(2);
|
|
104
|
+
const names = tools.map((t: any) => t.name);
|
|
105
|
+
expect(names).toContain("code_run");
|
|
106
|
+
expect(names).toContain("linear_issues");
|
|
96
107
|
expect(api.logger.warn).toHaveBeenCalledWith(
|
|
97
108
|
expect.stringContaining("Orchestration tools not available"),
|
|
98
109
|
);
|
|
99
110
|
});
|
|
111
|
+
|
|
112
|
+
it("handles linear_issues creation failure gracefully", () => {
|
|
113
|
+
vi.mocked(createLinearIssuesTool).mockImplementationOnce(() => {
|
|
114
|
+
throw new Error("no token");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const api = makeApi();
|
|
118
|
+
const tools = createLinearTools(api, {});
|
|
119
|
+
|
|
120
|
+
expect(tools).toHaveLength(3);
|
|
121
|
+
const names = tools.map((t: any) => t.name);
|
|
122
|
+
expect(names).toContain("code_run");
|
|
123
|
+
expect(names).toContain("spawn_agent");
|
|
124
|
+
expect(names).toContain("ask_agent");
|
|
125
|
+
expect(api.logger.warn).toHaveBeenCalledWith(
|
|
126
|
+
expect.stringContaining("linear_issues tool not available"),
|
|
127
|
+
);
|
|
128
|
+
});
|
|
100
129
|
});
|
package/src/tools/tools.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { createCodeTool } from "./code-tool.js";
|
|
3
3
|
import { createOrchestrationTools } from "./orchestration-tools.js";
|
|
4
|
+
import { createLinearIssuesTool } from "./linear-issues-tool.js";
|
|
4
5
|
|
|
5
6
|
export function createLinearTools(api: OpenClawPluginApi, ctx: Record<string, unknown>): any[] {
|
|
6
7
|
const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
|
|
@@ -24,11 +25,17 @@ export function createLinearTools(api: OpenClawPluginApi, ctx: Record<string, un
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// Linear issue management
|
|
28
|
-
|
|
28
|
+
// Linear issue management — native GraphQL API tool
|
|
29
|
+
const linearIssuesTools: AnyAgentTool[] = [];
|
|
30
|
+
try {
|
|
31
|
+
linearIssuesTools.push(createLinearIssuesTool(api));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
api.logger.warn(`linear_issues tool not available: ${err}`);
|
|
34
|
+
}
|
|
29
35
|
|
|
30
36
|
return [
|
|
31
37
|
...codeTools,
|
|
32
38
|
...orchestrationTools,
|
|
39
|
+
...linearIssuesTools,
|
|
33
40
|
];
|
|
34
41
|
}
|