@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 +6 -0
- package/package.json +1 -1
- package/src/infra/tmux-runner.ts +3 -3
- package/src/tools/claude-tool.ts +17 -15
- package/src/tools/codex-tool.ts +13 -13
- package/src/tools/gemini-tool.ts +11 -11
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
package/src/infra/tmux-runner.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface RunInTmuxOptions {
|
|
|
33
33
|
timeoutMs: number;
|
|
34
34
|
watchdogMs: number;
|
|
35
35
|
logPath: string;
|
|
36
|
-
mapEvent: (event: any) => ActivityContent
|
|
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
|
|
251
|
-
|
|
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}`);
|
package/src/tools/claude-tool.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
activities.push({ type: "action", action: `Running ${toolName}`, parameter: paramSummary });
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
303
|
-
const
|
|
304
|
-
|
|
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}`);
|
package/src/tools/codex-tool.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
252
|
-
|
|
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}`);
|
package/src/tools/gemini-tool.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
80
|
+
return [];
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
@@ -244,9 +244,9 @@ export async function runGemini(
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
// Stream
|
|
248
|
-
const
|
|
249
|
-
|
|
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}`);
|