@credal/actions 0.2.74 → 0.2.76

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.
@@ -425,7 +425,34 @@ export const jiraGetJiraIssuesByQueryParamsSchema = z.object({
425
425
  });
426
426
  export const jiraGetJiraIssuesByQueryOutputSchema = z.object({
427
427
  success: z.boolean().describe("Whether the records were successfully retrieved"),
428
- records: z.array(z.record(z.string()).describe("An issue from Jira")).describe("The retrieved records").optional(),
428
+ records: z
429
+ .object({
430
+ issues: z
431
+ .array(z.object({
432
+ id: z.string().describe("Internal Jira issue ID"),
433
+ key: z.string().describe("Human-readable issue key (e.g. SSPR-123)"),
434
+ summary: z.string().describe("Summary of the issue"),
435
+ description: z.string().describe("Plain text description"),
436
+ project: z.object({ id: z.string().optional(), key: z.string().optional(), name: z.string().optional() }),
437
+ issueType: z.object({ id: z.string().optional(), name: z.string().optional() }),
438
+ status: z.object({
439
+ id: z.string().optional(),
440
+ name: z.string().optional(),
441
+ category: z.string().optional(),
442
+ }),
443
+ assignee: z.string().nullable().describe("Email of the assignee, if any").optional(),
444
+ reporter: z.string().nullable().describe("Email of the reporter, if any").optional(),
445
+ creator: z.string().nullable().describe("Email of the creator, if any").optional(),
446
+ created: z.string().datetime({ offset: true }),
447
+ updated: z.string().datetime({ offset: true }),
448
+ resolution: z.string().nullable().optional(),
449
+ dueDate: z.string().date().nullable().optional(),
450
+ }))
451
+ .describe("The retrieved Jira issues")
452
+ .optional(),
453
+ })
454
+ .describe("The result object containing issues")
455
+ .optional(),
429
456
  error: z.string().describe("The error that occurred if the records were not successfully retrieved").optional(),
430
457
  });
