tide-commander 1.32.2 → 1.35.0
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/dist/assets/{BossLogsModal-2lqGyuCD.js → BossLogsModal-B_dgVF7L.js} +1 -1
- package/dist/assets/{BossSpawnModal-CR-ofDeB.js → BossSpawnModal-CSO1bYxA.js} +1 -1
- package/dist/assets/{ControlsModal-DQ8QVshA.js → ControlsModal-WMTTqbca.js} +1 -1
- package/dist/assets/{DockerLogsModal-D6yQTHy7.js → DockerLogsModal-THzhLHch.js} +1 -1
- package/dist/assets/{EmbeddedEditor-wechGxGl.js → EmbeddedEditor-DWLKJYav.js} +1 -1
- package/dist/assets/GmailOAuthSetup-DN9ceaS6.js +270 -0
- package/dist/assets/{GoogleOAuthSetup-BcpJEydM.js → GoogleOAuthSetup-bVST2EOB.js} +1 -1
- package/dist/assets/{IframeModal-E2E7NR08.js → IframeModal-BELsjvgi.js} +1 -1
- package/dist/assets/{IntegrationsPanel-CrS2QOFR.js → IntegrationsPanel-DwDr4BRt.js} +2 -2
- package/dist/assets/{LogViewerModal-BpIPZeFr.js → LogViewerModal-CMe04PO5.js} +1 -1
- package/dist/assets/MonitoringModal-CqSalNeY.js +1 -0
- package/dist/assets/{PM2LogsModal-CDk_2mi1.js → PM2LogsModal-CCmCDxVt.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-BmgBrk9J.js → RestoreArchivedAreaModal-IfzPidIv.js} +1 -1
- package/dist/assets/{SaveSnapshotModal-DWta9pcx.js → SaveSnapshotModal-DUhrVD5l.js} +1 -1
- package/dist/assets/{Scene2DCanvas-C3CcFsjU.js → Scene2DCanvas-Bl5DUC7w.js} +1 -1
- package/dist/assets/{SceneManager-mFUakRNl.js → SceneManager-BGO9tiaI.js} +1 -1
- package/dist/assets/{SkillsPanel-Br3h8GNh.js → SkillsPanel-CPFOI4Tl.js} +1 -1
- package/dist/assets/{SnapshotManager-CPYWfnPR.js → SnapshotManager-Cbu0tJBz.js} +1 -1
- package/dist/assets/{SpawnModal-DSDirR0j.js → SpawnModal-BqDbsYLY.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-Daz67phV.js → SubordinateAssignmentModal-DOqkhL_L.js} +1 -1
- package/dist/assets/{SupervisorPanel-DoAl5e8W.js → SupervisorPanel-BvX-dlk_.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-CVIHbqDt.js → TriggerManagerPanel-RUVFmKmf.js} +1 -1
- package/dist/assets/WorkflowEditorPanel-CwZpEqzM.js +42 -0
- package/dist/assets/browser-ponyfill-DZOWXZ4K.js +2 -0
- package/dist/assets/camera-D_KeL_pz.js +1 -0
- package/dist/assets/{index-I-I3pPPW.js → index-B-wV06cR.js} +1 -1
- package/dist/assets/index-BFguOWBW.js +2 -0
- package/dist/assets/{index-uXOqPsuU.js → index-C7gqY2AA.js} +1 -1
- package/dist/assets/{index-CE_GbjZ6.js → index-CiD1Rwaq.js} +1 -1
- package/dist/assets/index-D4nfDvz4.js +49 -0
- package/dist/assets/{index-CvMf5n2v.js → index-DDPUtz8-.js} +1 -1
- package/dist/assets/{index-Chrxgrys.js → index-EH8IBvSU.js} +1 -1
- package/dist/assets/{index-DWVQ48nQ.js → index-H0PzHVFw.js} +1 -1
- package/dist/assets/main-Cjm0d8dZ.js +152 -0
- package/dist/assets/main-DqC9_fF4.css +1 -0
- package/dist/assets/{prism-cpp-CcQnz8LL.js → prism-cpp-CK2Ly5dS.js} +1 -1
- package/dist/assets/{prism-csharp-DFIAaw4Y.js → prism-csharp-ByDDDiWW.js} +1 -1
- package/dist/assets/{prism-elixir-jP4m4T-8.js → prism-elixir-df27OMMQ.js} +1 -1
- package/dist/assets/{prism-haskell-BrMZM7_F.js → prism-haskell-Ce8aBmia.js} +1 -1
- package/dist/assets/{prism-java-BEsh8u4L.js → prism-java-CK6tws4L.js} +1 -1
- package/dist/assets/{prism-perl-ecHKp0bZ.js → prism-perl-UZfqnD51.js} +1 -1
- package/dist/assets/{prism-php-Ch-kk89U.js → prism-php-Dt9698bA.js} +1 -1
- package/dist/assets/{prism-ruby-zNpGDk6v.js → prism-ruby-CQBUuZIF.js} +1 -1
- package/dist/assets/{prism-scss-BeuXx0O0.js → prism-scss-CeN16CFC.js} +1 -1
- package/dist/assets/{vendor-react-uS-d4TUT.js → vendor-react--Eh9ivFN.js} +2 -2
- package/dist/assets/{web-IWJRtE3-.js → web-D1vWYL8u.js} +1 -1
- package/dist/assets/{web-DdRt5c0R.js → web-DUq3Undh.js} +1 -1
- package/dist/index.html +3 -3
- package/dist/src/packages/server/data/builtin-skills/boss-instructions.js +57 -66
- package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
- package/dist/src/packages/server/data/builtin-skills/workflow-builder.js +253 -0
- package/dist/src/packages/server/data/builtin-skills/workflow-designer.js +157 -0
- package/dist/src/packages/server/data/event-queries.js +24 -0
- package/dist/src/packages/server/data/migrations/002_workflow_agent_binding.sql +14 -0
- package/dist/src/packages/server/data/migrations/003_matcher_executions.sql +19 -0
- package/dist/src/packages/server/data/migrations/004_matcher_message_source.sql +8 -0
- package/dist/src/packages/server/integrations/gmail/gmail-client.js +75 -15
- package/dist/src/packages/server/integrations/gmail/gmail-config.js +32 -2
- package/dist/src/packages/server/integrations/gmail/gmail-routes.js +5 -0
- package/dist/src/packages/server/integrations/gmail/index.js +23 -1
- package/dist/src/packages/server/integrations/jira/jira-client.js +11 -5
- package/dist/src/packages/server/integrations/jira/jira-routes.js +20 -3
- package/dist/src/packages/server/integrations/jira/jira-skill.js +110 -58
- package/dist/src/packages/server/routes/trigger-routes.js +22 -0
- package/dist/src/packages/server/routes/workflow-routes.js +86 -2
- package/dist/src/packages/server/services/boss-message-service.js +1 -1
- package/dist/src/packages/server/services/llm-matcher-service.js +50 -81
- package/dist/src/packages/server/services/trigger-service.js +195 -6
- package/dist/src/packages/server/services/workflow-executor.js +230 -0
- package/dist/src/packages/server/services/workflow-service.js +59 -13
- package/package.json +7 -7
- package/dist/assets/GmailOAuthSetup-B2GDjROU.js +0 -222
- package/dist/assets/MonitoringModal-CIF9MUm9.js +0 -1
- package/dist/assets/WorkflowEditorPanel-C4BRfmDM.js +0 -42
- package/dist/assets/browser-ponyfill-DIm4hKhx.js +0 -2
- package/dist/assets/camera-8crtHeRa.js +0 -1
- package/dist/assets/index--PCy0J0f.js +0 -49
- package/dist/assets/index-DOzx4Y9b.js +0 -2
- package/dist/assets/main-DQpuQfqS.css +0 -1
- package/dist/assets/main-DahZb6P4.js +0 -152
|
@@ -14,6 +14,7 @@ const REDIRECT_PATH = '/api/email/auth/callback';
|
|
|
14
14
|
// ─── State ───
|
|
15
15
|
let ctx = null;
|
|
16
16
|
let config = {
|
|
17
|
+
authMethod: 'oauth2',
|
|
17
18
|
clientId: '',
|
|
18
19
|
clientSecret: '',
|
|
19
20
|
pollingIntervalMs: 30000,
|
|
@@ -31,25 +32,63 @@ let lastHistoryId;
|
|
|
31
32
|
export async function init(context) {
|
|
32
33
|
ctx = context;
|
|
33
34
|
loadConfig();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// Reset state on reinit
|
|
36
|
+
gmail = null;
|
|
37
|
+
oauth2Client = null;
|
|
38
|
+
authenticatedEmail = undefined;
|
|
39
|
+
lastError = undefined;
|
|
40
|
+
const authMethod = config.authMethod || 'oauth2';
|
|
41
|
+
if (authMethod === 'service_account') {
|
|
42
|
+
// ── Service Account authentication via domain-wide delegation ──
|
|
43
|
+
if (!config.serviceAccountJson || !config.impersonateEmail) {
|
|
44
|
+
ctx.log.info('Gmail not configured (missing service account credentials)');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
44
47
|
try {
|
|
48
|
+
const sa = JSON.parse(config.serviceAccountJson);
|
|
49
|
+
const jwtClient = new google.auth.JWT({
|
|
50
|
+
email: sa.client_email,
|
|
51
|
+
key: sa.private_key,
|
|
52
|
+
scopes: SCOPES,
|
|
53
|
+
subject: config.impersonateEmail,
|
|
54
|
+
});
|
|
55
|
+
gmail = google.gmail({ version: 'v1', auth: jwtClient });
|
|
45
56
|
const profile = await gmail.users.getProfile({ userId: 'me' });
|
|
46
57
|
authenticatedEmail = profile.data.emailAddress ?? undefined;
|
|
47
58
|
lastHistoryId = profile.data.historyId ?? undefined;
|
|
48
|
-
|
|
59
|
+
lastError = undefined;
|
|
60
|
+
ctx.log.info(`Gmail authenticated via service account as ${authenticatedEmail}`);
|
|
61
|
+
startPolling();
|
|
49
62
|
}
|
|
50
63
|
catch (err) {
|
|
51
|
-
lastError = `
|
|
52
|
-
ctx.log.error('Gmail authentication failed', err);
|
|
64
|
+
lastError = `Service account authentication failed: ${err}`;
|
|
65
|
+
ctx.log.error('Gmail service account authentication failed', err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// ── OAuth2 authentication ──
|
|
70
|
+
if (!config.clientId || !config.clientSecret) {
|
|
71
|
+
ctx.log.info('Gmail not configured (missing OAuth credentials)');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
|
|
75
|
+
// Load refresh token from secrets
|
|
76
|
+
const refreshToken = ctx.secrets.get('GOOGLE_REFRESH_TOKEN') || config.refreshToken;
|
|
77
|
+
if (refreshToken) {
|
|
78
|
+
oauth2Client.setCredentials({ refresh_token: refreshToken });
|
|
79
|
+
gmail = google.gmail({ version: 'v1', auth: oauth2Client });
|
|
80
|
+
try {
|
|
81
|
+
const profile = await gmail.users.getProfile({ userId: 'me' });
|
|
82
|
+
authenticatedEmail = profile.data.emailAddress ?? undefined;
|
|
83
|
+
lastHistoryId = profile.data.historyId ?? undefined;
|
|
84
|
+
lastError = undefined;
|
|
85
|
+
ctx.log.info(`Gmail authenticated as ${authenticatedEmail}`);
|
|
86
|
+
startPolling();
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
lastError = `Authentication failed: ${err}`;
|
|
90
|
+
ctx.log.error('Gmail authentication failed', err);
|
|
91
|
+
}
|
|
53
92
|
}
|
|
54
93
|
}
|
|
55
94
|
}
|
|
@@ -59,14 +98,21 @@ function loadConfig() {
|
|
|
59
98
|
const clientId = ctx.secrets.get('GOOGLE_CLIENT_ID') || '';
|
|
60
99
|
const clientSecret = ctx.secrets.get('GOOGLE_CLIENT_SECRET') || '';
|
|
61
100
|
const refreshToken = ctx.secrets.get('GOOGLE_REFRESH_TOKEN');
|
|
101
|
+
const serviceAccountJson = ctx.secrets.get('GOOGLE_SERVICE_ACCOUNT_JSON') || undefined;
|
|
102
|
+
const impersonateEmail = ctx.secrets.get('GOOGLE_IMPERSONATE_EMAIL') || undefined;
|
|
62
103
|
config = {
|
|
63
104
|
...config,
|
|
64
105
|
clientId,
|
|
65
106
|
clientSecret,
|
|
66
107
|
refreshToken: refreshToken || undefined,
|
|
108
|
+
serviceAccountJson,
|
|
109
|
+
impersonateEmail,
|
|
67
110
|
};
|
|
68
111
|
}
|
|
69
112
|
export function updateConfig(updates) {
|
|
113
|
+
if (updates.authMethod !== undefined) {
|
|
114
|
+
config.authMethod = updates.authMethod;
|
|
115
|
+
}
|
|
70
116
|
if (updates.clientId !== undefined) {
|
|
71
117
|
config.clientId = updates.clientId;
|
|
72
118
|
ctx?.secrets.set('GOOGLE_CLIENT_ID', updates.clientId);
|
|
@@ -75,6 +121,14 @@ export function updateConfig(updates) {
|
|
|
75
121
|
config.clientSecret = updates.clientSecret;
|
|
76
122
|
ctx?.secrets.set('GOOGLE_CLIENT_SECRET', updates.clientSecret);
|
|
77
123
|
}
|
|
124
|
+
if (updates.serviceAccountJson !== undefined) {
|
|
125
|
+
config.serviceAccountJson = updates.serviceAccountJson;
|
|
126
|
+
ctx?.secrets.set('GOOGLE_SERVICE_ACCOUNT_JSON', updates.serviceAccountJson);
|
|
127
|
+
}
|
|
128
|
+
if (updates.impersonateEmail !== undefined) {
|
|
129
|
+
config.impersonateEmail = updates.impersonateEmail;
|
|
130
|
+
ctx?.secrets.set('GOOGLE_IMPERSONATE_EMAIL', updates.impersonateEmail);
|
|
131
|
+
}
|
|
78
132
|
if (updates.pollingIntervalMs !== undefined) {
|
|
79
133
|
config.pollingIntervalMs = updates.pollingIntervalMs;
|
|
80
134
|
}
|
|
@@ -91,8 +145,10 @@ export function shutdown() {
|
|
|
91
145
|
}
|
|
92
146
|
// ─── Status ───
|
|
93
147
|
export function getStatus() {
|
|
148
|
+
const oauthConfigured = Boolean(config.clientId && config.clientSecret);
|
|
149
|
+
const serviceAccountConfigured = Boolean(config.serviceAccountJson && config.impersonateEmail);
|
|
94
150
|
return {
|
|
95
|
-
configured:
|
|
151
|
+
configured: oauthConfigured || serviceAccountConfigured,
|
|
96
152
|
authenticated: Boolean(gmail && authenticatedEmail),
|
|
97
153
|
emailAddress: authenticatedEmail,
|
|
98
154
|
pollingActive: pollingTimer !== null,
|
|
@@ -102,7 +158,9 @@ export function getStatus() {
|
|
|
102
158
|
};
|
|
103
159
|
}
|
|
104
160
|
export function isConfigured() {
|
|
105
|
-
|
|
161
|
+
const oauthConfigured = Boolean(config.clientId && config.clientSecret);
|
|
162
|
+
const serviceAccountConfigured = Boolean(config.serviceAccountJson && config.impersonateEmail);
|
|
163
|
+
return oauthConfigured || serviceAccountConfigured;
|
|
106
164
|
}
|
|
107
165
|
// ─── OAuth2 ───
|
|
108
166
|
export function getAuthUrl() {
|
|
@@ -248,6 +306,7 @@ function parseGmailMessage(msg) {
|
|
|
248
306
|
const subject = getHeader('Subject');
|
|
249
307
|
const date = msg.internalDate ? parseInt(msg.internalDate, 10) : Date.now();
|
|
250
308
|
const inReplyTo = getHeader('In-Reply-To') || undefined;
|
|
309
|
+
const rfc822MessageId = getHeader('Message-ID') || getHeader('Message-Id') || undefined;
|
|
251
310
|
// Extract body
|
|
252
311
|
let body = '';
|
|
253
312
|
let bodyHtml = '';
|
|
@@ -285,6 +344,7 @@ function parseGmailMessage(msg) {
|
|
|
285
344
|
return {
|
|
286
345
|
messageId: msg.id || '',
|
|
287
346
|
threadId: msg.threadId || '',
|
|
347
|
+
rfc822MessageId,
|
|
288
348
|
from,
|
|
289
349
|
to,
|
|
290
350
|
cc,
|
|
@@ -3,12 +3,25 @@
|
|
|
3
3
|
* ConfigField[] for OAuth credentials, polling interval, and approval defaults.
|
|
4
4
|
*/
|
|
5
5
|
export const gmailConfigSchema = [
|
|
6
|
+
{
|
|
7
|
+
key: 'authMethod',
|
|
8
|
+
label: 'Authentication Method',
|
|
9
|
+
type: 'select',
|
|
10
|
+
description: 'Choose OAuth2 (browser login) or Service Account (domain-wide delegation)',
|
|
11
|
+
required: false,
|
|
12
|
+
defaultValue: 'oauth2',
|
|
13
|
+
options: [
|
|
14
|
+
{ label: 'OAuth2', value: 'oauth2' },
|
|
15
|
+
{ label: 'Service Account', value: 'service_account' },
|
|
16
|
+
],
|
|
17
|
+
group: 'Authentication',
|
|
18
|
+
},
|
|
6
19
|
{
|
|
7
20
|
key: 'clientId',
|
|
8
21
|
label: 'Google OAuth Client ID',
|
|
9
22
|
type: 'text',
|
|
10
23
|
description: 'OAuth2 client ID from Google Cloud Console',
|
|
11
|
-
required:
|
|
24
|
+
required: false,
|
|
12
25
|
secret: true,
|
|
13
26
|
group: 'Authentication',
|
|
14
27
|
},
|
|
@@ -17,10 +30,27 @@ export const gmailConfigSchema = [
|
|
|
17
30
|
label: 'Google OAuth Client Secret',
|
|
18
31
|
type: 'password',
|
|
19
32
|
description: 'OAuth2 client secret from Google Cloud Console',
|
|
20
|
-
required:
|
|
33
|
+
required: false,
|
|
34
|
+
secret: true,
|
|
35
|
+
group: 'Authentication',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'serviceAccountJson',
|
|
39
|
+
label: 'Service Account JSON',
|
|
40
|
+
type: 'textarea',
|
|
41
|
+
description: 'Full JSON content of the Google service account key file',
|
|
42
|
+
required: false,
|
|
21
43
|
secret: true,
|
|
22
44
|
group: 'Authentication',
|
|
23
45
|
},
|
|
46
|
+
{
|
|
47
|
+
key: 'impersonateEmail',
|
|
48
|
+
label: 'Impersonate Email',
|
|
49
|
+
type: 'email',
|
|
50
|
+
description: 'Email address to impersonate via domain-wide delegation (required for service account)',
|
|
51
|
+
required: false,
|
|
52
|
+
group: 'Authentication',
|
|
53
|
+
},
|
|
24
54
|
{
|
|
25
55
|
key: 'pollingIntervalMs',
|
|
26
56
|
label: 'Polling Interval (ms)',
|
|
@@ -102,6 +102,11 @@ router.get('/status', (req, res) => {
|
|
|
102
102
|
// GET /api/email/auth/url — Get OAuth authorization URL
|
|
103
103
|
router.get('/auth/url', (req, res) => {
|
|
104
104
|
try {
|
|
105
|
+
const currentConfig = gmailClient.getConfig();
|
|
106
|
+
if (currentConfig.authMethod === 'service_account') {
|
|
107
|
+
res.status(400).json({ error: 'OAuth flow is not used with service account authentication. Configure service account JSON and impersonate email instead.' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
105
110
|
const authUrl = gmailClient.getAuthUrl();
|
|
106
111
|
res.json({ url: authUrl });
|
|
107
112
|
}
|
|
@@ -12,7 +12,7 @@ let integrationCtx = null;
|
|
|
12
12
|
export const gmailPlugin = {
|
|
13
13
|
id: 'gmail',
|
|
14
14
|
name: 'Gmail',
|
|
15
|
-
description: '
|
|
15
|
+
description: 'Send and receive emails through Gmail. Supports OAuth 2.0 and Service Account authentication.',
|
|
16
16
|
routePrefix: '/email',
|
|
17
17
|
async init(ctx) {
|
|
18
18
|
integrationCtx = ctx;
|
|
@@ -39,15 +39,21 @@ export const gmailPlugin = {
|
|
|
39
39
|
getConfig() {
|
|
40
40
|
if (!integrationCtx) {
|
|
41
41
|
return {
|
|
42
|
+
authMethod: 'oauth2',
|
|
42
43
|
clientId: '',
|
|
43
44
|
clientSecret: '',
|
|
45
|
+
serviceAccountJson: '',
|
|
46
|
+
impersonateEmail: '',
|
|
44
47
|
pollingIntervalMs: 30000,
|
|
45
48
|
defaultApprovalKeywords: 'approved,aprobado,autorizado,yes,ok',
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
return {
|
|
52
|
+
authMethod: gmailClient.getConfig().authMethod || 'oauth2',
|
|
49
53
|
clientId: integrationCtx.secrets.get('GOOGLE_CLIENT_ID') || '',
|
|
50
54
|
clientSecret: integrationCtx.secrets.get('GOOGLE_CLIENT_SECRET') || '',
|
|
55
|
+
serviceAccountJson: integrationCtx.secrets.get('GOOGLE_SERVICE_ACCOUNT_JSON') || '',
|
|
56
|
+
impersonateEmail: integrationCtx.secrets.get('GOOGLE_IMPERSONATE_EMAIL') || '',
|
|
51
57
|
pollingIntervalMs: gmailClient.getConfig().pollingIntervalMs,
|
|
52
58
|
defaultApprovalKeywords: gmailClient.getConfig().defaultApprovalKeywords.join(','),
|
|
53
59
|
};
|
|
@@ -56,6 +62,9 @@ export const gmailPlugin = {
|
|
|
56
62
|
if (!integrationCtx)
|
|
57
63
|
throw new Error('Gmail not initialized');
|
|
58
64
|
const updates = {};
|
|
65
|
+
if (config.authMethod !== undefined) {
|
|
66
|
+
updates.authMethod = config.authMethod;
|
|
67
|
+
}
|
|
59
68
|
if (config.clientId) {
|
|
60
69
|
updates.clientId = config.clientId;
|
|
61
70
|
integrationCtx.secrets.set('GOOGLE_CLIENT_ID', config.clientId);
|
|
@@ -64,6 +73,14 @@ export const gmailPlugin = {
|
|
|
64
73
|
updates.clientSecret = config.clientSecret;
|
|
65
74
|
integrationCtx.secrets.set('GOOGLE_CLIENT_SECRET', config.clientSecret);
|
|
66
75
|
}
|
|
76
|
+
if (config.serviceAccountJson) {
|
|
77
|
+
updates.serviceAccountJson = config.serviceAccountJson;
|
|
78
|
+
integrationCtx.secrets.set('GOOGLE_SERVICE_ACCOUNT_JSON', config.serviceAccountJson);
|
|
79
|
+
}
|
|
80
|
+
if (config.impersonateEmail) {
|
|
81
|
+
updates.impersonateEmail = config.impersonateEmail;
|
|
82
|
+
integrationCtx.secrets.set('GOOGLE_IMPERSONATE_EMAIL', config.impersonateEmail);
|
|
83
|
+
}
|
|
67
84
|
if (config.pollingIntervalMs) {
|
|
68
85
|
updates.pollingIntervalMs = config.pollingIntervalMs;
|
|
69
86
|
}
|
|
@@ -75,6 +92,11 @@ export const gmailPlugin = {
|
|
|
75
92
|
updates.defaultApprovalKeywords = keywords;
|
|
76
93
|
}
|
|
77
94
|
gmailClient.updateConfig(updates);
|
|
95
|
+
// Re-initialize authentication when auth method or credentials change
|
|
96
|
+
if (config.authMethod !== undefined || config.serviceAccountJson || config.clientId) {
|
|
97
|
+
gmailClient.shutdown();
|
|
98
|
+
await gmailClient.init(integrationCtx);
|
|
99
|
+
}
|
|
78
100
|
},
|
|
79
101
|
getCustomSettingsComponent() {
|
|
80
102
|
return 'gmail-oauth';
|
|
@@ -118,12 +118,18 @@ export class JiraClient {
|
|
|
118
118
|
}
|
|
119
119
|
// ─── Search ───
|
|
120
120
|
async searchIssues(jql, opts) {
|
|
121
|
-
|
|
121
|
+
// Use POST /rest/api/3/search which returns full issue fields by default
|
|
122
|
+
// (the newer /search/jql endpoint only returns IDs unless fields are explicit)
|
|
123
|
+
const body = {
|
|
122
124
|
jql,
|
|
123
|
-
maxResults:
|
|
124
|
-
startAt:
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
maxResults: opts?.maxResults ?? 25,
|
|
126
|
+
startAt: opts?.startAt ?? 0,
|
|
127
|
+
fields: opts?.fields ?? [
|
|
128
|
+
'summary', 'status', 'priority', 'assignee', 'issuetype',
|
|
129
|
+
'project', 'created', 'updated', 'labels',
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
return (await this.request('POST', '/rest/api/3/search', body));
|
|
127
133
|
}
|
|
128
134
|
// ─── Service Desk (optional) ───
|
|
129
135
|
async createServiceRequest(serviceDeskId, requestTypeId, params) {
|
|
@@ -202,13 +202,30 @@ export function createJiraRoutes(client, ctx) {
|
|
|
202
202
|
try {
|
|
203
203
|
const jql = req.query.jql;
|
|
204
204
|
if (!jql) {
|
|
205
|
-
res.status(400).json({ error: 'jql query parameter is required' });
|
|
205
|
+
res.status(400).json({ error: 'jql query parameter is required. Jira Cloud requires bounded queries - always include a filter like project = X, created >= -30d, or similar.' });
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
const maxResults = parseInt(req.query.maxResults) || 25;
|
|
209
209
|
const startAt = parseInt(req.query.startAt) || 0;
|
|
210
|
-
const
|
|
211
|
-
|
|
210
|
+
const fieldsParam = req.query.fields;
|
|
211
|
+
const fields = fieldsParam ? fieldsParam.split(',').map(f => f.trim()) : undefined;
|
|
212
|
+
const result = await client.searchIssues(jql, { maxResults, startAt, fields });
|
|
213
|
+
// Return a clean, agent-friendly response with key fields inline
|
|
214
|
+
const issues = result.issues.map(issue => ({
|
|
215
|
+
key: issue.key,
|
|
216
|
+
id: issue.id,
|
|
217
|
+
summary: issue.fields.summary,
|
|
218
|
+
status: issue.fields.status?.name,
|
|
219
|
+
priority: issue.fields.priority?.name,
|
|
220
|
+
assignee: issue.fields.assignee?.displayName,
|
|
221
|
+
issueType: issue.fields.issuetype?.name,
|
|
222
|
+
project: issue.fields.project?.key,
|
|
223
|
+
created: issue.fields.created,
|
|
224
|
+
updated: issue.fields.updated,
|
|
225
|
+
labels: issue.fields.labels,
|
|
226
|
+
self: issue.self,
|
|
227
|
+
}));
|
|
228
|
+
res.json({ issues, total: result.total, startAt: result.startAt, maxResults: result.maxResults });
|
|
212
229
|
}
|
|
213
230
|
catch (err) {
|
|
214
231
|
const message = err instanceof Error ? err.message : 'Search failed';
|
|
@@ -9,105 +9,157 @@ export const jiraSkill = {
|
|
|
9
9
|
allowedTools: ['Bash(curl:*)'],
|
|
10
10
|
content: `# Jira Service Desk
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Manage Jira issues through the proxy API. Auth headers are added automatically by the system.
|
|
13
|
+
All Jira credentials (base URL, email, API token) are handled server-side - never include them in requests.
|
|
13
14
|
|
|
14
|
-
##
|
|
15
|
+
## Quick Reference
|
|
15
16
|
|
|
17
|
+
**Search recent issues:**
|
|
16
18
|
\`\`\`bash
|
|
17
|
-
curl -s
|
|
19
|
+
curl -s "http://localhost:5174/api/jira/search?jql=created%20%3E%3D%20-30d%20ORDER%20BY%20created%20DESC&maxResults=5"
|
|
20
|
+
\`\`\`
|
|
21
|
+
|
|
22
|
+
**Get a specific issue:**
|
|
23
|
+
\`\`\`bash
|
|
24
|
+
curl -s http://localhost:5174/api/jira/issues/SD-1234
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
**Create an issue:**
|
|
28
|
+
\`\`\`bash
|
|
29
|
+
curl -s -X POST http://localhost:5174/api/jira/issues \\
|
|
18
30
|
-H "Content-Type: application/json" \\
|
|
19
|
-
-
|
|
20
|
-
-d '{
|
|
21
|
-
"projectKey": "SD",
|
|
22
|
-
"issueType": "Change Request",
|
|
23
|
-
"summary": "CC - Release v2.1.0 - 2026-03-20",
|
|
24
|
-
"description": "Control de Cambios for release v2.1.0. Requested by: John Doe. Systems affected: API, Frontend.",
|
|
25
|
-
"priority": "Medium",
|
|
26
|
-
"labels": ["cc", "release"]
|
|
27
|
-
}'
|
|
31
|
+
-d '{"summary":"Issue title","description":"Details here"}'
|
|
28
32
|
\`\`\`
|
|
29
|
-
|
|
33
|
+
If projectKey and issueType are omitted, configured defaults are used.
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Search (JQL)
|
|
32
38
|
|
|
33
39
|
\`\`\`bash
|
|
34
|
-
curl -s "
|
|
35
|
-
-H "X-Auth-Token: {{AUTH_TOKEN}}"
|
|
40
|
+
curl -s "http://localhost:5174/api/jira/search?jql=QUERY&maxResults=N&startAt=0"
|
|
36
41
|
\`\`\`
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
Returns flat issue objects with key, summary, status, priority, assignee, issueType, project, created, updated, and labels inline - no extra API calls needed.
|
|
44
|
+
|
|
45
|
+
Query params: \`jql\` (required), \`maxResults\` (default 25), \`startAt\` (default 0), \`fields\` (comma-separated, optional override)
|
|
46
|
+
|
|
47
|
+
**IMPORTANT: Jira Cloud rejects unbounded JQL.** Always include a filter like \`project = X\`, \`created >= -30d\`, or similar.
|
|
39
48
|
|
|
49
|
+
### Common JQL Recipes
|
|
50
|
+
|
|
51
|
+
\`\`\`
|
|
52
|
+
# Last N issues across all projects
|
|
53
|
+
created >= -30d ORDER BY created DESC
|
|
54
|
+
|
|
55
|
+
# Issues by project
|
|
56
|
+
project = SD ORDER BY created DESC
|
|
57
|
+
|
|
58
|
+
# Open issues
|
|
59
|
+
project = SD AND status != Done ORDER BY updated DESC
|
|
60
|
+
|
|
61
|
+
# Issues assigned to someone
|
|
62
|
+
project = SD AND assignee = "user@email.com" ORDER BY created DESC
|
|
63
|
+
|
|
64
|
+
# Search by keyword in summary
|
|
65
|
+
project = SD AND summary ~ "vpn" ORDER BY created DESC
|
|
66
|
+
|
|
67
|
+
# Issues with specific label
|
|
68
|
+
project = SD AND labels = "release" ORDER BY created DESC
|
|
69
|
+
|
|
70
|
+
# Recently updated
|
|
71
|
+
updated >= -7d ORDER BY updated DESC
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
Remember to URL-encode the jql value in the query string (spaces as %20, = as %3D, etc.), or use curl --data-urlencode:
|
|
40
75
|
\`\`\`bash
|
|
41
|
-
curl -s -
|
|
42
|
-
-H "Content-Type: application/json" \\
|
|
43
|
-
-H "X-Auth-Token: {{AUTH_TOKEN}}" \\
|
|
44
|
-
-d '{ "summary": "Updated summary", "priority": "High" }'
|
|
76
|
+
curl -s -G http://localhost:5174/api/jira/search --data-urlencode "jql=project = SD ORDER BY created DESC" --data-urlencode "maxResults=10"
|
|
45
77
|
\`\`\`
|
|
46
78
|
|
|
47
|
-
##
|
|
79
|
+
## Get Issue
|
|
48
80
|
|
|
49
81
|
\`\`\`bash
|
|
50
|
-
curl -s
|
|
82
|
+
curl -s http://localhost:5174/api/jira/issues/SD-1234
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
Returns full Jira issue with all fields including description, comments count, etc.
|
|
86
|
+
|
|
87
|
+
## Create Issue
|
|
88
|
+
|
|
89
|
+
\`\`\`bash
|
|
90
|
+
curl -s -X POST http://localhost:5174/api/jira/issues \\
|
|
51
91
|
-H "Content-Type: application/json" \\
|
|
52
|
-
-
|
|
53
|
-
|
|
92
|
+
-d '{
|
|
93
|
+
"projectKey": "SD",
|
|
94
|
+
"issueType": "Task",
|
|
95
|
+
"summary": "Issue title",
|
|
96
|
+
"description": "Issue description",
|
|
97
|
+
"priority": "Medium",
|
|
98
|
+
"labels": ["tag1", "tag2"]
|
|
99
|
+
}'
|
|
54
100
|
\`\`\`
|
|
55
101
|
|
|
56
|
-
|
|
102
|
+
\`projectKey\` and \`issueType\` are optional if defaults are configured in the integration settings.
|
|
103
|
+
Returns: \`{ "key": "SD-1234", "id": "10042", "self": "..." }\`
|
|
104
|
+
|
|
105
|
+
## Update Issue
|
|
57
106
|
|
|
58
|
-
First, list available transitions:
|
|
59
107
|
\`\`\`bash
|
|
60
|
-
curl -s
|
|
61
|
-
-H "
|
|
108
|
+
curl -s -X PATCH http://localhost:5174/api/jira/issues/SD-1234 \\
|
|
109
|
+
-H "Content-Type: application/json" \\
|
|
110
|
+
-d '{"summary": "Updated title", "priority": "High"}'
|
|
62
111
|
\`\`\`
|
|
63
112
|
|
|
64
|
-
|
|
113
|
+
## Comments
|
|
114
|
+
|
|
115
|
+
**Add a comment:**
|
|
65
116
|
\`\`\`bash
|
|
66
|
-
curl -s -X POST
|
|
117
|
+
curl -s -X POST http://localhost:5174/api/jira/issues/SD-1234/comments \\
|
|
67
118
|
-H "Content-Type: application/json" \\
|
|
68
|
-
-
|
|
69
|
-
|
|
119
|
+
-d '{"body": "Comment text here"}'
|
|
120
|
+
\`\`\`
|
|
121
|
+
|
|
122
|
+
**Get comments:**
|
|
123
|
+
\`\`\`bash
|
|
124
|
+
curl -s http://localhost:5174/api/jira/issues/SD-1234/comments
|
|
70
125
|
\`\`\`
|
|
71
126
|
|
|
72
|
-
##
|
|
127
|
+
## Transitions (Change Status)
|
|
128
|
+
|
|
129
|
+
**List available transitions:**
|
|
130
|
+
\`\`\`bash
|
|
131
|
+
curl -s http://localhost:5174/api/jira/issues/SD-1234/transitions
|
|
132
|
+
\`\`\`
|
|
73
133
|
|
|
134
|
+
**Apply a transition:**
|
|
74
135
|
\`\`\`bash
|
|
75
|
-
curl -s
|
|
76
|
-
-H "
|
|
136
|
+
curl -s -X POST http://localhost:5174/api/jira/issues/SD-1234/transitions \\
|
|
137
|
+
-H "Content-Type: application/json" \\
|
|
138
|
+
-d '{"transitionId": "31", "comment": "Transition reason"}'
|
|
77
139
|
\`\`\`
|
|
78
140
|
|
|
79
|
-
##
|
|
141
|
+
## Service Desk Requests
|
|
80
142
|
|
|
81
143
|
\`\`\`bash
|
|
82
|
-
curl -s -X POST
|
|
144
|
+
curl -s -X POST http://localhost:5174/api/jira/service-desk/1/requests \\
|
|
83
145
|
-H "Content-Type: application/json" \\
|
|
84
|
-
-
|
|
85
|
-
-d '{
|
|
86
|
-
"requestTypeId": "10",
|
|
87
|
-
"summary": "New service request",
|
|
88
|
-
"description": "Details of the request"
|
|
89
|
-
}'
|
|
146
|
+
-d '{"requestTypeId": "10", "summary": "Request title", "description": "Details"}'
|
|
90
147
|
\`\`\`
|
|
91
148
|
|
|
92
149
|
## Custom Fields
|
|
93
150
|
|
|
94
|
-
|
|
95
|
-
Field mappings (workflow variable to Jira custom field IDs) are configured in the integration settings.
|
|
151
|
+
Pass custom fields via the \`customFields\` object when creating or updating. Field mappings (friendly name to Jira field ID) are configured in integration settings.
|
|
96
152
|
|
|
97
153
|
\`\`\`bash
|
|
98
|
-
curl -s -X POST
|
|
154
|
+
curl -s -X POST http://localhost:5174/api/jira/issues \\
|
|
99
155
|
-H "Content-Type: application/json" \\
|
|
100
|
-
-
|
|
101
|
-
-d '{
|
|
102
|
-
"projectKey": "SD",
|
|
103
|
-
"issueType": "Change Request",
|
|
104
|
-
"summary": "Release v2.1.0",
|
|
105
|
-
"description": "Details...",
|
|
106
|
-
"customFields": {
|
|
107
|
-
"release_name": "v2.1.0",
|
|
108
|
-
"environment": "production"
|
|
109
|
-
}
|
|
110
|
-
}'
|
|
156
|
+
-d '{"summary": "Release v2.1.0", "customFields": {"release_name": "v2.1.0", "environment": "production"}}'
|
|
111
157
|
\`\`\`
|
|
158
|
+
|
|
159
|
+
## Notes
|
|
160
|
+
- Auth headers are added automatically by the system.
|
|
161
|
+
- The proxy handles all Jira authentication - never include Jira credentials in requests.
|
|
162
|
+
- All text is automatically converted to Atlassian Document Format (ADF) by the proxy.
|
|
163
|
+
- Use \`curl -s -G ... --data-urlencode\` for JQL queries with special characters.
|
|
112
164
|
`,
|
|
113
165
|
};
|
|
@@ -31,6 +31,11 @@ router.get('/', (_req, res) => {
|
|
|
31
31
|
const triggers = triggerService.getAllTriggers();
|
|
32
32
|
res.json(triggers);
|
|
33
33
|
});
|
|
34
|
+
// All matchers evaluated against a specific source message (must be before /:id)
|
|
35
|
+
router.get('/matchers/by-source/:sourceType/:sourceId', (req, res) => {
|
|
36
|
+
const matchers = triggerService.getMatchersBySource(req.params.sourceType, req.params.sourceId);
|
|
37
|
+
res.json({ matchers });
|
|
38
|
+
});
|
|
34
39
|
// Get single trigger
|
|
35
40
|
router.get('/:id', (req, res) => {
|
|
36
41
|
const trigger = triggerService.getTrigger(req.params.id);
|
|
@@ -212,6 +217,23 @@ router.get('/:id/events', (req, res) => {
|
|
|
212
217
|
const events = triggerService.getTriggerEvents(req.params.id, limit);
|
|
213
218
|
res.json(events);
|
|
214
219
|
});
|
|
220
|
+
// ─── Matcher Execution Debugging ───
|
|
221
|
+
// List all matchers that ran for a specific trigger event
|
|
222
|
+
router.get('/events/:eventId/matchers', (req, res) => {
|
|
223
|
+
const eventId = parseInt(req.params.eventId);
|
|
224
|
+
if (isNaN(eventId)) {
|
|
225
|
+
res.status(400).json({ error: 'Invalid eventId' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const matchers = triggerService.getMatchersByEvent(eventId);
|
|
229
|
+
res.json({ matchers });
|
|
230
|
+
});
|
|
231
|
+
// Matcher history for a trigger (across all events)
|
|
232
|
+
router.get('/:triggerId/matcher-history', (req, res) => {
|
|
233
|
+
const limit = parseInt(req.query.limit) || 100;
|
|
234
|
+
const matchers = triggerService.getMatcherHistoryByTrigger(req.params.triggerId, limit);
|
|
235
|
+
res.json({ matchers });
|
|
236
|
+
});
|
|
215
237
|
// ─── Helper ───
|
|
216
238
|
function getNestedValue(obj, path) {
|
|
217
239
|
const parts = path.split('.');
|