@credal/actions 0.2.216 → 0.2.218

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.
@@ -125,8 +125,16 @@ async function addActionTypes({ file, prefix, action }) {
125
125
  });
126
126
  }
127
127
  async function addTypesToFile({ file, obj, fallback, name, }) {
128
- // Tool calling framework currently having trouble filling in records as opposed to objects
129
- const zodSchema = obj ? convert(obj).replace(/z\.record\(z\.any\(\)\)/g, "z.object({}).catchall(z.any())") : fallback;
128
+ // Tool calling framework currently having trouble filling in records as opposed to objects.
129
+ // Also coerce numeric types from strings: LLMs often emit numbers as JSON strings (e.g. "2"
130
+ // instead of 2) even when the schema specifies integer/number, causing Zod validation failures.
131
+ // z.coerce.number() is a no-op for actual numbers, so this is fully backward-compatible.
132
+ const zodSchema = obj
133
+ ? convert(obj)
134
+ .replace(/z\.record\(z\.any\(\)\)/g, "z.object({}).catchall(z.any())")
135
+ .replace(/z\.number\(\)\.int\(\)/g, "z.coerce.number().int()")
136
+ .replace(/z\.number\(\)/g, "z.coerce.number()")
137
+ : fallback;
130
138
  const zodName = `${name}Schema`;
131
139
  file.addVariableStatement({
132
140
  declarationKind: VariableDeclarationKind.Const,
@@ -9,16 +9,17 @@ const appendRowsToSpreadsheet = async ({ params, authParams, }) => {
9
9
  throw new Error(MISSING_AUTH_TOKEN);
10
10
  }
11
11
  const { spreadsheetId, sheetName, rows } = params;
12
- // Transform rows from schema format to Google Sheets API format
13
- // Schema: [[{ stringValue: "cell1" }, { stringValue: "cell2" }], ...]
14
- // API expects: [["cell1", "cell2"], ...]
15
- const values = rows.map(row => row.map(cell => cell.stringValue));
16
- const appendUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/'${sheetName ?? "Sheet1"}':append`;
17
12
  try {
13
+ if (rows.length === 0) {
14
+ throw new Error("rows array cannot be empty");
15
+ }
16
+ const sheet = sheetName ?? "Sheet1";
17
+ const quotedSheet = /[\s'!]/.test(sheet) ? `'${sheet.replace(/'/g, "''")}'` : sheet;
18
+ const appendUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(quotedSheet)}:append`;
18
19
  const response = await axiosClient.post(appendUrl, {
19
- values,
20
+ values: rows,
20
21
  majorDimension: "ROWS",
21
- range: `'${sheetName}'`,
22
+ range: quotedSheet,
22
23
  }, {
23
24
  headers: {
24
25
  Authorization: `Bearer ${authParams.authToken}`,
@@ -8,20 +8,26 @@ const updateRowsInSpreadsheet = async ({ params, authParams, }) => {
8
8
  if (!authParams.authToken) {
9
9
  throw new Error(MISSING_AUTH_TOKEN);
10
10
  }
11
- const { spreadsheetId, sheetName, startRow, rows } = params;
12
- if (rows.length === 0) {
13
- throw new Error("rows array cannot be empty");
14
- }
15
- const values = rows.map(row => row.map(cell => cell.stringValue));
16
- if (startRow < 1) {
17
- throw new Error("startRow must be >= 1");
18
- }
19
- const endRow = startRow + rows.length - 1;
20
- const range = `'${sheetName ?? "Sheet1"}'!A${startRow}:ZZ${endRow}`;
21
- const updateUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}`;
11
+ const { spreadsheetId, sheetName, startRow, startColumn, rows } = params;
22
12
  try {
13
+ if (rows.length === 0) {
14
+ throw new Error("rows array cannot be empty");
15
+ }
16
+ if (startRow < 1) {
17
+ throw new Error("startRow must be >= 1");
18
+ }
19
+ const col = startColumn ?? "A";
20
+ if (!/^[A-Za-z]+$/.test(col)) {
21
+ throw new Error(`startColumn must be a column letter (e.g. "A", "BE"), got: "${col}"`);
22
+ }
23
+ const endRow = startRow + rows.length - 1;
24
+ const sheet = sheetName ?? "Sheet1";
25
+ // Only quote sheet names that contain spaces or special characters
26
+ const quotedSheet = /[\s'!]/.test(sheet) ? `'${sheet.replace(/'/g, "''")}'` : sheet;
27
+ const range = `${quotedSheet}!${col}${startRow}:ZZ${endRow}`;
28
+ const updateUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}`;
23
29
  const response = await axiosClient.put(updateUrl, {
24
- values,
30
+ values: rows,
25
31
  majorDimension: "ROWS",
26
32
  range,
27
33
  }, {
@@ -1,9 +1,8 @@
1
1
  import type { jiraGetJiraIssuesByQueryFunction } from "../../autogen/types.js";
2
2
  /**
3
3
  * Get Jira issues from Jira Data Center using offset-based pagination (startAt).
4
- * Returns `total` from the Jira API response so callers can detect truncation by
5
- * comparing total > results.length. Does not return `truncated` use `total` instead.
6
- * (Contrast with the Cloud implementation which returns `truncated` but not `total`.)
4
+ * Returns `itemsReturned` and `truncated` so agents can page through results by
5
+ * incrementing startAt by itemsReturned each call until truncated is false.
7
6
  */
8
7
  declare const getJiraDCIssuesByQuery: jiraGetJiraIssuesByQueryFunction;
9
8
  export default getJiraDCIssuesByQuery;
@@ -3,13 +3,12 @@ import { getJiraApiConfig, getErrorMessage, extractPlainText } from "./utils.js"
3
3
  const DEFAULT_LIMIT = 100;
4
4
  /**
5
5
  * Get Jira issues from Jira Data Center using offset-based pagination (startAt).
6
- * Returns `total` from the Jira API response so callers can detect truncation by
7
- * comparing total > results.length. Does not return `truncated` use `total` instead.
8
- * (Contrast with the Cloud implementation which returns `truncated` but not `total`.)
6
+ * Returns `itemsReturned` and `truncated` so agents can page through results by
7
+ * incrementing startAt by itemsReturned each call until truncated is false.
9
8
  */
10
9
  const getJiraDCIssuesByQuery = async ({ params, authParams, }) => {
11
10
  const { authToken } = authParams;
12
- const { query, limit } = params;
11
+ const { query, limit, startAt: paramStartAt } = params;
13
12
  const { apiUrl, browseUrl, strategy } = getJiraApiConfig(authParams);
14
13
  if (!authToken) {
15
14
  throw new Error("Auth token is required");
@@ -36,7 +35,7 @@ const getJiraDCIssuesByQuery = async ({ params, authParams, }) => {
36
35
  const searchEndpoint = strategy.getSearchEndpoint();
37
36
  const requestedLimit = limit ?? DEFAULT_LIMIT;
38
37
  const allIssues = [];
39
- let startAt = 0;
38
+ let currentStartAt = paramStartAt ?? 0;
40
39
  let jiraTotal = undefined;
41
40
  try {
42
41
  // Keep fetching pages until we have all requested issues
@@ -47,7 +46,7 @@ const getJiraDCIssuesByQuery = async ({ params, authParams, }) => {
47
46
  const queryParams = new URLSearchParams();
48
47
  queryParams.set("jql", query);
49
48
  queryParams.set("maxResults", String(maxResults));
50
- queryParams.set("startAt", String(startAt));
49
+ queryParams.set("startAt", String(currentStartAt));
51
50
  queryParams.set("fields", fields.join(","));
52
51
  const fullApiUrl = `${apiUrl}${searchEndpoint}?${queryParams.toString()}`;
53
52
  const response = await axiosClient.get(fullApiUrl, {
@@ -59,13 +58,16 @@ const getJiraDCIssuesByQuery = async ({ params, authParams, }) => {
59
58
  const { issues, total } = response.data;
60
59
  jiraTotal = total;
61
60
  allIssues.push(...issues);
62
- if (allIssues.length >= total || issues.length === 0) {
61
+ if ((paramStartAt ?? 0) + allIssues.length >= total || issues.length === 0) {
63
62
  break;
64
63
  }
65
- startAt += issues.length;
64
+ currentStartAt += issues.length;
66
65
  }
66
+ const absoluteEnd = (paramStartAt ?? 0) + allIssues.length;
67
+ const truncated = jiraTotal !== undefined && absoluteEnd < jiraTotal;
67
68
  return {
68
- total: jiraTotal,
69
+ itemsReturned: allIssues.length,
70
+ truncated,
69
71
  results: allIssues.map(issue => {
70
72
  const { id, key, fields } = issue;
71
73
  const { summary, description, project, issuetype, status, assignee, reporter, creator, created, updated, resolution, duedate, } = fields;
@@ -1,13 +1,12 @@
1
1
  import { Version3Client } from "jira.js";
2
+ import { axiosClient } from "../../util/axiosClient.js";
2
3
  import { getJiraApiConfig, getErrorMessage, extractPlainText, getUserInfoFromAccountId } from "./utils.js";
3
4
  const DEFAULT_LIMIT = 100;
4
- // Jira Cloud implementation using the enhanced search API (cursor-based pagination).
5
- // Returns `truncated: true` when results were cut off at the limit and more pages exist.
6
- // Note: the enhanced API does not expose a total count, so `total` is never returned here.
7
- // Use `jiraDataCenter` provider if you need the exact total count.
5
+ // Jira Cloud implementation using the legacy offset-based search API (/rest/api/3/search).
6
+ // Uses startAt for pagination so agents can resume from any offset even when app-layer truncation occurs.
8
7
  const getJiraIssuesByQuery = async ({ params, authParams, }) => {
9
8
  const { authToken, cloudId } = authParams;
10
- const { query, limit } = params;
9
+ const { query, limit, startAt: paramStartAt } = params;
11
10
  const { browseUrl } = getJiraApiConfig(authParams);
12
11
  if (!authToken)
13
12
  throw new Error("Auth token is required");
@@ -34,47 +33,36 @@ const getJiraIssuesByQuery = async ({ params, authParams, }) => {
34
33
  ];
35
34
  const requestedLimit = limit ?? DEFAULT_LIMIT;
36
35
  const allIssues = [];
37
- let nextPageToken = undefined;
38
- let truncated = false;
36
+ let currentStartAt = paramStartAt ?? 0;
37
+ let jiraTotal = undefined;
38
+ // jira.js client is kept solely for getUserInfoFromAccountId (user email lookups)
39
+ const client = new Version3Client({
40
+ host: `https://api.atlassian.com/ex/jira/${cloudId}`,
41
+ authentication: { oauth2: { accessToken: authToken } },
42
+ });
39
43
  try {
40
- // Initialize jira.js client with OAuth 2.0 authentication
41
- const client = new Version3Client({
42
- host: `https://api.atlassian.com/ex/jira/${cloudId}`,
43
- authentication: {
44
- oauth2: {
45
- accessToken: authToken,
46
- },
47
- },
48
- });
49
- // Keep fetching pages until we have all requested issues
50
44
  while (allIssues.length < requestedLimit) {
51
- // Calculate how many results to fetch in this request
52
45
  const remainingIssues = requestedLimit - allIssues.length;
53
46
  const maxResults = Math.min(remainingIssues, DEFAULT_LIMIT);
54
- // Use the enhanced search endpoint (recommended)
55
- const searchResults = await client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
56
- jql: query,
57
- nextPageToken,
58
- maxResults,
59
- fields,
60
- });
61
- if (!searchResults.issues || searchResults.issues.length === 0) {
62
- break;
63
- }
64
- allIssues.push(...searchResults.issues);
65
- // Check if we've reached the end or have enough results
66
- if (allIssues.length >= requestedLimit || !searchResults.nextPageToken || searchResults.issues.length === 0) {
67
- // Truncated when we hit the limit but Jira still has more pages
68
- truncated = allIssues.length >= requestedLimit && !!searchResults.nextPageToken;
47
+ const queryParams = new URLSearchParams();
48
+ queryParams.set("jql", query);
49
+ queryParams.set("maxResults", String(maxResults));
50
+ queryParams.set("startAt", String(currentStartAt));
51
+ queryParams.set("fields", fields.join(","));
52
+ const response = await axiosClient.get(`https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search?${queryParams.toString()}`, { headers: { Authorization: `Bearer ${authToken}`, Accept: "application/json" } });
53
+ const { issues, total } = response.data;
54
+ jiraTotal = total;
55
+ allIssues.push(...issues);
56
+ if ((paramStartAt ?? 0) + allIssues.length >= total || issues.length === 0) {
69
57
  break;
70
58
  }
71
- nextPageToken = searchResults.nextPageToken;
59
+ currentStartAt += issues.length;
72
60
  }
73
- // Map issues with email addresses
61
+ const absoluteEnd = (paramStartAt ?? 0) + allIssues.length;
62
+ const truncated = jiraTotal !== undefined && absoluteEnd < jiraTotal;
74
63
  const results = await Promise.all(allIssues.map(async ({ id, key, fields }) => {
75
64
  const ticketUrl = `${browseUrl}/browse/${key}`;
76
65
  const { summary, description, project, issuetype, status, assignee, reporter, creator, created, updated, resolution, duedate, } = fields;
77
- // Fetch user info in parallel
78
66
  const [assigneeInfo, reporterInfo, creatorInfo] = await Promise.all([
79
67
  getUserInfoFromAccountId(assignee?.accountId, client),
80
68
  getUserInfoFromAccountId(reporter?.accountId, client),
@@ -88,39 +76,25 @@ const getJiraIssuesByQuery = async ({ params, authParams, }) => {
88
76
  key,
89
77
  summary,
90
78
  description: extractPlainText(description),
91
- project: {
92
- id: project?.id,
93
- key: project?.key,
94
- name: project?.name,
95
- },
96
- issueType: {
97
- id: issuetype?.id,
98
- name: issuetype?.name,
99
- },
100
- status: {
101
- id: status?.id,
102
- name: status?.name,
103
- category: status?.statusCategory?.name,
104
- },
79
+ project: { id: project?.id, key: project?.key, name: project?.name },
80
+ issueType: { id: issuetype?.id, name: issuetype?.name },
81
+ status: { id: status?.id, name: status?.name, category: status?.statusCategory?.name },
105
82
  assignee: assigneeInfo,
106
83
  reporter: reporterInfo,
107
84
  creator: creatorInfo,
108
- created: created,
109
- updated: updated,
85
+ created,
86
+ updated,
110
87
  resolution: resolution?.name,
111
88
  dueDate: duedate,
112
89
  url: ticketUrl,
113
90
  },
114
91
  };
115
92
  }));
116
- return { results, truncated };
93
+ return { itemsReturned: allIssues.length, truncated, results };
117
94
  }
118
95
  catch (error) {
119
96
  console.error("Error retrieving Jira issues:", error);
120
- return {
121
- results: [],
122
- error: getErrorMessage(error),
123
- };
97
+ return { results: [], error: getErrorMessage(error) };
124
98
  }
125
99
  };
126
100
  export default getJiraIssuesByQuery;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@credal/actions",
3
- "version": "0.2.216",
3
+ "version": "0.2.218",
4
4
  "type": "module",
5
5
  "description": "AI Actions by Credal AI",
6
6
  "sideEffects": false,