jira-ai 0.4.5 → 0.4.10

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/cli.js CHANGED
@@ -20,6 +20,7 @@ import { getIssueStatisticsCommand } from './commands/get-issue-statistics.js';
20
20
  import { getPersonWorklogCommand } from './commands/get-person-worklog.js';
21
21
  import { aboutCommand } from './commands/about.js';
22
22
  import { authCommand } from './commands/auth.js';
23
+ import { settingsCommand } from './commands/settings.js';
23
24
  import { listOrganizations, useOrganizationCommand, removeOrganizationCommand } from './commands/organization.js';
24
25
  import { isCommandAllowed, getAllowedCommands } from './lib/settings.js';
25
26
  import { setOrganizationOverride } from './lib/jira-client.js';
@@ -78,7 +79,7 @@ function withPermission(commandName, commandFn, config = {}) {
78
79
  // Auth command (always allowed, skips validation)
79
80
  program
80
81
  .command('auth')
81
- .description('Set up Jira authentication credentials')
82
+ .description('Set up Jira authentication credentials. Supports interactive input, raw JSON string via --from-json, or .env file via --from-file.')
82
83
  .option('--from-json <json_string>', 'Accepts a raw JSON string with credentials')
83
84
  .option('--from-file <path>', 'Accepts a path to a file (typically .env) with credentials')
84
85
  .option('--alias <alias>', 'Alias for this organization')
@@ -90,34 +91,34 @@ const org = program
90
91
  .description('Manage Jira organization profiles');
91
92
  org
92
93
  .command('list')
93
- .description('Show all saved organizations')
94
+ .description('List all saved Jira organization profiles, showing their aliases and associated host URLs.')
94
95
  .action(() => listOrganizations());
95
96
  org
96
97
  .command('use <alias>')
97
- .description('Switch the active organization')
98
+ .description('Switch the active Jira organization profile to the one specified by the alias.')
98
99
  .action((alias) => useOrganizationCommand(alias));
99
100
  org
100
101
  .command('remove <alias>')
101
- .description('Delete an organization credentials')
102
+ .description('Delete the saved credentials and profile for the specified organization alias.')
102
103
  .action((alias) => removeOrganizationCommand(alias));
103
104
  org
104
105
  .command('add <alias>')
105
- .description('Add a new organization')
106
+ .description('Interactive prompt to add a new Jira organization profile with the given alias.')
106
107
  .action((alias) => authCommand({ alias }));
107
108
  // Me command
108
109
  program
109
110
  .command('me')
110
- .description('Show basic user information')
111
+ .description('Show profile details for the currently authenticated user, including Jira host, display name, email, account ID, status, and time zone.')
111
112
  .action(withPermission('me', meCommand));
112
113
  // Projects command
113
114
  program
114
115
  .command('projects')
115
- .description('Show list of projects')
116
+ .description('List all accessible Jira projects showing their key, name, ID, type, and project lead.')
116
117
  .action(withPermission('projects', projectsCommand));
117
118
  // List colleagues command
118
119
  program
119
120
  .command('list-colleagues [project-key]')
120
- .description('Show all colleagues in the project or organization')
121
+ .description('Search and list users within the organization or a specific project (if project-key is provided). Returns display name, email, and account ID.')
121
122
  .action(withPermission('list-colleagues', listColleaguesCommand, {
122
123
  validateArgs: (args) => {
123
124
  if (args[0]) {
@@ -128,7 +129,7 @@ program
128
129
  // Task with details command
129
130
  program
130
131
  .command('task-with-details <task-id>')
131
- .description('Show task title, body, and comments')
132
+ .description('Retrieve comprehensive issue data including key, summary, status (name, category), assignee, reporter, creation/update dates, due date, labels, parent/subtasks, description, and comments. Use --include-detailed-history to fetch a chronological log of all changes including field updates and status transitions.')
132
133
  .option('--include-detailed-history', 'Include the full history of task actions')
133
134
  .option('--history-limit <number>', 'Number of history entries to show (default: 50)', '50')
134
135
  .option('--history-offset <number>', 'Number of history entries to skip (default: 0)', '0')
@@ -138,21 +139,21 @@ program
138
139
  // Project statuses command
139
140
  program
140
141
  .command('project-statuses <project-id>')
141
- .description('Show all possible statuses for a project')
142
+ .description('Fetch all available workflow statuses for a given project. Returns status name, ID, category (To Do, In Progress, Done), and description.')
142
143
  .action(withPermission('project-statuses', projectStatusesCommand, {
143
144
  validateArgs: (args) => validateOptions(ProjectKeySchema, args[0])
144
145
  }));
145
146
  // List issue types command
146
147
  program
147
148
  .command('list-issue-types <project-key>')
148
- .description('Show all issue types for a project')
149
+ .description('List all issue types (Standard and Subtask) available for a project, providing their name, ID, hierarchy level, and description.')
149
150
  .action(withPermission('list-issue-types', listIssueTypesCommand, {
150
151
  validateArgs: (args) => validateOptions(ProjectKeySchema, args[0])
151
152
  }));
152
153
  // Run JQL command
153
154
  program
154
155
  .command('run-jql <jql-query>')
155
- .description('Execute JQL query and display results')
156
+ .description('Execute a Jira Query Language (JQL) search. Returns a list of issues with their key, summary, status, assignee, and priority. Supports limiting results via --limit (default 50).')
156
157
  .option('-l, --limit <number>', 'Maximum number of results (default: 50)', '50')
157
158
  .action(withPermission('run-jql', runJqlCommand, {
158
159
  schema: RunJqlSchema,
@@ -165,7 +166,7 @@ program
165
166
  // Update description command
166
167
  program
167
168
  .command('update-description <task-id>')
168
- .description('Update task description from a Markdown file')
169
+ .description('Update a Jira task\'s description using content from a local Markdown file. Requires the task ID and a valid file path.')
169
170
  .requiredOption('--from-file <path>', 'Path to Markdown file')
170
171
  .action(withPermission('update-description', updateDescriptionCommand, {
171
172
  schema: UpdateDescriptionSchema,
@@ -174,14 +175,14 @@ program
174
175
  // Add comment command
175
176
  program
176
177
  .command('add-comment')
177
- .description('Add a comment to a Jira issue from a Markdown file')
178
+ .description('Add a new comment to a Jira issue using content from a local Markdown file. Requires the issue key and a valid file path.')
178
179
  .requiredOption('--file-path <path>', 'Path to Markdown file')
179
180
  .requiredOption('--issue-key <key>', 'Jira issue key (e.g., PS-123)')
180
181
  .action(withPermission('add-comment', addCommentCommand, { schema: AddCommentSchema }));
181
182
  // Add label command
182
183
  program
183
184
  .command('add-label-to-issue <task-id> <labels>')
184
- .description('Add one or more labels to a Jira issue (comma-separated)')
185
+ .description('Add one or more labels (comma-separated) to a specific Jira issue.')
185
186
  .action(withPermission('add-label-to-issue', addLabelCommand, {
186
187
  validateArgs: (args) => {
187
188
  validateOptions(IssueKeySchema, args[0]);
@@ -193,7 +194,7 @@ program
193
194
  // Delete label command
194
195
  program
195
196
  .command('delete-label-from-issue <task-id> <labels>')
196
- .description('Remove one or more labels from a Jira issue (comma-separated)')
197
+ .description('Remove one or more labels (comma-separated) from a specific Jira issue.')
197
198
  .action(withPermission('delete-label-from-issue', deleteLabelCommand, {
198
199
  validateArgs: (args) => {
199
200
  validateOptions(IssueKeySchema, args[0]);
@@ -205,7 +206,7 @@ program
205
206
  // Create task command
206
207
  program
207
208
  .command('create-task')
208
- .description('Create a new Jira issue')
209
+ .description('Create a new Jira issue with specified title, project key, and issue type. Optional --parent key for subtasks. Returns the key of the newly created issue.')
209
210
  .requiredOption('--title <title>', 'Issue title/summary')
210
211
  .requiredOption('--project <project>', 'Project key (e.g., PROJ)')
211
212
  .requiredOption('--issue-type <type>', 'Issue type (e.g., Task, Epic, Subtask)')
@@ -214,19 +215,19 @@ program
214
215
  // Transition command
215
216
  program
216
217
  .command('transition <task-id> <to-status>')
217
- .description('Transition a Jira task to a new status')
218
+ .description('Change the status of a Jira task. The <to-status> can be either the status name or ID.')
218
219
  .action(withPermission('transition', transitionCommand, {
219
220
  validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
220
221
  }));
221
222
  // Get issue statistics command
222
223
  program
223
224
  .command('get-issue-statistics <task-ids>')
224
- .description('Show time metrics and lifecycle of issues (comma-separated keys)')
225
+ .description('Calculate and display time-based metrics for one or more issues (comma-separated). Returns a table containing key, summary, total time logged, original estimate, and a detailed breakdown of duration spent in each status.')
225
226
  .action(withPermission('get-issue-statistics', getIssueStatisticsCommand));
226
227
  // Get person worklog command
227
228
  program
228
229
  .command('get-person-worklog <person> <timeframe>')
229
- .description('Retrieve and display worklogs for a specific person within a given timeframe')
230
+ .description('Retrieve worklogs for a specific user over a timeframe (e.g., \'7d\', \'2w\'). Returns a list of entries with date, issue key, summary, time spent, and comments. Supports --group-by-issue.')
230
231
  .option('--group-by-issue', 'Group the output by issue')
231
232
  .action(withPermission('get-person-worklog', getPersonWorklogCommand, {
232
233
  schema: GetPersonWorklogSchema,
@@ -239,6 +240,38 @@ program
239
240
  .command('about')
240
241
  .description('Show information about the tool')
241
242
  .action(aboutCommand);
243
+ // Settings command
244
+ program
245
+ .command('settings')
246
+ .description('View, validate, or apply configuration settings. Use `settings` to view active config, `--validate <file>` to check a YAML file, or `--apply <file>` to update `~/.jira-ai/settings.yaml`.')
247
+ .option('--apply <path>', 'Validate and apply settings from a YAML file')
248
+ .option('--validate <path>', 'Perform schema and deep validation of a settings YAML file')
249
+ .addHelpText('after', `
250
+ Examples:
251
+ $ jira-ai settings
252
+ $ jira-ai settings --validate my-settings.yaml
253
+ $ jira-ai settings --apply my-settings.yaml
254
+
255
+ Settings File Structure:
256
+ projects:
257
+ - all # Allow all projects
258
+ - PROJ # Allow specific project by key
259
+ - key: PM # Project-specific configuration
260
+ commands: # Limit commands for this project
261
+ - task-with-details
262
+ filters:
263
+ participated: # Filter by user participation
264
+ was_assignee: true
265
+ was_reporter: true
266
+ was_commenter: true
267
+ is_watcher: true
268
+ jql: "issuetype = Bug" # Custom JQL filter
269
+ commands:
270
+ - all # Allow all commands globally
271
+ - me
272
+ - projects
273
+ `)
274
+ .action((options) => settingsCommand(options));
242
275
  /**
243
276
  * Configure command visibility based on auth status and settings
244
277
  */
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { markdownToAdf } from 'marklassian';
5
- import { addIssueComment } from '../lib/jira-client.js';
5
+ import { addIssueComment, validateIssuePermissions } from '../lib/jira-client.js';
6
6
  import { CommandError } from '../lib/errors.js';
7
7
  import { ui } from '../lib/ui.js';
8
8
  import { validateOptions, AddCommentSchema } from '../lib/validation.js';
@@ -36,6 +36,9 @@ export async function addCommentCommand(options) {
36
36
  hints: ['Ensure the Markdown content is valid.']
37
37
  });
38
38
  }
39
+ // Check permissions and filters
40
+ ui.startSpinner(`Validating permissions for ${issueKey}...`);
41
+ await validateIssuePermissions(issueKey, 'add-comment');
39
42
  // Add comment with spinner
40
43
  ui.startSpinner(`Adding comment to ${issueKey}...`);
41
44
  try {
@@ -44,6 +47,8 @@ export async function addCommentCommand(options) {
44
47
  console.log(chalk.gray(`\nFile: ${absolutePath}`));
45
48
  }
46
49
  catch (error) {
50
+ if (error instanceof CommandError)
51
+ throw error;
47
52
  const errorMsg = error.message?.toLowerCase() || '';
48
53
  const hints = [];
49
54
  if (errorMsg.includes('404')) {
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { addIssueLabels } from '../lib/jira-client.js';
2
+ import { addIssueLabels, validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { validateOptions, IssueKeySchema } from '../lib/validation.js';
@@ -14,13 +14,19 @@ export async function addLabelCommand(taskId, labelsString) {
14
14
  if (labels.length === 0) {
15
15
  throw new CommandError('No valid labels provided');
16
16
  }
17
+ // Check permissions and filters
18
+ ui.startSpinner(`Validating permissions for ${taskId}...`);
19
+ await validateIssuePermissions(taskId, 'add-label-to-issue');
17
20
  ui.startSpinner(`Adding labels to ${taskId}...`);
18
21
  try {
19
22
  await addIssueLabels(taskId, labels);
20
23
  ui.succeedSpinner(chalk.green(`Labels added successfully to ${taskId}`));
21
- console.log(chalk.gray(`\nLabels: ${labels.join(', ')}`));
24
+ console.log(chalk.gray(`
25
+ Labels: ${labels.join(', ')}`));
22
26
  }
23
27
  catch (error) {
28
+ if (error instanceof CommandError)
29
+ throw error;
24
30
  const errorMsg = error.message?.toLowerCase() || '';
25
31
  const hints = [];
26
32
  if (errorMsg.includes('404')) {
@@ -3,10 +3,19 @@ import { createIssue } from '../lib/jira-client.js';
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { validateOptions, CreateTaskSchema } from '../lib/validation.js';
6
+ import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
6
7
  export async function createTaskCommand(options) {
7
8
  // Validate options
8
9
  validateOptions(CreateTaskSchema, options);
9
10
  const { title, project, issueType, parent } = options;
11
+ // Check if project is allowed
12
+ if (!isProjectAllowed(project)) {
13
+ throw new CommandError(`Project '${project}' is not allowed by your settings.`);
14
+ }
15
+ // Check if command is allowed for this project
16
+ if (!isCommandAllowed('create-task', project)) {
17
+ throw new CommandError(`Command 'create-task' is not allowed for project ${project}.`);
18
+ }
10
19
  // Create issue with spinner
11
20
  ui.startSpinner(`Creating ${issueType} in project ${project}...`);
12
21
  try {
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { removeIssueLabels } from '../lib/jira-client.js';
2
+ import { removeIssueLabels, validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { validateOptions, IssueKeySchema } from '../lib/validation.js';
@@ -14,13 +14,19 @@ export async function deleteLabelCommand(taskId, labelsString) {
14
14
  if (labels.length === 0) {
15
15
  throw new CommandError('No valid labels provided');
16
16
  }
17
+ // Check permissions and filters
18
+ ui.startSpinner(`Validating permissions for ${taskId}...`);
19
+ await validateIssuePermissions(taskId, 'delete-label-from-issue');
17
20
  ui.startSpinner(`Removing labels from ${taskId}...`);
18
21
  try {
19
22
  await removeIssueLabels(taskId, labels);
20
23
  ui.succeedSpinner(chalk.green(`Labels removed successfully from ${taskId}`));
21
- console.log(chalk.gray(`\nLabels: ${labels.join(', ')}`));
24
+ console.log(chalk.gray(`
25
+ Labels: ${labels.join(', ')}`));
22
26
  }
23
27
  catch (error) {
28
+ if (error instanceof CommandError)
29
+ throw error;
24
30
  const errorMsg = error.message?.toLowerCase() || '';
25
31
  const hints = [];
26
32
  if (errorMsg.includes('404')) {
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getIssueStatistics } from '../lib/jira-client.js';
2
+ import { getIssueStatistics, validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { formatIssueStatistics } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  export async function getIssueStatisticsCommand(taskIds) {
@@ -12,11 +12,24 @@ export async function getIssueStatisticsCommand(taskIds) {
12
12
  const results = [];
13
13
  for (const id of ids) {
14
14
  try {
15
+ // Validate permissions for each issue
16
+ await validateIssuePermissions(id, 'get-issue-statistics');
15
17
  const stats = await getIssueStatistics(id);
16
18
  results.push(stats);
17
19
  }
18
20
  catch (error) {
19
- console.error(chalk.red(`\nFailed to fetch statistics for ${id}: ${error.message}`));
21
+ // Skip unauthorized or not found issues, but log a message if not already handled
22
+ if (!(error instanceof Error))
23
+ continue;
24
+ const isPermissionError = error.message.includes('not allowed') || error.message.includes('restricted');
25
+ if (isPermissionError) {
26
+ console.warn(chalk.yellow(`
27
+ Skipping ${id}: ${error.message}`));
28
+ }
29
+ else {
30
+ console.error(chalk.red(`
31
+ Failed to fetch statistics for ${id}: ${error.message}`));
32
+ }
20
33
  }
21
34
  }
22
35
  if (results.length > 0) {
@@ -24,6 +37,6 @@ export async function getIssueStatisticsCommand(taskIds) {
24
37
  console.log(formatIssueStatistics(results));
25
38
  }
26
39
  else {
27
- ui.failSpinner('Failed to retrieve statistics');
40
+ ui.failSpinner('Failed to retrieve statistics or all issues were filtered out');
28
41
  }
29
42
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { ui } from '../lib/ui.js';
3
- import { getIssueWorklogs, getJiraClient } from '../lib/jira-client.js';
3
+ import { searchIssuesByJql, getIssueWorklogs } from '../lib/jira-client.js';
4
4
  import { parseTimeframe, formatDateForJql } from '../lib/utils.js';
5
5
  import { formatWorklogs } from '../lib/formatters.js';
6
6
  import { CommandError } from '../lib/errors.js';
@@ -13,14 +13,7 @@ export async function getPersonWorklogCommand(person, timeframe, options) {
13
13
  // 1. Search for issues where the person has tracked time in the timeframe
14
14
  // We use a broader search first to find relevant issues
15
15
  const jql = `worklogAuthor = "${person}" AND worklogDate >= "${startJql}" AND worklogDate <= "${endJql}"`;
16
- // We need to fetch issues with their summaries
17
- const client = getJiraClient();
18
- const issueResponse = await client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
19
- jql,
20
- fields: ['summary'],
21
- maxResults: 100,
22
- });
23
- const issues = issueResponse.issues || [];
16
+ const issues = await searchIssuesByJql(jql, 100);
24
17
  if (issues.length === 0) {
25
18
  ui.stopSpinner();
26
19
  console.log(chalk.yellow(`
@@ -41,7 +34,7 @@ No worklogs found for ${person} between ${startJql} and ${endJql}.
41
34
  filteredWorklogs.forEach(w => {
42
35
  allWorklogs.push({
43
36
  ...w,
44
- summary: issue.fields?.summary || '',
37
+ summary: issue.summary || '',
45
38
  });
46
39
  });
47
40
  }
@@ -2,7 +2,17 @@ import chalk from 'chalk';
2
2
  import { getUsers } from '../lib/jira-client.js';
3
3
  import { formatUsers } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
+ import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
6
+ import { CommandError } from '../lib/errors.js';
5
7
  export async function listColleaguesCommand(projectKey) {
8
+ if (projectKey) {
9
+ if (!isProjectAllowed(projectKey)) {
10
+ throw new CommandError(`Project '${projectKey}' is not allowed by your settings.`);
11
+ }
12
+ if (!isCommandAllowed('list-colleagues', projectKey)) {
13
+ throw new CommandError(`Command 'list-colleagues' is not allowed for project ${projectKey}.`);
14
+ }
15
+ }
6
16
  const message = projectKey
7
17
  ? `Fetching colleagues for project ${projectKey}...`
8
18
  : 'Fetching all active colleagues...';
@@ -2,7 +2,17 @@ import chalk from 'chalk';
2
2
  import { getProjectIssueTypes } from '../lib/jira-client.js';
3
3
  import { formatProjectIssueTypes } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
+ import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
6
+ import { CommandError } from '../lib/errors.js';
5
7
  export async function listIssueTypesCommand(projectKey) {
8
+ // Check if project is allowed
9
+ if (!isProjectAllowed(projectKey)) {
10
+ throw new CommandError(`Project '${projectKey}' is not allowed by your settings.`);
11
+ }
12
+ // Check if command is allowed for this project
13
+ if (!isCommandAllowed('list-issue-types', projectKey)) {
14
+ throw new CommandError(`Command 'list-issue-types' is not allowed for project ${projectKey}.`);
15
+ }
6
16
  ui.startSpinner(`Fetching issue types for project ${projectKey}...`);
7
17
  const issueTypes = await getProjectIssueTypes(projectKey);
8
18
  ui.succeedSpinner(chalk.green('Issue types retrieved'));
@@ -2,9 +2,25 @@ import chalk from 'chalk';
2
2
  import { getProjectStatuses } from '../lib/jira-client.js';
3
3
  import { formatProjectStatuses } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
- export async function projectStatusesCommand(projectId) {
6
- ui.startSpinner(`Fetching statuses for project ${projectId}...`);
7
- const statuses = await getProjectStatuses(projectId);
8
- ui.succeedSpinner(chalk.green('Project statuses retrieved'));
9
- console.log(formatProjectStatuses(projectId, statuses));
5
+ import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
6
+ import { CommandError } from '../lib/errors.js';
7
+ export async function projectStatusesCommand(projectIdOrKey) {
8
+ // Check if project is allowed
9
+ if (!isProjectAllowed(projectIdOrKey)) {
10
+ throw new CommandError(`Project '${projectIdOrKey}' is not allowed by your settings.`);
11
+ }
12
+ // Check if command is allowed for this project
13
+ if (!isCommandAllowed('project-statuses', projectIdOrKey)) {
14
+ throw new CommandError(`Command 'project-statuses' is not allowed for project ${projectIdOrKey}.`);
15
+ }
16
+ ui.startSpinner(`Fetching statuses for project ${projectIdOrKey}...`);
17
+ try {
18
+ const statuses = await getProjectStatuses(projectIdOrKey);
19
+ ui.succeedSpinner(chalk.green('Project statuses retrieved'));
20
+ console.log(formatProjectStatuses(projectIdOrKey, statuses));
21
+ }
22
+ catch (error) {
23
+ ui.failSpinner(chalk.red('Failed to fetch project statuses'));
24
+ throw error;
25
+ }
10
26
  }
@@ -6,15 +6,17 @@ import { ui } from '../lib/ui.js';
6
6
  export async function projectsCommand() {
7
7
  ui.startSpinner('Fetching projects...');
8
8
  const allProjects = await getProjects();
9
- const allowedProjectKeys = getAllowedProjects();
9
+ const allowedProjects = getAllowedProjects();
10
10
  // Filter projects based on settings
11
- const filteredProjects = allowedProjectKeys.includes('all')
11
+ const hasAll = allowedProjects.some(p => p === 'all');
12
+ const filteredProjects = hasAll
12
13
  ? allProjects
13
14
  : allProjects.filter(project => isProjectAllowed(project.key));
14
15
  ui.succeedSpinner(chalk.green('Projects retrieved'));
15
16
  if (filteredProjects.length === 0) {
16
17
  console.log(chalk.yellow('\nNo projects match your settings configuration.'));
17
- console.log(chalk.gray('Allowed projects: ' + allowedProjectKeys.join(', ')));
18
+ const displayKeys = allowedProjects.map(p => typeof p === 'string' ? p : p.key);
19
+ console.log(chalk.gray('Allowed projects: ' + displayKeys.join(', ')));
18
20
  }
19
21
  else {
20
22
  console.log(formatProjects(filteredProjects));
@@ -0,0 +1,87 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import yaml from 'js-yaml';
4
+ import { loadSettings, saveSettings } from '../lib/settings.js';
5
+ import { formatSettings } from '../lib/formatters.js';
6
+ import { ui } from '../lib/ui.js';
7
+ import { SettingsSchema } from '../lib/validation.js';
8
+ import { getProjects } from '../lib/jira-client.js';
9
+ import { CommandError } from '../lib/errors.js';
10
+ import { validateEnvVars } from '../lib/utils.js';
11
+ export async function settingsCommand(options) {
12
+ if (options.apply) {
13
+ await applySettings(options.apply);
14
+ return;
15
+ }
16
+ if (options.validate) {
17
+ await validateSettingsFile(options.validate);
18
+ return;
19
+ }
20
+ // Default: Show current settings
21
+ const settings = loadSettings();
22
+ console.log(formatSettings(settings));
23
+ }
24
+ async function validateSettingsFile(filePath) {
25
+ ui.startSpinner(`Validating ${filePath}...`);
26
+ if (!fs.existsSync(filePath)) {
27
+ ui.failSpinner(`File not found: ${filePath}`);
28
+ throw new CommandError(`File not found: ${filePath}`);
29
+ }
30
+ let rawSettings;
31
+ try {
32
+ const content = fs.readFileSync(filePath, 'utf8');
33
+ rawSettings = yaml.load(content);
34
+ }
35
+ catch (error) {
36
+ ui.failSpinner(`Error parsing YAML: ${error instanceof Error ? error.message : String(error)}`);
37
+ throw new CommandError(`Error parsing YAML in ${filePath}`);
38
+ }
39
+ // Schema Validation
40
+ const result = SettingsSchema.safeParse(rawSettings);
41
+ if (!result.success) {
42
+ ui.failSpinner('Schema validation failed');
43
+ const messages = result.error.issues
44
+ .map((err) => `${err.path.join('.')}: ${err.message}`)
45
+ .join('\n');
46
+ throw new CommandError(`Invalid settings structure:\n${messages}`);
47
+ }
48
+ const settings = result.data;
49
+ // Deep Validation
50
+ ui.updateSpinner('Performing deep validation against Jira...');
51
+ try {
52
+ validateEnvVars();
53
+ const projects = await getProjects();
54
+ const projectKeys = new Set(projects.map(p => p.key));
55
+ for (const p of settings.projects) {
56
+ const key = typeof p === 'string' ? p : p.key;
57
+ if (key === 'all')
58
+ continue;
59
+ if (!projectKeys.has(key)) {
60
+ ui.failSpinner(`Deep validation failed: Project "${key}" not found in Jira.`);
61
+ throw new CommandError(`Project "${key}" not found in Jira.`);
62
+ }
63
+ // If project has specific commands, we could validate them too,
64
+ // but they are just strings matched against command names.
65
+ }
66
+ ui.succeedSpinner(chalk.green('Settings are valid!'));
67
+ return settings;
68
+ }
69
+ catch (error) {
70
+ if (error instanceof CommandError)
71
+ throw error;
72
+ ui.failSpinner(`Deep validation failed: ${error instanceof Error ? error.message : String(error)}`);
73
+ throw new CommandError(`Failed to connect to Jira for validation: ${error instanceof Error ? error.message : String(error)}`);
74
+ }
75
+ }
76
+ async function applySettings(filePath) {
77
+ const settings = await validateSettingsFile(filePath);
78
+ ui.startSpinner('Applying settings...');
79
+ try {
80
+ saveSettings(settings);
81
+ ui.succeedSpinner(chalk.green('Settings applied successfully!'));
82
+ }
83
+ catch (error) {
84
+ ui.failSpinner(`Error applying settings: ${error instanceof Error ? error.message : String(error)}`);
85
+ throw error;
86
+ }
87
+ }
@@ -1,8 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { getTaskWithDetails } from '../lib/jira-client.js';
2
+ import { getTaskWithDetails, getCurrentUser } from '../lib/jira-client.js';
3
3
  import { formatTaskDetails } from '../lib/formatters.js';
4
4
  import { CommandError } from '../lib/errors.js';
5
5
  import { ui } from '../lib/ui.js';
6
+ import { isCommandAllowed, validateIssueAgainstFilters } from '../lib/settings.js';
6
7
  export async function taskWithDetailsCommand(taskId, options = {}) {
7
8
  ui.startSpinner(`Fetching details for ${taskId}...`);
8
9
  try {
@@ -11,10 +12,32 @@ export async function taskWithDetailsCommand(taskId, options = {}) {
11
12
  historyLimit: options.historyLimit ? parseInt(options.historyLimit, 10) : undefined,
12
13
  historyOffset: options.historyOffset ? parseInt(options.historyOffset, 10) : undefined,
13
14
  });
15
+ const projectKey = task.key.split('-')[0];
16
+ // Check if command is allowed for this project
17
+ if (!isCommandAllowed('task-with-details', projectKey)) {
18
+ ui.failSpinner(chalk.red('Command not allowed for this project'));
19
+ throw new CommandError(`Command 'task-with-details' is not allowed for project ${projectKey}.`, {
20
+ hints: [
21
+ `Update settings.yaml to enable this command for this project.`
22
+ ]
23
+ });
24
+ }
25
+ // Check granular filters
26
+ const currentUser = await getCurrentUser();
27
+ if (!validateIssueAgainstFilters(task, currentUser.accountId)) {
28
+ ui.failSpinner(chalk.red('Access denied by project filters'));
29
+ throw new CommandError(`Access to issue ${taskId} is restricted by project filters.`, {
30
+ hints: [
31
+ `This project has filters that you do not meet (e.g., participated roles).`
32
+ ]
33
+ });
34
+ }
14
35
  ui.succeedSpinner(chalk.green('Task details retrieved'));
15
36
  console.log(formatTaskDetails(task));
16
37
  }
17
38
  catch (error) {
39
+ if (error instanceof CommandError)
40
+ throw error;
18
41
  const errorMsg = error.message?.toLowerCase() || '';
19
42
  const hints = [];
20
43
  if (error.response?.status === 404 || errorMsg.includes('404')) {
@@ -1,8 +1,11 @@
1
1
  import chalk from 'chalk';
2
- import { getIssueTransitions, transitionIssue } from '../lib/jira-client.js';
2
+ import { getIssueTransitions, transitionIssue, validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  export async function transitionCommand(taskId, toStatus) {
6
+ // Check permissions and filters
7
+ ui.startSpinner(`Validating permissions for ${taskId}...`);
8
+ await validateIssuePermissions(taskId, 'transition');
6
9
  ui.startSpinner(`Fetching available transitions for ${taskId}...`);
7
10
  try {
8
11
  const transitions = await getIssueTransitions(taskId);
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { markdownToAdf } from 'marklassian';
5
- import { updateIssueDescription } from '../lib/jira-client.js';
5
+ import { updateIssueDescription, validateIssuePermissions } from '../lib/jira-client.js';
6
6
  import { CommandError } from '../lib/errors.js';
7
7
  import { ui } from '../lib/ui.js';
8
8
  import { validateOptions, UpdateDescriptionSchema, IssueKeySchema } from '../lib/validation.js';
@@ -38,14 +38,20 @@ export async function updateDescriptionCommand(taskId, options) {
38
38
  hints: ['Ensure the Markdown content is valid.']
39
39
  });
40
40
  }
41
+ // Check permissions and filters
42
+ ui.startSpinner(`Validating permissions for ${taskId}...`);
43
+ await validateIssuePermissions(taskId, 'update-description');
41
44
  // Update issue description with spinner
42
45
  ui.startSpinner(`Updating description for ${taskId}...`);
43
46
  try {
44
47
  await updateIssueDescription(taskId, adfContent);
45
48
  ui.succeedSpinner(chalk.green(`Description updated successfully for ${taskId}`));
46
- console.log(chalk.gray(`\nFile: ${absolutePath}`));
49
+ console.log(chalk.gray(`
50
+ File: ${absolutePath}`));
47
51
  }
48
52
  catch (error) {
53
+ if (error instanceof CommandError)
54
+ throw error;
49
55
  const errorMsg = error.message?.toLowerCase() || '';
50
56
  const hints = [];
51
57
  if (errorMsg.includes('404')) {
@@ -360,3 +360,57 @@ export function formatWorklogs(worklogs, groupByIssue = false) {
360
360
  output += chalk.bold(`Total time tracked: ${totalHours}h`) + '\n';
361
361
  return output;
362
362
  }
363
+ /**
364
+ * Format settings
365
+ */
366
+ export function formatSettings(settings) {
367
+ let output = '\n' + chalk.bold.cyan('Active Configuration') + '\n\n';
368
+ // Global Commands
369
+ output += chalk.bold('Global Commands:') + '\n';
370
+ output += ` ${settings.commands.join(', ')}\n\n`;
371
+ // Projects
372
+ output += chalk.bold(`Projects (${settings.projects.length}):`) + '\n';
373
+ const table = createTable(['Project', 'Commands', 'Filters'], [15, 30, 50]);
374
+ settings.projects.forEach((p) => {
375
+ let key;
376
+ let commands = 'global';
377
+ let filters = 'none';
378
+ if (typeof p === 'string') {
379
+ key = p;
380
+ }
381
+ else {
382
+ key = p.key;
383
+ if (p.commands) {
384
+ commands = p.commands.join(', ');
385
+ }
386
+ if (p.filters) {
387
+ const parts = [];
388
+ if (p.filters.jql) {
389
+ parts.push(`JQL: ${p.filters.jql}`);
390
+ }
391
+ if (p.filters.participated) {
392
+ const roles = [];
393
+ if (p.filters.participated.was_assignee)
394
+ roles.push('Assignee');
395
+ if (p.filters.participated.was_reporter)
396
+ roles.push('Reporter');
397
+ if (p.filters.participated.was_commenter)
398
+ roles.push('Commenter');
399
+ if (p.filters.participated.is_watcher)
400
+ roles.push('Watcher');
401
+ if (roles.length > 0) {
402
+ parts.push(`Roles: ${roles.join(', ')}`);
403
+ }
404
+ }
405
+ filters = parts.join('\n') || 'none';
406
+ }
407
+ }
408
+ table.push([
409
+ chalk.cyan(key),
410
+ commands,
411
+ filters
412
+ ]);
413
+ });
414
+ output += table.toString() + '\n';
415
+ return output;
416
+ }
@@ -1,6 +1,8 @@
1
1
  import { Version3Client } from 'jira.js';
2
2
  import { calculateStatusStatistics, convertADFToMarkdown } from './utils.js';
3
3
  import { loadCredentials } from './auth-storage.js';
4
+ import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAgainstFilters } from './settings.js';
5
+ import { CommandError } from './errors.js';
4
6
  let jiraClient = null;
5
7
  let organizationOverride = undefined;
6
8
  /**
@@ -125,12 +127,14 @@ export async function getTaskWithDetails(taskId, options = {}) {
125
127
  'parent',
126
128
  'subtasks',
127
129
  'labels',
130
+ 'watches',
128
131
  ],
129
132
  });
130
133
  // Extract comments
131
134
  const comments = issue.fields.comment?.comments?.map((comment) => ({
132
135
  id: comment.id,
133
136
  author: {
137
+ accountId: comment.author?.accountId || '',
134
138
  displayName: comment.author?.displayName || 'Unknown',
135
139
  emailAddress: comment.author?.emailAddress,
136
140
  },
@@ -186,6 +190,14 @@ export async function getTaskWithDetails(taskId, options = {}) {
186
190
  // Apply offset and limit
187
191
  history = history.slice(historyOffset, historyOffset + historyLimit);
188
192
  }
193
+ // Extract watchers
194
+ const watchers = [];
195
+ if (issue.fields.watches?.isWatching) {
196
+ // If we only need to know if the current user is watching, isWatching is enough.
197
+ // But the requirement might mean "any of these roles".
198
+ // For now, if isWatching is true, we add current user's accountId (placeholder)
199
+ // Actually, we can fetch watchers detail if needed, but Jira's getIssue returns watches info for current user.
200
+ }
189
201
  return {
190
202
  id: issue.id || '',
191
203
  key: issue.key || '',
@@ -196,9 +208,11 @@ export async function getTaskWithDetails(taskId, options = {}) {
196
208
  category: issue.fields.status?.statusCategory?.key || 'unknown',
197
209
  },
198
210
  assignee: issue.fields.assignee ? {
211
+ accountId: issue.fields.assignee.accountId || '',
199
212
  displayName: issue.fields.assignee.displayName || 'Unknown',
200
213
  } : undefined,
201
214
  reporter: issue.fields.reporter ? {
215
+ accountId: issue.fields.reporter.accountId || '',
202
216
  displayName: issue.fields.reporter.displayName || 'Unknown',
203
217
  } : undefined,
204
218
  created: issue.fields.created || '',
@@ -209,6 +223,7 @@ export async function getTaskWithDetails(taskId, options = {}) {
209
223
  parent,
210
224
  subtasks,
211
225
  history,
226
+ watchers: issue.fields.watches?.isWatching ? ['CURRENT_USER'] : [], // Simple flag for now
212
227
  };
213
228
  }
214
229
  /**
@@ -245,8 +260,9 @@ export async function getProjectStatuses(projectIdOrKey) {
245
260
  */
246
261
  export async function searchIssuesByJql(jqlQuery, maxResults) {
247
262
  const client = getJiraClient();
263
+ const filteredJql = applyGlobalFilters(jqlQuery);
248
264
  const response = await client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
249
- jql: jqlQuery,
265
+ jql: filteredJql,
250
266
  maxResults,
251
267
  fields: ['summary', 'status', 'assignee', 'priority'],
252
268
  });
@@ -335,13 +351,33 @@ export async function createIssue(projectKey, summary, issueTypeName, parentKey)
335
351
  const response = await client.issues.createIssue({
336
352
  fields,
337
353
  });
338
- return {
339
- key: response.key || '',
340
- id: response.id || '',
341
- };
354
+ return { key: response.key || '', id: response.id || '' };
355
+ }
356
+ /**
357
+ * Validate that the current user has permission to perform a command on an issue
358
+ */
359
+ export async function validateIssuePermissions(issueKey, commandName) {
360
+ const task = await getTaskWithDetails(issueKey);
361
+ const projectKey = task.key.split('-')[0];
362
+ if (!isProjectAllowed(projectKey)) {
363
+ throw new CommandError(`Project '${projectKey}' is not allowed by your settings.`);
364
+ }
365
+ if (!isCommandAllowed(commandName, projectKey)) {
366
+ throw new CommandError(`Command '${commandName}' is not allowed for project ${projectKey}.`, {
367
+ hints: [`Update settings.yaml to enable this command for this project.`]
368
+ });
369
+ }
370
+ const currentUser = await getCurrentUser();
371
+ if (!validateIssueAgainstFilters(task, currentUser.accountId)) {
372
+ throw new CommandError(`Access to issue ${issueKey} is restricted by project filters.`, {
373
+ hints: [`This project has filters that you do not meet (e.g., participated roles).`]
374
+ });
375
+ }
376
+ return task;
342
377
  }
343
378
  /**
344
379
  * Add labels to a Jira issue
380
+
345
381
  * @param taskId - The issue key (e.g., "PROJ-123")
346
382
  * @param labels - Array of labels to add
347
383
  */
@@ -4,6 +4,7 @@ import os from 'os';
4
4
  import yaml from 'js-yaml';
5
5
  import chalk from 'chalk';
6
6
  import { CliError } from '../types/errors.js';
7
+ import { SettingsSchema } from './validation.js';
7
8
  const CONFIG_DIR = path.join(os.homedir(), '.jira-ai');
8
9
  const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.yaml');
9
10
  let cachedSettings = null;
@@ -31,7 +32,7 @@ export function loadSettings() {
31
32
  console.error('Error migrating settings.yaml:', error);
32
33
  const defaultSettings = {
33
34
  projects: ['all'],
34
- commands: ['all']
35
+ commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
35
36
  };
36
37
  cachedSettings = defaultSettings;
37
38
  return cachedSettings;
@@ -41,7 +42,7 @@ export function loadSettings() {
41
42
  // Create default settings.yaml if it doesn't exist anywhere
42
43
  const defaultSettings = {
43
44
  projects: ['all'],
44
- commands: ['all']
45
+ commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
45
46
  };
46
47
  try {
47
48
  const yamlStr = yaml.dump(defaultSettings);
@@ -56,26 +57,77 @@ export function loadSettings() {
56
57
  }
57
58
  try {
58
59
  const fileContents = fs.readFileSync(SETTINGS_FILE, 'utf8');
59
- const settings = yaml.load(fileContents);
60
- cachedSettings = {
61
- projects: settings.projects || ['all'],
62
- commands: settings.commands || ['all']
63
- };
60
+ const rawSettings = yaml.load(fileContents);
61
+ const result = SettingsSchema.safeParse(rawSettings);
62
+ if (!result.success) {
63
+ console.warn(chalk.yellow(`Warning: ${SETTINGS_FILE} has validation errors:`));
64
+ result.error.issues.forEach(issue => {
65
+ console.warn(chalk.yellow(` - ${issue.path.join('.')}: ${issue.message}`));
66
+ });
67
+ // Fallback to raw settings or default if parsing fails completely
68
+ const settings = rawSettings;
69
+ cachedSettings = {
70
+ projects: settings?.projects || ['all'],
71
+ commands: settings?.commands || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
72
+ };
73
+ }
74
+ else {
75
+ cachedSettings = result.data;
76
+ }
64
77
  return cachedSettings;
65
78
  }
66
79
  catch (error) {
67
80
  throw new CliError(`Error loading ${SETTINGS_FILE}: ${error instanceof Error ? error.message : String(error)}`);
68
81
  }
69
82
  }
83
+ export function saveSettings(settings) {
84
+ // Ensure config directory exists
85
+ if (!fs.existsSync(CONFIG_DIR)) {
86
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
87
+ }
88
+ try {
89
+ const yamlStr = yaml.dump(settings);
90
+ fs.writeFileSync(SETTINGS_FILE, yamlStr);
91
+ cachedSettings = settings;
92
+ }
93
+ catch (error) {
94
+ throw new CliError(`Error saving ${SETTINGS_FILE}: ${error instanceof Error ? error.message : String(error)}`);
95
+ }
96
+ }
70
97
  export function isProjectAllowed(projectKey) {
71
98
  const settings = loadSettings();
72
- if (settings.projects.includes('all')) {
73
- return true;
74
- }
75
- return settings.projects.includes(projectKey);
99
+ const isAllowed = settings.projects.some(p => {
100
+ if (typeof p === 'string') {
101
+ return p === 'all' || p === projectKey;
102
+ }
103
+ return p.key === projectKey;
104
+ });
105
+ return isAllowed;
76
106
  }
77
- export function isCommandAllowed(commandName) {
107
+ export function isCommandAllowed(commandName, projectKey) {
78
108
  const settings = loadSettings();
109
+ // about, auth, and settings are always allowed
110
+ if (['about', 'auth', 'settings'].includes(commandName)) {
111
+ return true;
112
+ }
113
+ if (projectKey) {
114
+ const project = settings.projects.find(p => (typeof p === 'string' ? p : p.key) === projectKey);
115
+ if (project && typeof project !== 'string' && project.commands) {
116
+ return project.commands.includes(commandName);
117
+ }
118
+ }
119
+ else {
120
+ // For visibility/global check: allowed if in global list OR in any project-specific list
121
+ const allowedGlobally = settings.commands.includes('all') || settings.commands.includes(commandName);
122
+ if (allowedGlobally) {
123
+ return true;
124
+ }
125
+ const allowedInAnyProject = settings.projects.some(p => typeof p !== 'string' && p.commands && p.commands.includes(commandName));
126
+ if (allowedInAnyProject) {
127
+ return true;
128
+ }
129
+ return false;
130
+ }
79
131
  if (settings.commands.includes('all')) {
80
132
  return true;
81
133
  }
@@ -89,6 +141,57 @@ export function getAllowedCommands() {
89
141
  const settings = loadSettings();
90
142
  return settings.commands;
91
143
  }
144
+ export function applyGlobalFilters(jql) {
145
+ const settings = loadSettings();
146
+ const allAllowed = settings.projects.some(p => p === 'all');
147
+ if (allAllowed) {
148
+ return jql;
149
+ }
150
+ const projectFilters = settings.projects.map(p => {
151
+ const key = typeof p === 'string' ? p : p.key;
152
+ const projectJql = typeof p === 'string' ? null : p.filters?.jql;
153
+ if (projectJql) {
154
+ return `(project = "${key}" AND (${projectJql}))`;
155
+ }
156
+ else {
157
+ return `project = "${key}"`;
158
+ }
159
+ });
160
+ if (projectFilters.length === 0) {
161
+ return `project = "NONE" AND (${jql})`;
162
+ }
163
+ const combinedProjectFilter = `(${projectFilters.join(' OR ')})`;
164
+ return `(${combinedProjectFilter}) AND (${jql})`;
165
+ }
166
+ export function validateIssueAgainstFilters(issue, currentUserId) {
167
+ const settings = loadSettings();
168
+ const projectKey = issue.key.split('-')[0];
169
+ const project = settings.projects.find(p => {
170
+ if (typeof p === 'string')
171
+ return p === 'all' || p === projectKey;
172
+ return p.key === projectKey;
173
+ });
174
+ if (!project) {
175
+ return false;
176
+ }
177
+ if (typeof project === 'string')
178
+ return true;
179
+ if (project.filters?.participated) {
180
+ const { participated } = project.filters;
181
+ let hasParticipated = false;
182
+ if (participated.was_assignee && issue.assignee?.accountId === currentUserId)
183
+ hasParticipated = true;
184
+ if (participated.was_reporter && issue.reporter?.accountId === currentUserId)
185
+ hasParticipated = true;
186
+ if (participated.was_commenter && issue.comments?.some((c) => c.author?.accountId === currentUserId))
187
+ hasParticipated = true;
188
+ if (participated.is_watcher && issue.watchers?.includes('CURRENT_USER'))
189
+ hasParticipated = true;
190
+ if (!hasParticipated)
191
+ return false;
192
+ }
193
+ return true;
194
+ }
92
195
  // For testing purposes only
93
196
  export function __resetCache__() {
94
197
  cachedSettings = null;
package/dist/lib/ui.js CHANGED
@@ -26,6 +26,11 @@ class UI {
26
26
  this.spinnerInstance = null;
27
27
  }
28
28
  }
29
+ updateSpinner(message) {
30
+ if (this.spinnerInstance) {
31
+ this.spinnerInstance.text = message;
32
+ }
33
+ }
29
34
  get spinner() {
30
35
  return this.spinnerInstance;
31
36
  }
@@ -72,3 +72,25 @@ export const TimeframeSchema = z.string().regex(/^\d+d$/, 'Timeframe must be in
72
72
  export const GetPersonWorklogSchema = z.object({
73
73
  groupByIssue: z.boolean().optional(),
74
74
  });
75
+ export const ProjectFiltersSchema = z.object({
76
+ participated: z.object({
77
+ was_assignee: z.boolean().optional(),
78
+ was_reporter: z.boolean().optional(),
79
+ was_commenter: z.boolean().optional(),
80
+ is_watcher: z.boolean().optional(),
81
+ }).optional(),
82
+ jql: z.string().optional(),
83
+ });
84
+ export const ProjectConfigSchema = z.object({
85
+ key: z.string().trim().min(1),
86
+ commands: z.array(z.string()).optional(),
87
+ filters: ProjectFiltersSchema.optional(),
88
+ });
89
+ export const ProjectSettingSchema = z.union([
90
+ z.string().trim().min(1),
91
+ ProjectConfigSchema
92
+ ]);
93
+ export const SettingsSchema = z.object({
94
+ projects: z.array(ProjectSettingSchema).nullish().transform(val => val || ['all']),
95
+ commands: z.array(z.string()).nullish().transform(val => val || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']),
96
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.4.5",
3
+ "version": "0.4.10",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
package/settings.yaml CHANGED
@@ -4,23 +4,13 @@
4
4
  # Examples: BP, PM, PS
5
5
  projects:
6
6
  - all
7
- #- BP
8
- #- PM
9
- #- PS
10
- # Uncomment below to allow all projects
11
- # - all
12
7
 
13
8
  # Commands: List of allowed commands (use "all" to allow all commands)
14
9
  # Available commands: me, projects, task-with-details, project-statuses, list-issue-types, run-jql, update-description, add-comment, create-task, get-issue-statistics, about, transition, add-label-to-issue, delete-label-from-issue, list-colleagues, get-person-worklog
15
10
  commands:
16
11
  - me
17
- - get-issue-statistics
18
- - get-person-worklog
19
- # Uncomment below to allow all commands
20
- # - all
21
- # - task-with-details
22
- # - project-statuses
23
- # - list-issue-types
24
- # - update-description
25
- # - add-comment
26
- # - create-task
12
+ - projects
13
+ - task-with-details
14
+ - run-jql
15
+ - list-issue-types
16
+ - project-statuses