@calltelemetry/openclaw-linear 0.9.21 → 0.9.22

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/index.ts CHANGED
@@ -102,6 +102,8 @@ export default function register(api: OpenClawPluginApi) {
102
102
  // Register Linear webhook handler on a dedicated route
103
103
  api.registerHttpRoute({
104
104
  path: "/linear/webhook",
105
+ auth: "plugin",
106
+ match: "exact",
105
107
  handler: async (req, res) => {
106
108
  await handleLinearWebhook(api, req, res);
107
109
  },
@@ -110,6 +112,8 @@ export default function register(api: OpenClawPluginApi) {
110
112
  // Back-compat route so existing production webhook URLs keep working.
111
113
  api.registerHttpRoute({
112
114
  path: "/hooks/linear",
115
+ auth: "plugin",
116
+ match: "exact",
113
117
  handler: async (req, res) => {
114
118
  await handleLinearWebhook(api, req, res);
115
119
  },
@@ -118,6 +122,8 @@ export default function register(api: OpenClawPluginApi) {
118
122
  // Register OAuth callback route
119
123
  api.registerHttpRoute({
120
124
  path: "/linear/oauth/callback",
125
+ auth: "plugin",
126
+ match: "exact",
121
127
  handler: async (req, res) => {
122
128
  await handleOAuthCallback(api, req, res);
123
129
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.21",
3
+ "version": "0.9.22",
4
4
  "description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@ export interface RunInTmuxOptions {
33
33
  timeoutMs: number;
34
34
  watchdogMs: number;
35
35
  logPath: string;
36
- mapEvent: (event: any) => ActivityContent | null;
36
+ mapEvent: (event: any) => ActivityContent[];
37
37
  linearApi?: LinearAgentApi;
38
38
  agentSessionId?: string;
39
39
  steeringMode: "stdin-pipe" | "one-shot";
@@ -247,8 +247,8 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
247
247
  }
248
248
 
249
249
  // Stream to Linear
250
- const activity = mapEvent(event);
251
- if (activity) {
250
+ const activities = mapEvent(event);
251
+ for (const activity of activities) {
252
252
  if (linearApi && agentSessionId) {
253
253
  linearApi.emitActivity(agentSessionId, activity).catch((err) => {
254
254
  logger.warn(`Failed to emit tmux activity: ${err}`);
@@ -26,17 +26,18 @@ const CLAUDE_BIN = "claude";
26
26
  * Claude event types:
27
27
  * system(init) → assistant (text|tool_use) → user (tool_result) → result
28
28
  */
29
- function mapClaudeEventToActivity(event: any): ActivityContent | null {
29
+ function mapClaudeEventToActivity(event: any): ActivityContent[] {
30
30
  const type = event?.type;
31
31
 
32
- // Assistant message — text response or tool use
32
+ // Assistant message — text response and/or tool use (emit all blocks)
33
33
  if (type === "assistant") {
34
34
  const content = event.message?.content;
35
- if (!Array.isArray(content)) return null;
35
+ if (!Array.isArray(content)) return [];
36
36
 
37
+ const activities: ActivityContent[] = [];
37
38
  for (const block of content) {
38
39
  if (block.type === "text" && block.text) {
39
- return { type: "thought", body: block.text.slice(0, 1000) };
40
+ activities.push({ type: "thought", body: block.text.slice(0, 1000) });
40
41
  }
41
42
  if (block.type === "tool_use") {
42
43
  const toolName = block.name ?? "tool";
@@ -54,30 +55,31 @@ function mapClaudeEventToActivity(event: any): ActivityContent | null {
54
55
  } else {
55
56
  paramSummary = JSON.stringify(input).slice(0, 500);
56
57
  }
57
- return { type: "action", action: `Running ${toolName}`, parameter: paramSummary };
58
+ activities.push({ type: "action", action: `Running ${toolName}`, parameter: paramSummary });
58
59
  }
59
60
  }
60
- return null;
61
+ return activities;
61
62
  }
62
63
 
63
64
  // Tool result
64
65
  if (type === "user") {
65
66
  const content = event.message?.content;
66
- if (!Array.isArray(content)) return null;
67
+ if (!Array.isArray(content)) return [];
67
68
 
69
+ const activities: ActivityContent[] = [];
68
70
  for (const block of content) {
69
71
  if (block.type === "tool_result") {
70
72
  const output = typeof block.content === "string" ? block.content : "";
71
73
  const truncated = output.length > 1000 ? output.slice(0, 1000) + "..." : output;
72
74
  const isError = block.is_error === true;
73
- return {
75
+ activities.push({
74
76
  type: "action",
75
77
  action: isError ? "Tool error" : "Tool result",
76
78
  parameter: truncated || "(no output)",
77
- };
79
+ });
78
80
  }
79
81
  }
80
- return null;
82
+ return activities;
81
83
  }
82
84
 
83
85
  // Final result
@@ -92,10 +94,10 @@ function mapClaudeEventToActivity(event: any): ActivityContent | null {
92
94
  const output = usage.output_tokens ?? 0;
93
95
  parts.push(`${input} in / ${output} out tokens`);
94
96
  }
95
- return { type: "thought", body: parts.join(" — ") };
97
+ return [{ type: "thought", body: parts.join(" — ") }];
96
98
  }
97
99
 
98
- return null;
100
+ return [];
99
101
  }
100
102
 
101
103
  /**
@@ -299,9 +301,9 @@ export async function runClaude(
299
301
  // (it duplicates the last assistant text message)
300
302
  }
301
303
 
302
- // Stream activity to Linear + session progress
303
- const activity = mapClaudeEventToActivity(event);
304
- if (activity) {
304
+ // Stream activities to Linear + session progress
305
+ const activities = mapClaudeEventToActivity(event);
306
+ for (const activity of activities) {
305
307
  if (linearApi && agentSessionId) {
306
308
  linearApi.emitActivity(agentSessionId, activity).catch((err) => {
307
309
  api.logger.warn(`Failed to emit Claude activity: ${err}`);
@@ -23,13 +23,13 @@ const CODEX_BIN = "codex";
23
23
  /**
24
24
  * Parse a JSONL line from `codex exec --json` and map it to a Linear activity.
25
25
  */
26
- function mapCodexEventToActivity(event: any): ActivityContent | null {
26
+ function mapCodexEventToActivity(event: any): ActivityContent[] {
27
27
  const eventType = event?.type;
28
28
  const item = event?.item;
29
29
 
30
30
  if (item?.type === "reasoning") {
31
31
  const text = item.text ?? "";
32
- return { type: "thought", body: text ? text.slice(0, 500) : "Reasoning..." };
32
+ return [{ type: "thought", body: text ? text.slice(0, 500) : "Reasoning..." }];
33
33
  }
34
34
 
35
35
  if (
@@ -37,8 +37,8 @@ function mapCodexEventToActivity(event: any): ActivityContent | null {
37
37
  (item?.type === "agent_message" || item?.type === "message")
38
38
  ) {
39
39
  const text = item.text ?? item.content ?? "";
40
- if (text) return { type: "thought", body: text.slice(0, 1000) };
41
- return null;
40
+ if (text) return [{ type: "thought", body: text.slice(0, 1000) }];
41
+ return [];
42
42
  }
43
43
 
44
44
  if (eventType === "item.started" && item?.type === "command_execution") {
@@ -46,7 +46,7 @@ function mapCodexEventToActivity(event: any): ActivityContent | null {
46
46
  const cleaned = typeof cmd === "string"
47
47
  ? cmd.replace(/^\/usr\/bin\/\w+ -lc ['"]?/, "").replace(/['"]?$/, "")
48
48
  : JSON.stringify(cmd);
49
- return { type: "action", action: "Running", parameter: cleaned.slice(0, 200) };
49
+ return [{ type: "action", action: "Running", parameter: cleaned.slice(0, 200) }];
50
50
  }
51
51
 
52
52
  if (eventType === "item.completed" && item?.type === "command_execution") {
@@ -57,19 +57,19 @@ function mapCodexEventToActivity(event: any): ActivityContent | null {
57
57
  ? cmd.replace(/^\/usr\/bin\/\w+ -lc ['"]?/, "").replace(/['"]?$/, "")
58
58
  : JSON.stringify(cmd);
59
59
  const truncated = output.length > 1000 ? output.slice(0, 1000) + "..." : output;
60
- return {
60
+ return [{
61
61
  type: "action",
62
62
  action: `${cleaned.slice(0, 150)}`,
63
63
  parameter: `exit ${exitCode}`,
64
64
  result: truncated || undefined,
65
- };
65
+ }];
66
66
  }
67
67
 
68
68
  if (eventType === "item.completed" && item?.type === "file_changes") {
69
69
  const files = item.files ?? [];
70
70
  const fileList = Array.isArray(files) ? files.join(", ") : String(files);
71
71
  const preview = (item.diff ?? item.content ?? "").slice(0, 500) || undefined;
72
- return { type: "action", action: "Modified files", parameter: fileList || "unknown files", result: preview };
72
+ return [{ type: "action", action: "Modified files", parameter: fileList || "unknown files", result: preview }];
73
73
  }
74
74
 
75
75
  if (eventType === "turn.completed") {
@@ -78,12 +78,12 @@ function mapCodexEventToActivity(event: any): ActivityContent | null {
78
78
  const input = usage.input_tokens ?? 0;
79
79
  const cached = usage.cached_input_tokens ?? 0;
80
80
  const output = usage.output_tokens ?? 0;
81
- return { type: "thought", body: `Codex turn complete (${input} in / ${cached} cached / ${output} out tokens)` };
81
+ return [{ type: "thought", body: `Codex turn complete (${input} in / ${cached} cached / ${output} out tokens)` }];
82
82
  }
83
- return { type: "thought", body: "Codex turn complete" };
83
+ return [{ type: "thought", body: "Codex turn complete" }];
84
84
  }
85
85
 
86
- return null;
86
+ return [];
87
87
  }
88
88
 
89
89
  /**
@@ -248,8 +248,8 @@ export async function runCodex(
248
248
  collectedCommands.push(`\`${cleanCmd}\` → exit ${exitCode}${truncOutput ? "\n```\n" + truncOutput + "\n```" : ""}`);
249
249
  }
250
250
 
251
- const activity = mapCodexEventToActivity(event);
252
- if (activity) {
251
+ const activities = mapCodexEventToActivity(event);
252
+ for (const activity of activities) {
253
253
  if (linearApi && agentSessionId) {
254
254
  linearApi.emitActivity(agentSessionId, activity).catch((err) => {
255
255
  api.logger.warn(`Failed to emit Codex activity: ${err}`);
@@ -26,14 +26,14 @@ const GEMINI_BIN = "gemini";
26
26
  * Gemini event types:
27
27
  * init → message(user) → message(assistant) → tool_use → tool_result → result
28
28
  */
29
- function mapGeminiEventToActivity(event: any): ActivityContent | null {
29
+ function mapGeminiEventToActivity(event: any): ActivityContent[] {
30
30
  const type = event?.type;
31
31
 
32
32
  // Assistant message (delta text)
33
33
  if (type === "message" && event.role === "assistant") {
34
34
  const text = event.content;
35
- if (text) return { type: "thought", body: text.slice(0, 1000) };
36
- return null;
35
+ if (text) return [{ type: "thought", body: text.slice(0, 1000) }];
36
+ return [];
37
37
  }
38
38
 
39
39
  // Tool use — running a command or tool
@@ -50,7 +50,7 @@ function mapGeminiEventToActivity(event: any): ActivityContent | null {
50
50
  } else {
51
51
  paramSummary = JSON.stringify(params).slice(0, 500);
52
52
  }
53
- return { type: "action", action: `Running ${toolName}`, parameter: paramSummary };
53
+ return [{ type: "action", action: `Running ${toolName}`, parameter: paramSummary }];
54
54
  }
55
55
 
56
56
  // Tool result
@@ -58,11 +58,11 @@ function mapGeminiEventToActivity(event: any): ActivityContent | null {
58
58
  const status = event.status ?? "unknown";
59
59
  const output = event.output ?? "";
60
60
  const truncated = output.length > 1000 ? output.slice(0, 1000) + "..." : output;
61
- return {
61
+ return [{
62
62
  type: "action",
63
63
  action: `Tool ${status}`,
64
64
  parameter: truncated || "(no output)",
65
- };
65
+ }];
66
66
  }
67
67
 
68
68
  // Final result
@@ -74,10 +74,10 @@ function mapGeminiEventToActivity(event: any): ActivityContent | null {
74
74
  if (stats.total_tokens) parts.push(`${stats.total_tokens} tokens`);
75
75
  if (stats.tool_calls) parts.push(`${stats.tool_calls} tool calls`);
76
76
  }
77
- return { type: "thought", body: parts.join(" — ") };
77
+ return [{ type: "thought", body: parts.join(" — ") }];
78
78
  }
79
79
 
80
- return null;
80
+ return [];
81
81
  }
82
82
 
83
83
  /**
@@ -244,9 +244,9 @@ export async function runGemini(
244
244
  }
245
245
  }
246
246
 
247
- // Stream activity to Linear + session progress
248
- const activity = mapGeminiEventToActivity(event);
249
- if (activity) {
247
+ // Stream activities to Linear + session progress
248
+ const activities = mapGeminiEventToActivity(event);
249
+ for (const activity of activities) {
250
250
  if (linearApi && agentSessionId) {
251
251
  linearApi.emitActivity(agentSessionId, activity).catch((err) => {
252
252
  api.logger.warn(`Failed to emit Gemini activity: ${err}`);