@calltelemetry/openclaw-linear 0.9.3 → 0.9.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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",
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Recorded API responses from sub-issue decomposition smoke test.
3
+ *
4
+ * Initially seeded with placeholder data. Overwritten with real API responses
5
+ * when the smoke test runs: npx vitest run src/__test__/smoke-linear-api.test.ts
6
+ *
7
+ * Last recorded: seed (placeholder)
8
+ */
9
+
10
+ export const RECORDED = {
11
+ teamStates: [
12
+ { id: "state-backlog", name: "Backlog", type: "backlog" },
13
+ { id: "state-started", name: "In Progress", type: "started" },
14
+ { id: "state-done", name: "Done", type: "completed" },
15
+ { id: "state-canceled", name: "Canceled", type: "canceled" },
16
+ ],
17
+ createParent: { id: "parent-001", identifier: "UAT-100" },
18
+ createSubIssue1: { id: "sub1-001", identifier: "UAT-101" },
19
+ createSubIssue2: { id: "sub2-001", identifier: "UAT-102" },
20
+ parentDetails: {
21
+ id: "parent-001",
22
+ identifier: "UAT-100",
23
+ title: "[SMOKE TEST] Sub-Issue Parent: Search Feature",
24
+ description:
25
+ "Auto-generated by smoke test to verify sub-issue decomposition.\n\n" +
26
+ "This parent issue should have two sub-issues created under it.",
27
+ estimate: null,
28
+ state: { name: "Backlog", type: "backlog" },
29
+ creator: { name: "Test User", email: null as string | null },
30
+ assignee: null as { name: string } | null,
31
+ labels: { nodes: [] as Array<{ id: string; name: string }> },
32
+ team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
33
+ comments: {
34
+ nodes: [] as Array<{
35
+ body: string;
36
+ user: { name: string } | null;
37
+ createdAt: string;
38
+ }>,
39
+ },
40
+ project: null as { id: string; name: string } | null,
41
+ parent: null as { id: string; identifier: string } | null,
42
+ relations: {
43
+ nodes: [] as Array<{
44
+ type: string;
45
+ relatedIssue: { id: string; identifier: string; title: string };
46
+ }>,
47
+ },
48
+ },
49
+ subIssue1Details: {
50
+ id: "sub1-001",
51
+ identifier: "UAT-101",
52
+ title: "[SMOKE TEST] Sub-Issue 1: Backend API",
53
+ description:
54
+ "Implement the backend search API endpoint.\n\n" +
55
+ "Given a search query, when the API is called, then matching results are returned.",
56
+ estimate: 2,
57
+ state: { name: "Backlog", type: "backlog" },
58
+ creator: { name: "Test User", email: null as string | null },
59
+ assignee: null as { name: string } | null,
60
+ labels: { nodes: [] as Array<{ id: string; name: string }> },
61
+ team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
62
+ comments: {
63
+ nodes: [] as Array<{
64
+ body: string;
65
+ user: { name: string } | null;
66
+ createdAt: string;
67
+ }>,
68
+ },
69
+ project: null as { id: string; name: string } | null,
70
+ parent: { id: "parent-001", identifier: "UAT-100" },
71
+ relations: {
72
+ nodes: [] as Array<{
73
+ type: string;
74
+ relatedIssue: { id: string; identifier: string; title: string };
75
+ }>,
76
+ },
77
+ },
78
+ subIssue2Details: {
79
+ id: "sub2-001",
80
+ identifier: "UAT-102",
81
+ title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
82
+ description:
83
+ "Build the frontend search UI component.\n\n" +
84
+ "Given the search page loads, when the user types a query, then results display in real-time.",
85
+ estimate: 3,
86
+ state: { name: "Backlog", type: "backlog" },
87
+ creator: { name: "Test User", email: null as string | null },
88
+ assignee: null as { name: string } | null,
89
+ labels: { nodes: [] as Array<{ id: string; name: string }> },
90
+ team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
91
+ comments: {
92
+ nodes: [] as Array<{
93
+ body: string;
94
+ user: { name: string } | null;
95
+ createdAt: string;
96
+ }>,
97
+ },
98
+ project: null as { id: string; name: string } | null,
99
+ parent: { id: "parent-001", identifier: "UAT-100" },
100
+ relations: {
101
+ nodes: [] as Array<{
102
+ type: string;
103
+ relatedIssue: { id: string; identifier: string; title: string };
104
+ }>,
105
+ },
106
+ },
107
+ createRelation: { id: "rel-001" },
108
+ subIssue1WithRelation: {
109
+ id: "sub1-001",
110
+ identifier: "UAT-101",
111
+ title: "[SMOKE TEST] Sub-Issue 1: Backend API",
112
+ description:
113
+ "Implement the backend search API endpoint.\n\n" +
114
+ "Given a search query, when the API is called, then matching results are returned.",
115
+ estimate: 2,
116
+ state: { name: "Backlog", type: "backlog" },
117
+ creator: { name: "Test User", email: null as string | null },
118
+ assignee: null as { name: string } | null,
119
+ labels: { nodes: [] as Array<{ id: string; name: string }> },
120
+ team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
121
+ comments: {
122
+ nodes: [] as Array<{
123
+ body: string;
124
+ user: { name: string } | null;
125
+ createdAt: string;
126
+ }>,
127
+ },
128
+ project: null as { id: string; name: string } | null,
129
+ parent: { id: "parent-001", identifier: "UAT-100" },
130
+ relations: {
131
+ nodes: [
132
+ {
133
+ type: "blocks",
134
+ relatedIssue: {
135
+ id: "sub2-001",
136
+ identifier: "UAT-102",
137
+ title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
138
+ },
139
+ },
140
+ ],
141
+ },
142
+ },
143
+ subIssue2WithRelation: {
144
+ id: "sub2-001",
145
+ identifier: "UAT-102",
146
+ title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
147
+ description:
148
+ "Build the frontend search UI component.\n\n" +
149
+ "Given the search page loads, when the user types a query, then results display in real-time.",
150
+ estimate: 3,
151
+ state: { name: "Backlog", type: "backlog" },
152
+ creator: { name: "Test User", email: null as string | null },
153
+ assignee: null as { name: string } | null,
154
+ labels: { nodes: [] as Array<{ id: string; name: string }> },
155
+ team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
156
+ comments: {
157
+ nodes: [] as Array<{
158
+ body: string;
159
+ user: { name: string } | null;
160
+ createdAt: string;
161
+ }>,
162
+ },
163
+ project: null as { id: string; name: string } | null,
164
+ parent: { id: "parent-001", identifier: "UAT-100" },
165
+ relations: {
166
+ nodes: [] as Array<{
167
+ type: string;
168
+ relatedIssue: { id: string; identifier: string; title: string };
169
+ }>,
170
+ },
171
+ },
172
+ };
@@ -10,13 +10,14 @@
10
10
  */
