@calltelemetry/openclaw-linear 0.3.1 → 0.4.1

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.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * tier-assess.ts — LLM-based complexity assessment for Linear issues.
3
+ *
4
+ * Uses runAgent() with the agent's configured model (e.g. kimi-k2.5)
5
+ * to assess issue complexity. The agent model handles orchestration —
6
+ * it never calls coding CLIs directly.
7
+ *
8
+ * Cost: one short agent turn (~500 tokens). Latency: ~2-5s.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
13
+ import type { Tier } from "./dispatch-state.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Tier → Model mapping
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export const TIER_MODELS: Record<Tier, string> = {
20
+ junior: "anthropic/claude-haiku-4-5",
21
+ medior: "anthropic/claude-sonnet-4-6",
22
+ senior: "anthropic/claude-opus-4-6",
23
+ };
24
+
25
+ export interface TierAssessment {
26
+ tier: Tier;
27
+ model: string;
28
+ reasoning: string;
29
+ }
30
+
31
+ export interface IssueContext {
32
+ identifier: string;
33
+ title: string;
34
+ description?: string | null;
35
+ labels?: string[];
36
+ commentCount?: number;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Assessment
41
+ // ---------------------------------------------------------------------------
42
+
43
+ const ASSESS_PROMPT = `You are a complexity assessor. Assess this issue and respond ONLY with JSON.
44
+
45
+ Tiers:
46
+ - junior: typos, copy changes, config tweaks, simple CSS, env var additions
47
+ - medior: features, bugfixes, moderate refactoring, adding tests, API changes
48
+ - senior: architecture changes, database migrations, security fixes, multi-service coordination
49
+
50
+ Consider:
51
+ 1. How many files/services are likely affected?
52
+ 2. Does it touch auth, data, or external APIs? (higher risk → higher tier)
53
+ 3. Is the description clear and actionable?
54
+ 4. Are there dependencies or unknowns?
55
+
56
+ Respond ONLY with: {"tier":"junior|medior|senior","reasoning":"one sentence"}`;
57
+
58
+ /**
59
+ * Assess issue complexity using the agent's configured model.
60
+ *
61
+ * Falls back to "medior" if the agent call fails or returns invalid JSON.
62
+ */
63
+ export async function assessTier(
64
+ api: OpenClawPluginApi,
65
+ issue: IssueContext,
66
+ agentId?: string,
67
+ ): Promise<TierAssessment> {
68
+ const issueText = [
69
+ `Issue: ${issue.identifier} — ${issue.title}`,
70
+ issue.description ? `Description: ${issue.description.slice(0, 1500)}` : "",
71
+ issue.labels?.length ? `Labels: ${issue.labels.join(", ")}` : "",
72
+ issue.commentCount != null ? `Comments: ${issue.commentCount}` : "",
73
+ ].filter(Boolean).join("\n");
74
+
75
+ const message = `${ASSESS_PROMPT}\n\n${issueText}`;
76
+
77
+ try {
78
+ const { runAgent } = await import("./agent.js");
79
+ const result = await runAgent({
80
+ api,
81
+ agentId: agentId ?? resolveDefaultAgent(api),
82
+ sessionId: `tier-assess-${issue.identifier}-${Date.now()}`,
83
+ message,
84
+ timeoutMs: 30_000, // 30s — this should be fast
85
+ });
86
+
87
+ // Try to parse assessment from output regardless of success flag.
88
+ // runAgent may report success:false (non-zero exit code) even when
89
+ // the agent produced valid JSON output — e.g. agent exited with
90
+ // signal but wrote the response before terminating.
91
+ if (result.output) {
92
+ const parsed = parseAssessment(result.output);
93
+ if (parsed) {
94
+ api.logger.info(`Tier assessment for ${issue.identifier}: ${parsed.tier} — ${parsed.reasoning} (agent success=${result.success})`);
95
+ return parsed;
96
+ }
97
+ }
98
+
99
+ if (!result.success) {
100
+ api.logger.warn(`Tier assessment agent failed for ${issue.identifier}: ${result.output.slice(0, 200)}`);
101
+ } else {
102
+ api.logger.warn(`Tier assessment for ${issue.identifier}: could not parse response: ${result.output.slice(0, 200)}`);
103
+ }
104
+ } catch (err) {
105
+ api.logger.warn(`Tier assessment error for ${issue.identifier}: ${err}`);
106
+ }
107
+
108
+ // Fallback: medior is the safest default
109
+ const fallback: TierAssessment = {
110
+ tier: "medior",
111
+ model: TIER_MODELS.medior,
112
+ reasoning: "Assessment failed — defaulting to medior",
113
+ };
114
+ api.logger.info(`Tier assessment fallback for ${issue.identifier}: medior`);
115
+ return fallback;
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Helpers
120
+ // ---------------------------------------------------------------------------
121
+
122
+ function resolveDefaultAgent(api: OpenClawPluginApi): string {
123
+ // Use the plugin's configured default agent (same one that runs the pipeline)
124
+ const fromConfig = (api as any).pluginConfig?.defaultAgentId;
125
+ if (typeof fromConfig === "string" && fromConfig) return fromConfig;
126
+
127
+ // Fall back to isDefault in agent profiles
128
+ try {
129
+ const profilesPath = join(process.env.HOME ?? "/home/claw", ".openclaw", "agent-profiles.json");
130
+ const raw = readFileSync(profilesPath, "utf8");
131
+ const profiles = JSON.parse(raw).agents ?? {};
132
+ const defaultAgent = Object.entries(profiles).find(([, p]: [string, any]) => p.isDefault);
133
+ if (defaultAgent) return defaultAgent[0];
134
+ } catch { /* fall through */ }
135
+
136
+ return "zoe";
137
+ }
138
+
139
+ function parseAssessment(raw: string): TierAssessment | null {
140
+ // Extract JSON from the response (may have markdown wrapping)
141
+ const jsonMatch = raw.match(/\{[^}]+\}/);
142
+ if (!jsonMatch) return null;
143
+
144
+ try {
145
+ const parsed = JSON.parse(jsonMatch[0]);
146
+ const tier = parsed.tier as string;
147
+ if (tier !== "junior" && tier !== "medior" && tier !== "senior") return null;
148
+
149
+ return {
150
+ tier: tier as Tier,
151
+ model: TIER_MODELS[tier as Tier],
152
+ reasoning: parsed.reasoning ?? "no reasoning provided",
153
+ };
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
package/src/tools.ts CHANGED
@@ -1,84 +1,34 @@
1
- import type { AnyAgentTool, OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
2
- import { jsonResult } from "openclaw/plugin-sdk";
3
- import { LinearClient } from "./client.js";
4
- import { resolveLinearToken } from "./linear-api.js";
1
+ import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { createCodeTool } from "./code-tool.js";
3
+ import { createOrchestrationTools } from "./orchestration-tools.js";
5
4
 
6
- export function createLinearTools(api: OpenClawPluginApi, ctx: OpenClawPluginToolContext): AnyAgentTool[] {
7
- const getClient = () => {
8
- const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
9
- const resolved = resolveLinearToken(pluginConfig);
10
- if (!resolved.accessToken) {
11
- throw new Error("Linear access token not found. Run 'openclaw openclaw-linear auth' to authenticate.");
5
+ export function createLinearTools(api: OpenClawPluginApi, ctx: Record<string, unknown>): any[] {
6
+ const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
7
+
8
+ // Unified code_run tool — dispatches to configured backend (claude/codex/gemini)
9
+ const codeTools: AnyAgentTool[] = [];
10
+ try {
11
+ codeTools.push(createCodeTool(api, ctx));
12
+ } catch (err) {
13
+ api.logger.warn(`code_run tool not available: ${err}`);
14
+ }
15
+
16
+ // Orchestration tools (conditional on config — defaults to enabled)
17
+ const orchestrationTools: AnyAgentTool[] = [];
18
+ const enableOrchestration = pluginConfig?.enableOrchestration !== false;
19
+ if (enableOrchestration) {
20
+ try {
21
+ orchestrationTools.push(...createOrchestrationTools(api, ctx));
22
+ } catch (err) {
23
+ api.logger.warn(`Orchestration tools not available: ${err}`);
12
24
  }
13
- return new LinearClient(resolved.accessToken);
14
- };
15
-
25
+ }
26
+
27
+ // Linear issue management (list, create, update, close, comment, etc.)
28
+ // is handled by the `linearis` skill — no custom tools needed here.
29
+
16
30
  return [
17
- {
18
- name: "linear_list_issues",
19
- description: "List issues from a Linear workspace",
20
- parameters: {
21
- type: "object",
22
- properties: {
23
- limit: { type: "number", description: "Max issues to return", default: 10 },
24
- teamId: { type: "string", description: "Filter by team ID" }
25
- }
26
- },
27
- execute: async ({ limit, teamId }) => {
28
- const client = getClient();
29
- const data = await client.listIssues({ limit, teamId });
30
- return jsonResult({
31
- message: `Found ${data.issues.nodes.length} issues`,
32
- issues: data.issues.nodes
33
- });
34
- }
35
- },
36
- {
37
- name: "linear_create_issue",
38
- description: "Create a new issue in Linear",
39
- parameters: {
40
- type: "object",
41
- properties: {
42
- title: { type: "string", description: "Issue title" },
43
- description: { type: "string", description: "Issue description" },
44
- teamId: { type: "string", description: "Team ID" }
45
- },
46
- required: ["title", "teamId"]
47
- },
48
- execute: async ({ title, description, teamId }) => {
49
- const client = getClient();
50
- const data = await client.createIssue({ title, description, teamId });
51
- if (data.issueCreate.success) {
52
- return jsonResult({
53
- message: "Created issue successfully",
54
- issue: data.issueCreate.issue
55
- });
56
- }
57
- return jsonResult({ message: "Failed to create issue" });
58
- }
59
- },
60
- {
61
- name: "linear_add_comment",
62
- description: "Add a comment to a Linear issue",
63
- parameters: {
64
- type: "object",
65
- properties: {
66
- issueId: { type: "string", description: "Issue ID" },
67
- body: { type: "string", description: "Comment body" }
68
- },
69
- required: ["issueId", "body"]
70
- },
71
- execute: async ({ issueId, body }) => {
72
- const client = getClient();
73
- const data = await client.addComment({ issueId, body });
74
- if (data.commentCreate.success) {
75
- return jsonResult({
76
- message: "Added comment successfully",
77
- commentId: data.commentCreate.comment.id
78
- });
79
- }
80
- return jsonResult({ message: "Failed to add comment" });
81
- }
82
- }
31
+ ...codeTools,
32
+ ...orchestrationTools,
83
33
  ];
84
34
  }