431
458
  export const jiraOrgAssignJiraTicketParamsSchema = z.object({
@@ -573,7 +600,34 @@ export const jiraOrgGetJiraIssuesByQueryParamsSchema = z.object({
573
600
  });
574
601
  export const jiraOrgGetJiraIssuesByQueryOutputSchema = z.object({
575
602
  success: z.boolean().describe("Whether the records were successfully retrieved"),
576
- records: z.array(z.record(z.string()).describe("An issue from Jira")).describe("The retrieved records").optional(),
603
+ records: z
604
+ .object({
605
+ issues: z
606
+ .array(z.object({
607
+ id: z.string().describe("Internal Jira issue ID"),
608
+ key: z.string().describe("Human-readable issue key (e.g. SSPR-123)"),
609
+ summary: z.string().describe("Summary of the issue"),
610
+ description: z.string().describe("Plain text description"),
611
+ project: z.object({ id: z.string().optional(), key: z.string().optional(), name: z.string().optional() }),
612
+ issueType: z.object({ id: z.string().optional(), name: z.string().optional() }),
613
+ status: z.object({
614
+ id: z.string().optional(),
615
+ name: z.string().optional(),
616
+ category: z.string().optional(),
617
+ }),
618
+ assignee: z.string().nullable().describe("Email of the assignee, if any").optional(),
619
+ reporter: z.string().nullable().describe("Email of the reporter, if any").optional(),
620
+ creator: z.string().nullable().describe("Email of the creator, if any").optional(),
621
+ created: z.string().datetime({ offset: true }),
622
+ updated: z.string().datetime({ offset: true }),
623
+ resolution: z.string().nullable().optional(),
624
+ dueDate: z.string().date().nullable().optional(),
625
+ }))
626
+ .describe("The retrieved Jira issues")
627
+ .optional(),
628
+ })
629
+ .describe("The result object containing issues")
630
+ .optional(),
577
631
  error: z.string().describe("The error that occurred if the records were not successfully retrieved").optional(),
578
632
  });
579
633
  export const kandjiGetFVRecoveryKeyForDeviceParamsSchema = z.object({
@@ -1,4 +1,4 @@
1
- export interface GmailMessage {
1
+ interface GmailMessage {
2
2
  payload: {
3
3
  mimeType: string;
4
4
  body?: {
@@ -6,22 +6,22 @@ export interface GmailMessage {
6
6
  size: number;
7
7
  };
8
8
  parts?: Array<{
9
- partId: string;
10
9
  mimeType: string;
11
- body: {
10
+ body?: {
12
11
  data?: string;
13
12
  size: number;
14
13
  };
15
- parts?: Array<{
16
- partId: string;
17
- mimeType: string;
18
- body: {
19
- data?: string;
20
- size: number;
21
- };
22
- }>;
14
+ parts?: GmailMessagePart[];
23
15
  }>;
24
16
  };
25
17
  }
26
- export declare function decodeGmailBase64(base64String: string): string;
18
+ interface GmailMessagePart {
19
+ mimeType: string;
20
+ body?: {
21
+ data?: string;
22
+ size: number;
23
+ };
24
+ parts?: GmailMessagePart[];
25
+ }
27
26
  export declare function getEmailContent(message: GmailMessage): string | null;
27
+ export {};
@@ -1,37 +1,53 @@
1
- export function decodeGmailBase64(base64String) {
2
- // Gmail API uses URL-safe base64 encoding
3
- const standardBase64 = base64String.replace(/-/g, "+").replace(/_/g, "/");
4
- // Add padding if needed
5
- const padded = standardBase64.padEnd(standardBase64.length + ((4 - (standardBase64.length % 4)) % 4), "=");
6
- // Only works for Node.js environment
7
- return Buffer.from(padded, "base64").toString("utf-8");
8
- }
1
+ import { convert } from "html-to-text";
9
2
  export function getEmailContent(message) {
10
3
  var _a;
11
- let textContent = null;
12
- // Function to recursively search for plain text content in parts
13
- function searchParts(parts) {
14
- var _a;
15
- if (!parts)
16
- return;
17
- for (const part of parts) {
18
- if (part.mimeType === "text/plain" && ((_a = part.body) === null || _a === void 0 ? void 0 : _a.data) && !textContent) {
19
- textContent = decodeGmailBase64(part.body.data);
20
- }
21
- else if (part.parts) {
22
- // Recursively search nested parts
23
- searchParts(part.parts);
24
- }
25
- }
4
+ const { mimeType, body, parts } = message.payload;
5
+ if (mimeType === "text/plain" && (body === null || body === void 0 ? void 0 : body.data)) {
6
+ return tryDecode(body.data);
7
+ }
8
+ if (mimeType === "text/html" && (body === null || body === void 0 ? void 0 : body.data)) {
9
+ const htmlRaw = tryDecode(body.data);
10
+ if (htmlRaw)
11
+ return convert(htmlRaw, { wordwrap: false });
12
+ }
13
+ const { plainText, htmlText } = searchParts(parts);
14
+ return (_a = plainText !== null && plainText !== void 0 ? plainText : htmlText) !== null && _a !== void 0 ? _a : null;
15
+ }
16
+ function tryDecode(data) {
17
+ if (!data)
18
+ return null;
19
+ try {
20
+ const base64 = data.replace(/-/g, "+").replace(/_/g, "/");
21
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
22
+ return Buffer.from(padded, "base64").toString("utf-8");
26
23
  }
27
- // 1. Check if content is directly in the payload body (simple emails)
28
- if (((_a = message.payload.body) === null || _a === void 0 ? void 0 : _a.data) && message.payload.mimeType === "text/plain") {
29
- return decodeGmailBase64(message.payload.body.data);
24
+ catch (_a) {
25
+ return null;
30
26
  }
31
- // 2. Search through parts for plain text content
32
- if (message.payload.parts) {
33
- searchParts(message.payload.parts);
27
+ }
28
+ function searchParts(parts) {
29
+ let plainText = null;
30
+ let htmlText = null;
31
+ if (!parts)
32
+ return { plainText, htmlText };
33
+ for (const part of parts) {
34
+ const { mimeType, body, parts: subParts } = part;
35
+ if (mimeType === "text/plain" && !plainText) {
36
+ plainText = tryDecode(body === null || body === void 0 ? void 0 : body.data);
37
+ }
38
+ else if (mimeType === "text/html" && !htmlText) {
39
+ const htmlRaw = tryDecode(body === null || body === void 0 ? void 0 : body.data);
40
+ if (htmlRaw) {
41
+ htmlText = convert(htmlRaw, { wordwrap: false });
42
+ }
43
+ }
44
+ if (subParts === null || subParts === void 0 ? void 0 : subParts.length) {
45
+ const result = searchParts(subParts);
46
+ if (!plainText && result.plainText)
47
+ plainText = result.plainText;
48
+ if (!htmlText && result.htmlText)
49
+ htmlText = result.htmlText;
50
+ }
34
51
  }
35
- // 3. Return plain text content or null
36
- return textContent;
52
+ return { plainText, htmlText };
37
53
  }
@@ -7,68 +7,87 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ import { RateLimiter } from "limiter";
10
11
  import { axiosClient } from "../../util/axiosClient.js";
11
12
  import { MISSING_AUTH_TOKEN } from "../../util/missingAuthConstants.js";
12
13
  import { getEmailContent } from "../google-oauth/utils/decodeMessage.js";
14
+ const MAX_EMAIL_CONTENTS_FETCHED = 50;
15
+ const DEFAULT_EMAIL_CONTENTS_FETCHED = 25;
16
+ const MAX_RESULTS_PER_REQUEST = 100;
17
+ const MAX_EMAILS_FETCHED_CONCURRENTLY = 5;
18
+ const limiter = new RateLimiter({ tokensPerInterval: MAX_EMAILS_FETCHED_CONCURRENTLY, interval: "second" });
19
+ function cleanAndTruncateEmail(text, maxLength = 2000) {
20
+ if (!text)
21
+ return "";
22
+ // Remove quoted replies (naive)
23
+ text = text.replace(/^>.*$/gm, "");
24
+ // Remove signatures
25
+ const signatureMarkers = ["\nBest", "\nRegards", "\nThanks", "\nSincerely"];
26
+ for (const marker of signatureMarkers) {
27
+ const index = text.indexOf(marker);
28
+ if (index !== -1) {
29
+ text = text.slice(0, index).trim();
30
+ break;
31
+ }
32
+ }
33
+ // Normalize whitespace
34
+ text = text
35
+ .replace(/\r\n|\r/g, "\n")
36
+ .replace(/\n{3,}/g, "\n\n")
37
+ .trim();
38
+ return text.slice(0, maxLength).trim();
39
+ }
13
40
  const searchGmailMessages = (_a) => __awaiter(void 0, [_a], void 0, function* ({ params, authParams, }) {
14
41
  if (!authParams.authToken) {
15
42
  return { success: false, error: MISSING_AUTH_TOKEN, messages: [] };
16
43
  }
17
44
  const { query, maxResults } = params;
45
+ const max = Math.min(maxResults !== null && maxResults !== void 0 ? maxResults : DEFAULT_EMAIL_CONTENTS_FETCHED, MAX_EMAIL_CONTENTS_FETCHED);
18
46
  const allMessages = [];
19
- const max = maxResults !== null && maxResults !== void 0 ? maxResults : 100;
20
47
  const errorMessages = [];
21
- let pageToken = undefined;
48
+ let pageToken;
22
49
  let fetched = 0;
23
50
  try {
24
51
  while (fetched < max) {
25
52
  const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages?q=${encodeURIComponent(query)}` +
26
53
  (pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : "") +
27
- `&maxResults=${Math.min(100, max - fetched)}`;
54
+ `&maxResults=${Math.min(MAX_RESULTS_PER_REQUEST, max - fetched)}`;
28
55
  const listRes = yield axiosClient.get(url, {
29
- headers: {
30
- Authorization: `Bearer ${authParams.authToken}`,
31
- },
56
+ headers: { Authorization: `Bearer ${authParams.authToken}` },
32
57
  });
33
58
  const { messages: messageList = [], nextPageToken } = listRes.data;
34
59
  if (!Array.isArray(messageList) || messageList.length === 0)
35
60
  break;
36
- const remaining = max - allMessages.length;
37
- const batch = messageList.slice(0, remaining);
61
+ const batch = messageList.slice(0, max - allMessages.length);
38
62
  const results = yield Promise.all(batch.map((msg) => __awaiter(void 0, void 0, void 0, function* () {
39
63
  try {
64
+ yield limiter.removeTokens(1);
40
65
  const msgRes = yield axiosClient.get(`https://gmail.googleapis.com/gmail/v1/users/me/messages/${msg.id}?format=full`, {
41
- headers: {
42
- Authorization: `Bearer ${authParams.authToken}`,
43
- },
66
+ headers: { Authorization: `Bearer ${authParams.authToken}` },
67
+ validateStatus: () => true,
44
68
  });
45
69
  const { id, threadId, snippet, labelIds, internalDate } = msgRes.data;
46
- const emailBody = getEmailContent(msgRes.data) || "";
47
- return {
48
- id,
49
- threadId,
50
- snippet,
51
- labelIds,
52
- internalDate,
53
- emailBody,
54
- };
70
+ const rawBody = getEmailContent(msgRes.data) || "";
71
+ const emailBody = cleanAndTruncateEmail(rawBody);
72
+ return { id, threadId, snippet, labelIds, internalDate, emailBody };
55
73
  }
56
74
  catch (err) {
57
- errorMessages.push(err instanceof Error ? err.message : "Failed to fetch message details");
75
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch message details";
76
+ errorMessages.push(errorMessage);
58
77
  return {
59
78
  id: msg.id,
60
79
  threadId: "",
61
80
  snippet: "",
62
81
  labelIds: [],
63
82
  internalDate: "",
64
- payload: {},
65
- error: err instanceof Error ? err.message : "Failed to fetch message details",
83
+ emailBody: "",
84
+ error: errorMessage,
66
85
  };
67
86
  }
68
87
  })));
69
88
  allMessages.push(...results);
70
89
  fetched = allMessages.length;
71
- if (!nextPageToken || allMessages.length >= max)
90
+ if (!nextPageToken || fetched >= max)
72
91
  break;
73
92
  pageToken = nextPageToken;
74
93
  }
@@ -78,10 +97,10 @@ const searchGmailMessages = (_a) => __awaiter(void 0, [_a], void 0, function* ({
78
97
  error: errorMessages.join("; "),
79
98
  };
80
99
  }
81
- catch (error) {
100
+ catch (err) {
82
101
  return {
83
102
  success: false,
84
- error: error instanceof Error ? error.message : "Unknown error searching Gmail",
103
+ error: err instanceof Error ? err.message : "Unknown error searching Gmail",
85
104
  messages: [],
86
105
  };
87
106
  }
@@ -48,7 +48,38 @@ const getJiraIssuesByQuery = (_a) => __awaiter(void 0, [_a], void 0, function* (
48
48
  });
49
49
  return {
50
50
  success: true,
51
- records: response.data,
51
+ records: {
52
+ issues: response.data.issues.map(issue => {
53
+ var _a, _b, _c, _d;
54
+ return ({
55
+ id: issue.id,
56
+ key: issue.key,
57
+ summary: issue.fields.summary,
58
+ description: extractPlainText(issue.fields.description),
59
+ project: {
60
+ id: issue.fields.project.id,
61
+ key: issue.fields.project.key,
62
+ name: issue.fields.project.name,
63
+ },
64
+ issueType: {
65
+ id: issue.fields.issuetype.id,
66
+ name: issue.fields.issuetype.name,
67
+ },
68
+ status: {
69
+ id: issue.fields.status.id,
70
+ name: issue.fields.status.name,
71
+ category: issue.fields.status.statusCategory.name,
72
+ },
73
+ assignee: ((_a = issue.fields.assignee) === null || _a === void 0 ? void 0 : _a.emailAddress) || null,
74
+ reporter: ((_b = issue.fields.reporter) === null || _b === void 0 ? void 0 : _b.emailAddress) || null,
75
+ creator: ((_c = issue.fields.creator) === null || _c === void 0 ? void 0 : _c.emailAddress) || null,
76
+ created: issue.fields.created,
77
+ updated: issue.fields.updated,
78
+ resolution: ((_d = issue.fields.resolution) === null || _d === void 0 ? void 0 : _d.name) || null,
79
+ dueDate: issue.fields.duedate || null,
80
+ });
81
+ }),
82
+ },
52
83
  };
53
84
  }
54
85
  catch (error) {
@@ -63,4 +94,17 @@ const getJiraIssuesByQuery = (_a) => __awaiter(void 0, [_a], void 0, function* (
63
94
  };
64
95
  }
65
96
  });
97
+ function extractPlainText(adf) {
98
+ if (!adf || adf.type !== "doc" || !Array.isArray(adf.content))
99
+ return "";
100
+ return adf.content
101
+ .map(block => {
102
+ if (block.type === "paragraph" && Array.isArray(block.content)) {
103
+ return block.content.map(inline => { var _a; return (_a = inline.text) !== null && _a !== void 0 ? _a : ""; }).join("");
104
+ }
105
+ return "";
106
+ })
107
+ .join("\n")
108
+ .trim();
109
+ }
66
110
  export default getJiraIssuesByQuery;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@credal/actions",
3
- "version": "0.2.74",
3
+ "version": "0.2.76",
4
4
  "type": "module",
5
5
  "description": "AI Actions by Credal AI",
6
6
  "sideEffects": false,
@@ -28,6 +28,7 @@
28
28
  "devDependencies": {
29
29
  "@eslint/js": "^9.16.0",
30
30
  "@jest/globals": "^29.7.0",
31
+ "@types/html-to-text": "^9.0.4",
31
32
  "@types/js-yaml": "^4.0.9",
32
33
  "@types/jsonwebtoken": "^9.0.9",
33
34
  "@types/node": "^22.10.1",
@@ -58,8 +59,10 @@
58
59
  "date-fns": "^4.1.0",
59
60
  "docx": "^9.3.0",
60
61
  "dotenv": "^16.4.7",
62
+ "html-to-text": "^9.0.5",
61
63
  "json-schema-to-zod": "^2.5.0",
62
64
  "jsonwebtoken": "^9.0.2",
65
+ "limiter": "^3.0.0",
63
66
  "mammoth": "^1.4.27",
64
67
  "mongodb": "^6.13.1",
65
68
  "node-forge": "^1.3.1",