@plosson/agentio 0.1.5 → 0.1.8
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.md +18 -0
- package/package.json +1 -1
- package/src/auth/jira-oauth.ts +249 -0
- package/src/commands/jira.ts +319 -0
- package/src/commands/slack.ts +265 -0
- package/src/config/credentials.ts +9 -0
- package/src/index.ts +4 -0
- package/src/services/jira/client.ts +355 -0
- package/src/services/slack/client.ts +76 -0
- package/src/types/config.ts +3 -1
- package/src/types/jira.ts +87 -0
- package/src/types/slack.ts +17 -0
- package/src/utils/output.ts +88 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CliError } from '../../utils/errors';
|
|
2
|
+
import type {
|
|
3
|
+
SlackCredentials,
|
|
4
|
+
SlackSendOptions,
|
|
5
|
+
SlackSendResult,
|
|
6
|
+
SlackWebhookCredentials,
|
|
7
|
+
} from '../../types/slack';
|
|
8
|
+
|
|
9
|
+
export class SlackClient {
|
|
10
|
+
private credentials: SlackCredentials;
|
|
11
|
+
|
|
12
|
+
constructor(credentials: SlackCredentials) {
|
|
13
|
+
this.credentials = credentials;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async send(options: SlackSendOptions): Promise<SlackSendResult> {
|
|
17
|
+
if (this.credentials.type === 'webhook') {
|
|
18
|
+
return this.sendViaWebhook(options);
|
|
19
|
+
}
|
|
20
|
+
throw new CliError('INVALID_PARAMS', 'Unknown credentials type');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private async sendViaWebhook(options: SlackSendOptions): Promise<SlackSendResult> {
|
|
24
|
+
const webhookUrl = (this.credentials as SlackWebhookCredentials).webhookUrl;
|
|
25
|
+
|
|
26
|
+
if (!webhookUrl?.trim() || !webhookUrl.startsWith('https://')) {
|
|
27
|
+
throw new CliError(
|
|
28
|
+
'INVALID_PARAMS',
|
|
29
|
+
'Invalid webhook URL - must be HTTPS',
|
|
30
|
+
'Check the webhook URL configuration'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Use raw payload if provided, otherwise construct simple text message
|
|
35
|
+
const payload = options.payload ?? { text: options.text };
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(webhookUrl, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify(payload),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = await response.text();
|
|
48
|
+
throw new CliError(
|
|
49
|
+
this.getErrorCode(response.status),
|
|
50
|
+
`Failed to send message via webhook: ${response.status} ${error}`,
|
|
51
|
+
'Check that the webhook URL is valid and the app has permission to post'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
isJsonPayload: !!options.payload,
|
|
58
|
+
};
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err instanceof CliError) throw err;
|
|
61
|
+
throw new CliError(
|
|
62
|
+
'NETWORK_ERROR',
|
|
63
|
+
`Webhook request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
64
|
+
'Verify the webhook URL is correct and accessible'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private getErrorCode(status: number): 'AUTH_FAILED' | 'PERMISSION_DENIED' | 'NOT_FOUND' | 'RATE_LIMITED' | 'API_ERROR' {
|
|
70
|
+
if (status === 401) return 'AUTH_FAILED';
|
|
71
|
+
if (status === 403) return 'PERMISSION_DENIED';
|
|
72
|
+
if (status === 404) return 'NOT_FOUND';
|
|
73
|
+
if (status === 429) return 'RATE_LIMITED';
|
|
74
|
+
return 'API_ERROR';
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -3,14 +3,16 @@ export interface Config {
|
|
|
3
3
|
gmail?: string[];
|
|
4
4
|
gchat?: string[];
|
|
5
5
|
jira?: string[];
|
|
6
|
+
slack?: string[];
|
|
6
7
|
telegram?: string[];
|
|
7
8
|
};
|
|
8
9
|
defaults: {
|
|
9
10
|
gmail?: string;
|
|
10
11
|
gchat?: string;
|
|
11
12
|
jira?: string;
|
|
13
|
+
slack?: string;
|
|
12
14
|
telegram?: string;
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
export type ServiceName = 'gmail' | 'gchat' | 'jira' | 'telegram';
|
|
18
|
+
export type ServiceName = 'gmail' | 'gchat' | 'jira' | 'slack' | 'telegram';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// JIRA OAuth credentials stored per profile
|
|
2
|
+
// Note: clientId and clientSecret are embedded in the app (see config/credentials.ts)
|
|
3
|
+
export interface JiraCredentials {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
refreshToken: string;
|
|
6
|
+
expiryDate: number;
|
|
7
|
+
cloudId: string;
|
|
8
|
+
siteUrl: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// JIRA Project
|
|
12
|
+
export interface JiraProject {
|
|
13
|
+
id: string;
|
|
14
|
+
key: string;
|
|
15
|
+
name: string;
|
|
16
|
+
projectTypeKey: string;
|
|
17
|
+
simplified: boolean;
|
|
18
|
+
style: string;
|
|
19
|
+
isPrivate: boolean;
|
|
20
|
+
avatarUrls?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// JIRA Issue
|
|
24
|
+
export interface JiraIssue {
|
|
25
|
+
id: string;
|
|
26
|
+
key: string;
|
|
27
|
+
summary: string;
|
|
28
|
+
status: string;
|
|
29
|
+
statusCategoryKey: string;
|
|
30
|
+
priority?: string;
|
|
31
|
+
assignee?: string;
|
|
32
|
+
reporter?: string;
|
|
33
|
+
created: string;
|
|
34
|
+
updated: string;
|
|
35
|
+
projectKey: string;
|
|
36
|
+
issueType: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// JIRA Issue Transition
|
|
41
|
+
export interface JiraTransition {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
to: {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
statusCategory: {
|
|
48
|
+
key: string;
|
|
49
|
+
name: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// JIRA Comment
|
|
55
|
+
export interface JiraComment {
|
|
56
|
+
id: string;
|
|
57
|
+
author: string;
|
|
58
|
+
body: string;
|
|
59
|
+
created: string;
|
|
60
|
+
updated: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Options for search
|
|
64
|
+
export interface JiraSearchOptions {
|
|
65
|
+
jql?: string;
|
|
66
|
+
project?: string;
|
|
67
|
+
status?: string;
|
|
68
|
+
assignee?: string;
|
|
69
|
+
maxResults?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Options for listing projects
|
|
73
|
+
export interface JiraProjectListOptions {
|
|
74
|
+
maxResults?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Result types
|
|
78
|
+
export interface JiraCommentResult {
|
|
79
|
+
id: string;
|
|
80
|
+
issueKey: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface JiraTransitionResult {
|
|
84
|
+
issueKey: string;
|
|
85
|
+
transitionName: string;
|
|
86
|
+
newStatus: string;
|
|
87
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SlackWebhookCredentials {
|
|
2
|
+
type: 'webhook';
|
|
3
|
+
webhookUrl: string;
|
|
4
|
+
channelName?: string; // Optional metadata for display
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type SlackCredentials = SlackWebhookCredentials;
|
|
8
|
+
|
|
9
|
+
export interface SlackSendOptions {
|
|
10
|
+
text?: string;
|
|
11
|
+
payload?: Record<string, unknown>; // Raw JSON payload for Block Kit messages
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SlackSendResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
isJsonPayload?: boolean;
|
|
17
|
+
}
|
package/src/utils/output.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { GmailMessage } from '../types/gmail';
|
|
2
2
|
import type { GChatMessage } from '../types/gchat';
|
|
3
|
+
import type { JiraProject, JiraIssue, JiraTransition, JiraCommentResult, JiraTransitionResult } from '../types/jira';
|
|
4
|
+
import type { SlackSendResult } from '../types/slack';
|
|
3
5
|
|
|
4
6
|
// Format a list of Gmail messages
|
|
5
7
|
export function printMessageList(messages: GmailMessage[], total: number): void {
|
|
@@ -103,3 +105,89 @@ export function printGChatMessage(msg: GChatMessage): void {
|
|
|
103
105
|
console.log(msg.text);
|
|
104
106
|
}
|
|
105
107
|
}
|
|
108
|
+
|
|
109
|
+
// JIRA specific formatters
|
|
110
|
+
export function printJiraProjectList(projects: JiraProject[]): void {
|
|
111
|
+
if (projects.length === 0) {
|
|
112
|
+
console.log('No projects found');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`Projects (${projects.length})\n`);
|
|
117
|
+
|
|
118
|
+
for (const project of projects) {
|
|
119
|
+
const privateMarker = project.isPrivate ? ' [private]' : '';
|
|
120
|
+
console.log(`${project.key} - ${project.name}${privateMarker}`);
|
|
121
|
+
console.log(` Type: ${project.projectTypeKey}`);
|
|
122
|
+
console.log('');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function printJiraIssueList(issues: JiraIssue[]): void {
|
|
127
|
+
if (issues.length === 0) {
|
|
128
|
+
console.log('No issues found');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`Issues (${issues.length})\n`);
|
|
133
|
+
|
|
134
|
+
for (const issue of issues) {
|
|
135
|
+
console.log(`${issue.key} [${issue.status}] ${issue.summary}`);
|
|
136
|
+
console.log(` Type: ${issue.issueType} | Project: ${issue.projectKey}`);
|
|
137
|
+
if (issue.assignee) console.log(` Assignee: ${issue.assignee}`);
|
|
138
|
+
if (issue.priority) console.log(` Priority: ${issue.priority}`);
|
|
139
|
+
console.log(` Updated: ${issue.updated}`);
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function printJiraIssue(issue: JiraIssue): void {
|
|
145
|
+
console.log(`Key: ${issue.key}`);
|
|
146
|
+
console.log(`Summary: ${issue.summary}`);
|
|
147
|
+
console.log(`Status: ${issue.status}`);
|
|
148
|
+
console.log(`Type: ${issue.issueType}`);
|
|
149
|
+
console.log(`Project: ${issue.projectKey}`);
|
|
150
|
+
if (issue.priority) console.log(`Priority: ${issue.priority}`);
|
|
151
|
+
if (issue.assignee) console.log(`Assignee: ${issue.assignee}`);
|
|
152
|
+
if (issue.reporter) console.log(`Reporter: ${issue.reporter}`);
|
|
153
|
+
console.log(`Created: ${issue.created}`);
|
|
154
|
+
console.log(`Updated: ${issue.updated}`);
|
|
155
|
+
if (issue.description) {
|
|
156
|
+
console.log('---');
|
|
157
|
+
console.log(issue.description);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function printJiraTransitions(issueKey: string, transitions: JiraTransition[]): void {
|
|
162
|
+
if (transitions.length === 0) {
|
|
163
|
+
console.log(`No transitions available for ${issueKey}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`Available transitions for ${issueKey}:\n`);
|
|
168
|
+
|
|
169
|
+
for (const transition of transitions) {
|
|
170
|
+
console.log(`[${transition.id}] ${transition.name} → ${transition.to.name}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function printJiraCommentResult(result: JiraCommentResult): void {
|
|
175
|
+
console.log('Comment added');
|
|
176
|
+
console.log(`Issue: ${result.issueKey}`);
|
|
177
|
+
console.log(`Comment ID: ${result.id}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function printJiraTransitionResult(result: JiraTransitionResult): void {
|
|
181
|
+
console.log('Issue transitioned');
|
|
182
|
+
console.log(`Issue: ${result.issueKey}`);
|
|
183
|
+
console.log(`Transition: ${result.transitionName}`);
|
|
184
|
+
console.log(`New Status: ${result.newStatus}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Slack specific formatters
|
|
188
|
+
export function printSlackSendResult(result: SlackSendResult): void {
|
|
189
|
+
console.log('Message sent');
|
|
190
|
+
if (result.isJsonPayload) {
|
|
191
|
+
console.log('Type: Block Kit payload');
|
|
192
|
+
}
|
|
193
|
+
}
|