@mrc2204/openclaw-jira-tools 1.0.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.
package/README.en.md ADDED
@@ -0,0 +1,127 @@
1
+ # openclaw-jira-tools
2
+
3
+ Native OpenClaw plugin that registers Jira tools for agents, helping them use Jira consistently, avoid forgetting configuration, and prefer tool-native workflows instead of shelling out to host-level `jira-cli`.
4
+
5
+ ## Goals
6
+ - Standardize Jira tools for OpenClaw agents
7
+ - Avoid dependency on `brew install jira-cli` on the host machine
8
+ - Read Jira configuration from plugin config (`plugins.entries.jira-tools.config`)
9
+ - Support per-agent / per-user defaults to reduce config drift
10
+ - Sanitize fields against Jira `createmeta` / create screen constraints
11
+ - Support Vietnamese-first operational Jira workflows
12
+
13
+ ## Available tools
14
+
15
+ ### Create / defaults
16
+ - `jira_create_task`
17
+ - `jira_create_epic`
18
+ - `jira_create_subtask`
19
+ - `jira_set_defaults`
20
+ - `jira_inspect_project`
21
+
22
+ ### Issue management
23
+ - `jira_issue_list`
24
+ - `jira_issue_view`
25
+ - `jira_open`
26
+ - `jira_issue_edit`
27
+ - `jira_issue_assign`
28
+ - `jira_issue_comment_add`
29
+ - `jira_issue_clone`
30
+ - `jira_issue_delete`
31
+ - `jira_issue_watch`
32
+ - `jira_issue_move`
33
+ - `jira_issue_worklog_add`
34
+ - `jira_issue_link`
35
+ - `jira_issue_unlink`
36
+
37
+ ### Epic / project / board / sprint / release
38
+ - `jira_epic_list`
39
+ - `jira_epic_view`
40
+ - `jira_epic_add`
41
+ - `jira_epic_remove`
42
+ - `jira_project_list`
43
+ - `jira_project_view`
44
+ - `jira_board_list`
45
+ - `jira_sprint_list`
46
+ - `jira_sprint_add`
47
+ - `jira_sprint_close`
48
+ - `jira_release_list`
49
+ - `jira_me`
50
+ - `jira_serverinfo`
51
+
52
+ ## Repository structure
53
+ ```text
54
+ openclaw-jira-tools/
55
+ ├── openclaw.plugin.json
56
+ ├── package.json
57
+ ├── tsconfig.json
58
+ ├── src/
59
+ │ ├── index.ts
60
+ │ ├── lib/
61
+ │ ├── shared/
62
+ │ └── tools/
63
+ ├── skill/
64
+ │ └── jira/
65
+ │ ├── SKILL.md
66
+ │ └── _meta.json
67
+ └── dist/
68
+ ```
69
+
70
+ ## Bundled skill
71
+ This repository also ships a Jira skill to guide agents on how to use the toolset correctly:
72
+ - `skill/jira/SKILL.md`
73
+ - `skill/jira/_meta.json`
74
+
75
+ The bundled skill documents:
76
+ - issue classification rules
77
+ - Vietnamese issue/comment templates
78
+ - user intent → Jira tool mapping
79
+ - rules for preferring `jira-tools` over host `jira-cli`
80
+ - defaults strategy so agents do not forget configuration
81
+
82
+ ## Correct install from npm
83
+
84
+ > npm package: https://www.npmjs.com/package/@mrc2204/openclaw-jira-tools
85
+
86
+ ```bash
87
+ openclaw plugins install @mrc2204/openclaw-jira-tools
88
+ ```
89
+
90
+ If you need a local/development workflow, use path-based install separately instead of locking a specific version here.
91
+
92
+ ## Local install for development
93
+ ```bash
94
+ cd ~/Work/projects/jira-tools
95
+ npm install
96
+ npm run build
97
+ openclaw plugins install -l ~/Work/projects/jira-tools
98
+ ```
99
+
100
+ ## Example config
101
+ ```json5
102
+ {
103
+ plugins: {
104
+ entries: {
105
+ "jira-tools": {
106
+ enabled: true,
107
+ config: {
108
+ server: "https://your-domain.atlassian.net",
109
+ email: "you@example.com",
110
+ token: "<jira-token>",
111
+ defaultProject: "TAA",
112
+ language: "vi",
113
+ requireClickableLinks: true
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ## Notes
122
+ - If an agent allowlist includes `jira-tools`, OpenClaw enables all tools registered by this plugin.
123
+ - This is a plugin-native Jira surface; it does not automatically import every command from a host-installed `jira-cli` binary.
124
+ - Some Epic-related flows may still depend on Jira project screen configuration (for example Epic Name / Epic Link fields).
125
+
126
+ ## License
127
+ MIT
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # openclaw-jira-tools
2
+
3
+ > English README: [README.en.md](./README.en.md)
4
+
5
+ Plugin OpenClaw chuẩn để đăng ký native Jira tools cho agent, giúp agent dùng Jira nhất quán, không quên config và ưu tiên tool-native thay vì shell-out `jira-cli` trên host.
6
+
7
+ ## Mục tiêu
8
+ - Chuẩn hóa Jira tools cho agent trong OpenClaw
9
+ - Không phụ thuộc `brew install jira-cli` trên máy host
10
+ - Đọc config Jira từ plugin config (`plugins.entries.jira-tools.config`)
11
+ - Hỗ trợ defaults theo agent/user để giảm quên config
12
+ - Sanitize field theo Jira `createmeta` / create screen
13
+ - Ưu tiên nội dung tiếng Việt trong flow vận hành Jira
14
+
15
+ ## Bộ tools hiện có
16
+
17
+ ### Tạo issue / defaults
18
+ - `jira_create_task`
19
+ - `jira_create_epic`
20
+ - `jira_create_subtask`
21
+ - `jira_set_defaults`
22
+ - `jira_inspect_project`
23
+
24
+ ### Issue management
25
+ - `jira_issue_list`
26
+ - `jira_issue_view`
27
+ - `jira_open`
28
+ - `jira_issue_edit`
29
+ - `jira_issue_assign`
30
+ - `jira_issue_comment_add`
31
+ - `jira_issue_clone`
32
+ - `jira_issue_delete`
33
+ - `jira_issue_watch`
34
+ - `jira_issue_move`
35
+ - `jira_issue_worklog_add`
36
+ - `jira_issue_link`
37
+ - `jira_issue_unlink`
38
+
39
+ ### Epic / project / board / sprint / release
40
+ - `jira_epic_list`
41
+ - `jira_epic_view`
42
+ - `jira_epic_add`
43
+ - `jira_epic_remove`
44
+ - `jira_project_list`
45
+ - `jira_project_view`
46
+ - `jira_board_list`
47
+ - `jira_sprint_list`
48
+ - `jira_sprint_add`
49
+ - `jira_sprint_close`
50
+ - `jira_release_list`
51
+ - `jira_me`
52
+ - `jira_serverinfo`
53
+
54
+ ## Cấu trúc repo
55
+ ```text
56
+ openclaw-jira-tools/
57
+ ├── openclaw.plugin.json
58
+ ├── package.json
59
+ ├── tsconfig.json
60
+ ├── src/
61
+ │ ├── index.ts
62
+ │ ├── lib/
63
+ │ ├── shared/
64
+ │ └── tools/
65
+ ├── skill/
66
+ │ └── jira/
67
+ │ ├── SKILL.md
68
+ │ └── _meta.json
69
+ └── dist/
70
+ ```
71
+
72
+ ## Skill đi kèm
73
+ Repo này mang kèm skill Jira để hướng dẫn agent dùng bộ tools đúng flow:
74
+ - `skill/jira/SKILL.md`
75
+ - `skill/jira/_meta.json`
76
+
77
+ Skill này mô tả:
78
+ - chuẩn phân loại issue
79
+ - template tiếng Việt
80
+ - mapping ý định user → tool Jira tương ứng
81
+ - rule dùng `jira-tools` thay cho shell-out `jira-cli`
82
+ - rule defaults để agent không quên config
83
+
84
+ ## Cài đặt đúng từ npm
85
+
86
+ > Package npm: https://www.npmjs.com/package/@mrc2204/openclaw-jira-tools
87
+
88
+ ```bash
89
+ openclaw plugins install @mrc2204/openclaw-jira-tools
90
+ ```
91
+
92
+ Nếu cần môi trường development/local thì dùng path install riêng, không dùng flow này để khóa version.
93
+
94
+ ## Cài local cho phát triển
95
+ ```bash
96
+ cd ~/Work/projects/jira-tools
97
+ npm install
98
+ npm run build
99
+ openclaw plugins install -l ~/Work/projects/jira-tools
100
+ ```
101
+
102
+ ## Config mẫu
103
+ ```json5
104
+ {
105
+ plugins: {
106
+ entries: {
107
+ "jira-tools": {
108
+ enabled: true,
109
+ config: {
110
+ server: "https://your-domain.atlassian.net",
111
+ email: "you@example.com",
112
+ token: "<jira-token>",
113
+ defaultProject: "TAA",
114
+ language: "vi",
115
+ requireClickableLinks: true
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ ## Ghi chú
124
+ - Nếu allowlist của agent chứa `jira-tools`, OpenClaw sẽ bật toàn bộ tools của plugin này.
125
+ - Đây là plugin-native surface; không tự động import toàn bộ command tree của binary `jira-cli` host.
126
+ - Một số flow Epic có thể vẫn phụ thuộc Jira screen config của project (ví dụ Epic Name / Epic Link field).
127
+
128
+ ## License
129
+ MIT
@@ -0,0 +1,8 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ declare const plugin: {
3
+ id: string;
4
+ name: string;
5
+ version: string;
6
+ register(api: OpenClawPluginApi): void;
7
+ };
8
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import { registerCreateTools } from "./tools/create-tools.js";
2
+ import { registerDefaultsTools } from "./tools/defaults-tools.js";
3
+ import { registerInspectTool } from "./tools/inspect-tool.js";
4
+ import { registerManagementTools } from "./tools/management-tools.js";
5
+ const plugin = {
6
+ id: "jira-tools",
7
+ name: "Jira Tools",
8
+ version: "0.1.0",
9
+ register(api) {
10
+ const rawConfig = (api.pluginConfig || api?.config?.plugins?.entries?.["jira-tools"]?.config || {});
11
+ if (!rawConfig.server || !rawConfig.email || !rawConfig.token) {
12
+ throw new Error("jira-tools requires plugins.entries['jira-tools'].config.server/email/token");
13
+ }
14
+ registerCreateTools(api, rawConfig);
15
+ registerDefaultsTools(api);
16
+ registerInspectTool(api, rawConfig);
17
+ registerManagementTools(api, rawConfig);
18
+ console.log("[jira-tools] Registered tools: jira_create_task, jira_create_epic, jira_create_subtask, jira_set_defaults, jira_inspect_project, jira_me, jira_serverinfo, jira_project_list, jira_project_view, jira_issue_list, jira_issue_view, jira_open, jira_issue_edit, jira_issue_assign, jira_issue_comment_add, jira_issue_clone, jira_issue_delete, jira_issue_watch, jira_issue_move, jira_issue_worklog_add, jira_issue_link, jira_issue_unlink, jira_epic_list, jira_epic_view, jira_epic_add, jira_epic_remove, jira_board_list, jira_sprint_list, jira_sprint_add, jira_sprint_close, jira_release_list");
19
+ },
20
+ };
21
+ export default plugin;
@@ -0,0 +1,8 @@
1
+ import type { JiraDefaultsRecord } from "../shared/types.js";
2
+ export declare class JiraDefaultsStore {
3
+ private baseDir;
4
+ constructor(baseDir?: string);
5
+ private filePath;
6
+ get(agentId: string, userId: string): JiraDefaultsRecord;
7
+ set(agentId: string, userId: string, patch: JiraDefaultsRecord): JiraDefaultsRecord;
8
+ }
@@ -0,0 +1,27 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ function safeKey(input) {
4
+ return input.replace(/[^a-zA-Z0-9._-]+/g, "_");
5
+ }
6
+ export class JiraDefaultsStore {
7
+ baseDir;
8
+ constructor(baseDir) {
9
+ const stateDir = process.env.OPENCLAW_STATE_DIR || `${process.env.HOME}/.openclaw`;
10
+ this.baseDir = baseDir || join(stateDir, "plugin-data", "jira-tools", "defaults");
11
+ mkdirSync(this.baseDir, { recursive: true });
12
+ }
13
+ filePath(agentId, userId) {
14
+ return join(this.baseDir, `${safeKey(agentId)}__${safeKey(userId)}.json`);
15
+ }
16
+ get(agentId, userId) {
17
+ const fp = this.filePath(agentId, userId);
18
+ if (!existsSync(fp))
19
+ return {};
20
+ return JSON.parse(readFileSync(fp, "utf8"));
21
+ }
22
+ set(agentId, userId, patch) {
23
+ const next = { ...this.get(agentId, userId), ...patch };
24
+ writeFileSync(this.filePath(agentId, userId), JSON.stringify(next, null, 2), "utf8");
25
+ return next;
26
+ }
27
+ }
@@ -0,0 +1,36 @@
1
+ import type { JiraCreateInput, JiraDefaultsRecord, JiraToolsConfig } from "../shared/types.js";
2
+ export interface JiraResolvedConfig {
3
+ server: string;
4
+ email: string;
5
+ token: string;
6
+ defaultProject: string;
7
+ language: string;
8
+ requireClickableLinks: boolean;
9
+ issueTypeMap: {
10
+ task: string;
11
+ epic: string;
12
+ subtask: string;
13
+ };
14
+ fieldMappings: {
15
+ epicName: string;
16
+ epicLink: string;
17
+ parentLink: string;
18
+ };
19
+ }
20
+ export declare class JiraClient {
21
+ private readonly config;
22
+ constructor(config: JiraResolvedConfig);
23
+ private headers;
24
+ request(path: string, init?: RequestInit): Promise<any>;
25
+ issueUrl(issueKey: string): string;
26
+ getProject(projectKey: string): Promise<any>;
27
+ getCreateMeta(projectKey: string, issueTypeName: string): Promise<any>;
28
+ searchUsers(query: string): Promise<any>;
29
+ getIssue(issueKey: string): Promise<any>;
30
+ resolveAssigneeAccountId(email?: string, displayName?: string): Promise<string | undefined>;
31
+ buildDescription(kind: string, summary: string, objective: string, technicalDetails: string, dod: string, links?: string[], parentKey?: string): any;
32
+ sanitizeFields(fields: Record<string, any>, createmeta: any): Record<string, any>;
33
+ createIssue(fields: Record<string, any>): Promise<any>;
34
+ }
35
+ export declare function resolveConfig(raw: JiraToolsConfig): JiraResolvedConfig;
36
+ export declare function mergeDefaults(input: JiraCreateInput, defaults: JiraDefaultsRecord): JiraCreateInput;
@@ -0,0 +1,112 @@
1
+ export class JiraClient {
2
+ config;
3
+ constructor(config) {
4
+ this.config = config;
5
+ }
6
+ headers() {
7
+ return {
8
+ "Accept": "application/json",
9
+ "Content-Type": "application/json",
10
+ "Authorization": `Basic ${Buffer.from(`${this.config.email}:${this.config.token}`).toString("base64")}`,
11
+ };
12
+ }
13
+ async request(path, init) {
14
+ const res = await fetch(`${this.config.server}${path}`, {
15
+ ...init,
16
+ headers: { ...this.headers(), ...(init?.headers || {}) },
17
+ });
18
+ const text = await res.text();
19
+ const payload = text ? JSON.parse(text) : {};
20
+ if (!res.ok)
21
+ throw new Error(`${res.status}: ${JSON.stringify(payload)}`);
22
+ return payload;
23
+ }
24
+ issueUrl(issueKey) {
25
+ return `${this.config.server}/browse/${issueKey}`;
26
+ }
27
+ async getProject(projectKey) {
28
+ return this.request(`/rest/api/3/project/${projectKey}`);
29
+ }
30
+ async getCreateMeta(projectKey, issueTypeName) {
31
+ const q = new URLSearchParams({
32
+ projectKeys: projectKey,
33
+ issuetypeNames: issueTypeName,
34
+ expand: "projects.issuetypes.fields",
35
+ });
36
+ return this.request(`/rest/api/3/issue/createmeta?${q.toString()}`);
37
+ }
38
+ async searchUsers(query) {
39
+ const q = new URLSearchParams({ query, maxResults: "20" });
40
+ return this.request(`/rest/api/3/user/search?${q.toString()}`);
41
+ }
42
+ async getIssue(issueKey) {
43
+ return this.request(`/rest/api/3/issue/${issueKey}`);
44
+ }
45
+ async resolveAssigneeAccountId(email, displayName) {
46
+ const query = email || displayName;
47
+ if (!query)
48
+ return undefined;
49
+ const users = await this.searchUsers(query);
50
+ if (!Array.isArray(users) || users.length === 0)
51
+ throw new Error(`Không tìm thấy assignee: ${query}`);
52
+ const exact = users.find((u) => (email && u.emailAddress === email) || (displayName && u.displayName === displayName));
53
+ return (exact || users[0]).accountId;
54
+ }
55
+ buildDescription(kind, summary, objective, technicalDetails, dod, links, parentKey) {
56
+ const lines = [
57
+ `## Loại issue`, `- ${kind}`,
58
+ ``, `## Mục tiêu`, objective,
59
+ ``, `## Chi tiết kỹ thuật`, technicalDetails,
60
+ ``, `## Tiêu chuẩn hoàn thành (DoD)`, dod,
61
+ ];
62
+ if (parentKey)
63
+ lines.push("", "## Issue cha", `- ${parentKey}`, `- Link: ${this.issueUrl(parentKey)}`);
64
+ if (links && links.length)
65
+ lines.push("", "## Links liên quan", ...links.map((x) => `- ${x}`));
66
+ lines.push("", "## Ghi chú", "- Nội dung và comment Jira dùng tiếng Việt.");
67
+ const content = lines.map((line) => line ? ({ type: "paragraph", content: [{ type: "text", text: line }] }) : ({ type: "paragraph", content: [] }));
68
+ return { type: "doc", version: 1, content };
69
+ }
70
+ sanitizeFields(fields, createmeta) {
71
+ const allowed = new Set();
72
+ const projects = createmeta?.projects || [];
73
+ const issueTypes = projects[0]?.issuetypes || [];
74
+ const metaFields = issueTypes[0]?.fields || {};
75
+ for (const k of Object.keys(metaFields))
76
+ allowed.add(k);
77
+ const keep = new Set(["project", "summary", "description", "issuetype", "labels", "assignee", "parent"]);
78
+ return Object.fromEntries(Object.entries(fields).filter(([k]) => keep.has(k) || allowed.has(k)));
79
+ }
80
+ async createIssue(fields) {
81
+ return this.request(`/rest/api/3/issue`, { method: "POST", body: JSON.stringify({ fields }) });
82
+ }
83
+ }
84
+ export function resolveConfig(raw) {
85
+ return {
86
+ server: raw.server.replace(/\/$/, ""),
87
+ email: raw.email,
88
+ token: raw.token,
89
+ defaultProject: raw.defaultProject || "TAA",
90
+ language: raw.language || "vi",
91
+ requireClickableLinks: raw.requireClickableLinks !== false,
92
+ issueTypeMap: {
93
+ task: raw.issueTypeMap?.task || "Task",
94
+ epic: raw.issueTypeMap?.epic || "Epic",
95
+ subtask: raw.issueTypeMap?.subtask || "Subtask",
96
+ },
97
+ fieldMappings: {
98
+ epicName: raw.fieldMappings?.epicName || "customfield_10011",
99
+ epicLink: raw.fieldMappings?.epicLink || "customfield_10014",
100
+ parentLink: raw.fieldMappings?.parentLink || "customfield_10018",
101
+ },
102
+ };
103
+ }
104
+ export function mergeDefaults(input, defaults) {
105
+ return {
106
+ ...input,
107
+ project: input.project || defaults.defaultProject,
108
+ assigneeEmail: input.assigneeEmail || defaults.assigneeEmail,
109
+ assigneeName: input.assigneeName || defaults.assigneeName,
110
+ labels: input.labels?.length ? input.labels : defaults.labels,
111
+ };
112
+ }
@@ -0,0 +1,17 @@
1
+ export declare function getSessionKey(ctx: any): string;
2
+ export declare function parseSessionIdentity(sessionKey: string): {
3
+ agentId: string;
4
+ userId: string;
5
+ };
6
+ export declare function createToolResult(text: string, isError?: boolean): {
7
+ content: {
8
+ type: "text";
9
+ text: string;
10
+ }[];
11
+ details: {
12
+ toolResult: {
13
+ text: string;
14
+ };
15
+ };
16
+ isError: boolean;
17
+ };
@@ -0,0 +1,21 @@
1
+ export function getSessionKey(ctx) {
2
+ return String(ctx?.sessionKey ||
3
+ ctx?.session?.key ||
4
+ ctx?.session?.sessionKey ||
5
+ ctx?.meta?.sessionKey ||
6
+ "main");
7
+ }
8
+ export function parseSessionIdentity(sessionKey) {
9
+ const parts = sessionKey.split(":");
10
+ if (parts.length >= 5 && parts[0] === "agent") {
11
+ return { agentId: parts[1] || "main", userId: parts[4] || "main" };
12
+ }
13
+ return { agentId: "main", userId: sessionKey || "main" };
14
+ }
15
+ export function createToolResult(text, isError = false) {
16
+ return {
17
+ content: [{ type: "text", text }],
18
+ details: { toolResult: { text } },
19
+ isError,
20
+ };
21
+ }
@@ -0,0 +1,41 @@
1
+ export interface JiraToolsConfig {
2
+ server: string;
3
+ email: string;
4
+ token: string;
5
+ defaultProject?: string;
6
+ language?: string;
7
+ requireClickableLinks?: boolean;
8
+ issueTypeMap?: {
9
+ task?: string;
10
+ epic?: string;
11
+ subtask?: string;
12
+ };
13
+ fieldMappings?: {
14
+ epicName?: string;
15
+ epicLink?: string;
16
+ parentLink?: string;
17
+ };
18
+ }
19
+ export interface JiraDefaultsRecord {
20
+ defaultProject?: string;
21
+ assigneeEmail?: string;
22
+ assigneeName?: string;
23
+ labels?: string[];
24
+ epicKey?: string;
25
+ parentLink?: string;
26
+ templateTask?: string;
27
+ templateEpic?: string;
28
+ templateSubtask?: string;
29
+ }
30
+ export interface JiraCreateInput {
31
+ project?: string;
32
+ summary: string;
33
+ body?: string;
34
+ objective?: string;
35
+ technicalDetails?: string;
36
+ dod?: string;
37
+ labels?: string[];
38
+ links?: string[];
39
+ assigneeEmail?: string;
40
+ assigneeName?: string;
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import type { JiraToolsConfig } from "../shared/types.js";
3
+ export declare function registerCreateTools(api: OpenClawPluginApi, rawConfig: JiraToolsConfig): void;