11
11
  import { readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
+ import { homedir } from "node:os";
13
14
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
14
15
  import { LinearAgentApi } from "../api/linear-api.js";
15
16
 
16
17
  // ── Setup ──────────────────────────────────────────────────────────
17
18
 
18
19
  const AUTH_PROFILES_PATH = join(
19
- process.env.HOME ?? "/home/claw",
20
+ homedir(),
20
21
  ".openclaw",
21
22
  "auth-profiles.json",
22
23
  );
@@ -87,6 +87,9 @@ vi.mock("../pipeline/pipeline.js", () => ({
87
87
  vi.mock("../pipeline/active-session.js", () => ({
88
88
  setActiveSession: mockSetActiveSession,
89
89
  clearActiveSession: mockClearActiveSession,
90
+ getIssueAffinity: vi.fn().mockReturnValue(null),
91
+ _configureAffinityTtl: vi.fn(),
92
+ _resetAffinityForTesting: vi.fn(),
90
93
  }));
91
94
 
92
95
  vi.mock("../infra/observability.js", () => ({
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { join } from "node:path";
2
+ import { join, dirname } from "node:path";
3
+ import { homedir } from "node:os";
3
4
  import { mkdirSync, readFileSync } from "node:fs";
5
+ import { createRequire } from "node:module";
4
6
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
5
7
  import type { LinearAgentApi, ActivityContent } from "../api/linear-api.js";
6
8
  import { InactivityWatchdog, resolveWatchdogConfig } from "./watchdog.js";
@@ -15,7 +17,7 @@ interface AgentDirs {
15
17
  }
16
18
 
17
19
  function resolveAgentDirs(agentId: string, config: Record<string, any>): AgentDirs {
18
- const home = process.env.HOME ?? "/home/claw";
20
+ const home = homedir();
19
21
  const agentList = config?.agents?.list as Array<Record<string, any>> | undefined;
20
22
  const agentEntry = agentList?.find((a) => a.id === agentId);
21
23
 
@@ -33,13 +35,13 @@ function resolveAgentDirs(agentId: string, config: Record<string, any>): AgentDi
33
35
  }
34
36
 
35
37
  // Import extensionAPI for embedded agent runner (internal, not in public SDK)
36
- let _extensionAPI: typeof import("/home/claw/.npm-global/lib/node_modules/openclaw/dist/extensionAPI.js") | null = null;
38
+ let _extensionAPI: any | null = null;
37
39
  async function getExtensionAPI() {
38
40
  if (!_extensionAPI) {
39
- // Dynamic import to avoid blocking module load if unavailable
40
- _extensionAPI = await import(
41
- "/home/claw/.npm-global/lib/node_modules/openclaw/dist/extensionAPI.js"
42
- );
41
+ // Resolve the openclaw package location dynamically, then import extensionAPI
42
+ const _require = createRequire(import.meta.url);
43
+ const openclawDir = dirname(_require.resolve("openclaw/package.json"));
44
+ _extensionAPI = await import(join(openclawDir, "dist", "extensionAPI.js"));
43
45
  }
44
46
  return _extensionAPI;
45
47
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
+ import { homedir } from "node:os";
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // Defaults (seconds — matches config units)
@@ -129,7 +130,7 @@ interface AgentProfileWatchdog {
129
130
  toolTimeoutSec?: number;
130
131
  }
131
132
 
132
- const PROFILES_PATH = join(process.env.HOME ?? "/home/claw", ".openclaw", "agent-profiles.json");
133
+ const PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
133
134
 
134
135
  function loadProfileWatchdog(agentId: string): AgentProfileWatchdog | null {
135
136
  try {
@@ -1,11 +1,12 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { homedir } from "node:os";
3
4
  import { refreshLinearToken } from "./auth.js";
4
5
  import { withResilience } from "../infra/resilience.js";
5
6
 
6
7
  export const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
7
8
  export const AUTH_PROFILES_PATH = join(
8
- process.env.HOME ?? "/home/claw",
9
+ homedir(),
9
10
  ".openclaw",
10
11
  "auth-profiles.json",
11
12
  );
@@ -2,10 +2,11 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
3
  import { writeFileSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { homedir } from "node:os";
5
6
 
6
7
  const LINEAR_OAUTH_TOKEN_URL = "https://api.linear.app/oauth/token";
7
8
  const AUTH_PROFILES_PATH = join(
8
- process.env.HOME ?? "/home/claw",
9
+ homedir(),
9
10
  ".openclaw",
10
11
  "auth-profiles.json",
11
12
  );
package/src/infra/cli.ts CHANGED
@@ -846,8 +846,8 @@ async function reposAction(
846
846
  console.log(`\n No "repos" configured in plugin config.`);
847
847
  console.log(` Add a repos map to openclaw.json → plugins.entries.openclaw-linear.config:`);
848
848
  console.log(`\n "repos": {`);
849
- console.log(` "api": "/home/claw/repos/api",`);
850
- console.log(` "frontend": "/home/claw/repos/frontend"`);
849
+ console.log(` "api": "~/repos/api",`);
850
+ console.log(` "frontend": "~/repos/frontend"`);
851
851
  console.log(` }\n`);
852
852
  return;
853
853
  }
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { ensureGitignore } from "../pipeline/artifacts.js";
6
6
  import type { RepoConfig } from "./multi-repo.js";
7
7
 
8
- const DEFAULT_BASE_REPO = "/home/claw/ai-workspace";
8
+ const DEFAULT_BASE_REPO = path.join(homedir(), "ai-workspace");
9
9
  const DEFAULT_WORKTREE_BASE_DIR = path.join(homedir(), ".openclaw", "worktrees");
10
10
 
11
11
  export interface WorktreeInfo {
@@ -22,7 +22,7 @@ export interface WorktreeStatus {
22
22
  }
23
23
 
24
24
  export interface WorktreeOptions {
25
- /** Base git repo to create worktrees from. Default: /home/claw/ai-workspace */
25
+ /** Base git repo to create worktrees from. Default: ~/ai-workspace */
26
26
  baseRepo?: string;
27
27
  /** Directory under which worktrees are created. Default: ~/.openclaw/worktrees */
28
28
  baseDir?: string;
@@ -33,6 +33,9 @@ vi.mock("../api/linear-api.js", () => ({
33
33
  vi.mock("../pipeline/active-session.js", () => ({
34
34
  setActiveSession: vi.fn(),
35
35
  clearActiveSession: vi.fn(),
36
+ getIssueAffinity: vi.fn().mockReturnValue(null),
37
+ _configureAffinityTtl: vi.fn(),
38
+ _resetAffinityForTesting: vi.fn(),
36
39
  }));
37
40
 
38
41
  vi.mock("../infra/observability.js", () => ({