jira-ai 0.6.3 → 0.6.4

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.
@@ -1,15 +1,32 @@
1
1
  import chalk from 'chalk';
2
- import { getPage, getPageComments } from '../lib/confluence-client.js';
2
+ import { getPage, getPageComments, parseConfluenceUrl } from '../lib/confluence-client.js';
3
3
  import { formatConfluencePage } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { CommandError } from '../lib/errors.js';
6
+ import { isConfluenceSpaceAllowed } from '../lib/settings.js';
6
7
  export async function confluenceGetPageCommand(url) {
8
+ // Check permission before fetching if space key can be extracted from URL
9
+ try {
10
+ const { spaceKey } = parseConfluenceUrl(url);
11
+ if (spaceKey && !isConfluenceSpaceAllowed(spaceKey)) {
12
+ throw new CommandError(`Access to Confluence space '${spaceKey}' is restricted by your settings.`);
13
+ }
14
+ }
15
+ catch (e) {
16
+ if (e instanceof CommandError)
17
+ throw e;
18
+ // If URL parsing fails, let the fetch attempt handle it or catch it later
19
+ }
7
20
  ui.startSpinner(`Fetching Confluence page details for: ${url}`);
8
21
  try {
9
22
  const [page, comments] = await Promise.all([
10
23
  getPage(url),
11
24
  getPageComments(url)
12
25
  ]);
26
+ // Double check with the actual space name/key from the fetched page
27
+ if (!isConfluenceSpaceAllowed(page.space)) {
28
+ throw new CommandError(`Access to Confluence space '${page.space}' is restricted by your settings.`);
29
+ }
13
30
  ui.succeedSpinner(chalk.green('Confluence page details retrieved'));
14
31
  console.log(formatConfluencePage(page, comments));
15
32
  }
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import fs from 'fs';
3
3
  import yaml from 'js-yaml';
4
- import { loadSettings, saveSettings, DEFAULT_SETTINGS } from '../lib/settings.js';
4
+ import { loadSettings, saveSettings, DEFAULT_SETTINGS, migrateSettings } from '../lib/settings.js';
5
5
  import { formatSettings } from '../lib/formatters.js';
6
6
  import { ui } from '../lib/ui.js';
7
7
  import { SettingsSchema } from '../lib/validation.js';
@@ -57,23 +57,33 @@ async function validateSettingsFile(filePath) {
57
57
  .join('\n');
58
58
  throw new CommandError(`Invalid settings structure:\n${messages}`);
59
59
  }
60
- const settings = result.data;
60
+ const settings = migrateSettings(result.data);
61
61
  // Deep Validation
62
62
  ui.updateSpinner('Performing deep validation against Jira...');
63
63
  try {
64
64
  validateEnvVars();
65
65
  const projects = await getProjects();
66
66
  const projectKeys = new Set(projects.map(p => p.key));
67
- for (const p of settings.projects) {
68
- const key = typeof p === 'string' ? p : p.key;
69
- if (key === 'all')
70
- continue;
71
- if (!projectKeys.has(key)) {
72
- ui.failSpinner(`Deep validation failed: Project "${key}" not found in Jira.`);
73
- throw new CommandError(`Project "${key}" not found in Jira.`);
67
+ const validateOrg = (orgSettings, label) => {
68
+ const projectsToValidate = orgSettings['allowed-jira-projects'] || [];
69
+ for (const p of projectsToValidate) {
70
+ const key = typeof p === 'string' ? p : p.key;
71
+ if (key === 'all')
72
+ continue;
73
+ if (!projectKeys.has(key)) {
74
+ const msg = `Project "${key}" (in ${label}) not found in Jira.`;
75
+ ui.failSpinner(`Deep validation failed: ${msg}`);
76
+ throw new CommandError(msg);
77
+ }
78
+ }
79
+ };
80
+ if (settings.defaults) {
81
+ validateOrg(settings.defaults, 'defaults');
82
+ }
83
+ if (settings.organizations) {
84
+ for (const [alias, orgSettings] of Object.entries(settings.organizations)) {
85
+ validateOrg(orgSettings, `organizations.${alias}`);
74
86
  }
75
- // If project has specific commands, we could validate them too,
76
- // but they are just strings matched against command names.
77
87
  }
78
88
  ui.succeedSpinner(chalk.green('Settings are valid!'));
79
89
  return settings;
@@ -137,10 +137,24 @@ export function getOrganizations() {
137
137
  const config = loadConfig();
138
138
  return config.organizations;
139
139
  }
140
+ let organizationOverride = undefined;
140
141
  /**
141
- * Get the currently active organization alias
142
+
143
+ * Set a global organization override for the current execution
144
+
145
+ */
146
+ export function setOrganizationOverride(alias) {
147
+ organizationOverride = alias;
148
+ }
149
+ /**
150
+
151
+ * Get the currently active organization alias, respecting override
152
+
142
153
  */
143
154
  export function getCurrentOrganizationAlias() {
155
+ if (organizationOverride) {
156
+ return organizationOverride;
157
+ }
144
158
  const config = loadConfig();
145
159
  return config.current;
146
160
  }
@@ -1,13 +1,12 @@
1
1
  import { ConfluenceClient } from 'confluence.js';
2
- import { loadCredentials } from './auth-storage.js';
2
+ import { loadCredentials, getCurrentOrganizationAlias, setOrganizationOverride as setAuthOrgOverride } from './auth-storage.js';
3
3
  import { convertADFToMarkdown } from './utils.js';
4
4
  let confluenceClient = null;
5
- let organizationOverride = undefined;
6
5
  /**
7
6
  * Set a global organization override for the current execution
8
7
  */
9
8
  export function setOrganizationOverride(alias) {
10
- organizationOverride = alias;
9
+ setAuthOrgOverride(alias);
11
10
  confluenceClient = null; // Force client recreation
12
11
  }
13
12
  /**
@@ -30,7 +29,8 @@ export function getConfluenceClient() {
30
29
  });
31
30
  }
32
31
  else {
33
- const storedCreds = loadCredentials(organizationOverride);
32
+ const alias = getCurrentOrganizationAlias();
33
+ const storedCreds = loadCredentials(alias);
34
34
  if (storedCreds) {
35
35
  confluenceClient = new ConfluenceClient({
36
36
  host: storedCreds.host.replace(/\/$/, ''),
@@ -43,8 +43,8 @@ export function getConfluenceClient() {
43
43
  });
44
44
  }
45
45
  else {
46
- const errorMsg = organizationOverride
47
- ? `Credentials for organization "${organizationOverride}" not found.`
46
+ const errorMsg = alias
47
+ ? `Credentials for organization "${alias}" not found.`
48
48
  : 'Credentials not found. Please set environment variables or run "jira-ai auth"';
49
49
  throw new Error(errorMsg);
50
50
  }
@@ -53,20 +53,22 @@ export function getConfluenceClient() {
53
53
  return confluenceClient;
54
54
  }
55
55
  /**
56
- * Parse Confluence URL to extract page ID
56
+ * Parse Confluence URL to extract page ID and space key
57
57
  */
58
58
  export function parseConfluenceUrl(url) {
59
59
  try {
60
60
  const parsedUrl = new URL(url);
61
61
  // Pattern: /wiki/spaces/SPACE/pages/PAGE_ID/TITLE
62
+ const spaceMatch = parsedUrl.pathname.match(/\/spaces\/([^/]+)/);
63
+ const spaceKey = spaceMatch ? spaceMatch[1] : undefined;
62
64
  const pagesMatch = parsedUrl.pathname.match(/\/pages\/(\d+)/);
63
65
  if (pagesMatch && pagesMatch[1]) {
64
- return pagesMatch[1];
66
+ return { pageId: pagesMatch[1], spaceKey };
65
67
  }
66
68
  // Pattern: /wiki/pages/viewpage.action?pageId=PAGE_ID
67
69
  const pageIdParam = parsedUrl.searchParams.get('pageId');
68
70
  if (pageIdParam) {
69
- return pageIdParam;
71
+ return { pageId: pageIdParam, spaceKey };
70
72
  }
71
73
  throw new Error('Could not extract Page ID from URL');
72
74
  }
@@ -82,7 +84,7 @@ export function parseConfluenceUrl(url) {
82
84
  */
83
85
  export async function getPage(url) {
84
86
  const client = getConfluenceClient();
85
- const pageId = parseConfluenceUrl(url);
87
+ const { pageId } = parseConfluenceUrl(url);
86
88
  const page = await client.content.getContentById({
87
89
  id: pageId,
88
90
  expand: ['body.atlas_doc_format', 'version', 'space', 'history.lastUpdated'],
@@ -108,7 +110,7 @@ export async function getPage(url) {
108
110
  */
109
111
  export async function getPageComments(url) {
110
112
  const client = getConfluenceClient();
111
- const pageId = parseConfluenceUrl(url);
113
+ const { pageId } = parseConfluenceUrl(url);
112
114
  const response = await client.contentChildrenAndDescendants.getContentChildrenByType({
113
115
  id: pageId,
114
116
  type: 'comment',
@@ -461,17 +461,18 @@ export function formatWorklogs(worklogs, groupByIssue = false) {
461
461
  return output;
462
462
  }
463
463
  /**
464
- * Format settings
464
+ * Format organization settings
465
465
  */
466
- export function formatSettings(settings) {
467
- let output = '\n' + chalk.bold.cyan('Active Configuration') + '\n\n';
468
- // Global Commands
469
- output += chalk.bold('Global Commands:') + '\n';
470
- output += ` ${settings.commands.join(', ')}\n\n`;
471
- // Projects
472
- output += chalk.bold(`Projects (${settings.projects.length}):`) + '\n';
466
+ function formatOrgSettings(settings, title) {
467
+ let output = chalk.bold(`${title}:`) + '\n';
468
+ const commands = settings['allowed-commands'] || [];
469
+ output += ` ${chalk.bold('Allowed Commands:')} ${commands.join(', ')}\n`;
470
+ const spaces = settings['allowed-confluence-spaces'] || [];
471
+ output += ` ${chalk.bold('Allowed Confluence Spaces:')} ${spaces.join(', ')}\n\n`;
472
+ const projects = settings['allowed-jira-projects'] || [];
473
+ output += ` ${chalk.bold(`Allowed Jira Projects (${projects.length}):`)}\n`;
473
474
  const table = createTable(['Project', 'Commands', 'Filters'], [15, 30, 50]);
474
- settings.projects.forEach((p) => {
475
+ projects.forEach((p) => {
475
476
  let key;
476
477
  let commands = 'global';
477
478
  let filters = 'none';
@@ -511,7 +512,23 @@ export function formatSettings(settings) {
511
512
  filters
512
513
  ]);
513
514
  });
514
- output += table.toString() + '\n';
515
+ output += table.toString().split('\n').map(line => ' ' + line).join('\n') + '\n\n';
516
+ return output;
517
+ }
518
+ /**
519
+ * Format settings
520
+ */
521
+ export function formatSettings(settings) {
522
+ let output = '\n' + chalk.bold.cyan('Active Configuration') + '\n\n';
523
+ if (settings.defaults) {
524
+ output += formatOrgSettings(settings.defaults, 'Default Settings');
525
+ }
526
+ if (settings.organizations && Object.keys(settings.organizations).length > 0) {
527
+ output += chalk.bold.blue('Organization-Specific Settings:') + '\n\n';
528
+ for (const [alias, orgSettings] of Object.entries(settings.organizations)) {
529
+ output += formatOrgSettings(orgSettings, `Organization: ${alias}`);
530
+ }
531
+ }
515
532
  return output;
516
533
  }
517
534
  /**
@@ -1,15 +1,14 @@
1
1
  import { Version3Client } from 'jira.js';
2
2
  import { calculateStatusStatistics, convertADFToMarkdown } from './utils.js';
3
- import { loadCredentials } from './auth-storage.js';
4
- import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAgainstFilters, loadSettings } from './settings.js';
3
+ import { loadCredentials, getCurrentOrganizationAlias, setOrganizationOverride as setAuthOrgOverride } from './auth-storage.js';
4
+ import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAgainstFilters, getAllowedProjects } from './settings.js';
5
5
  import { CommandError } from './errors.js';
6
6
  let jiraClient = null;
7
- let organizationOverride = undefined;
8
7
  /**
9
8
  * Set a global organization override for the current execution
10
9
  */
11
10
  export function setOrganizationOverride(alias) {
12
- organizationOverride = alias;
11
+ setAuthOrgOverride(alias);
13
12
  jiraClient = null; // Force client recreation
14
13
  }
15
14
  /**
@@ -32,7 +31,8 @@ export function getJiraClient() {
32
31
  });
33
32
  }
34
33
  else {
35
- const storedCreds = loadCredentials(organizationOverride);
34
+ const alias = getCurrentOrganizationAlias();
35
+ const storedCreds = loadCredentials(alias);
36
36
  if (storedCreds) {
37
37
  jiraClient = new Version3Client({
38
38
  host: storedCreds.host,
@@ -45,8 +45,8 @@ export function getJiraClient() {
45
45
  });
46
46
  }
47
47
  else {
48
- const errorMsg = organizationOverride
49
- ? `Jira credentials for organization "${organizationOverride}" not found.`
48
+ const errorMsg = alias
49
+ ? `Jira credentials for organization "${alias}" not found.`
50
50
  : 'Jira credentials not found. Please set environment variables or run "jira-ai auth"';
51
51
  throw new Error(errorMsg);
52
52
  }
@@ -374,10 +374,10 @@ export async function validateIssuePermissions(issueKey, commandName, options =
374
374
  });
375
375
  }
376
376
  // Check JQL filters
377
- const settings = loadSettings();
378
- let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
377
+ const allowedProjects = getAllowedProjects();
378
+ let project = allowedProjects.find(p => typeof p !== 'string' && p.key === projectKey);
379
379
  if (!project) {
380
- project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
380
+ project = allowedProjects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
381
381
  }
382
382
  if (project && typeof project !== 'string' && project.filters?.jql) {
383
383
  const client = getJiraClient();
@@ -5,9 +5,10 @@ import yaml from 'js-yaml';
5
5
  import chalk from 'chalk';
6
6
  import { CliError } from '../types/errors.js';
7
7
  import { SettingsSchema } from './validation.js';
8
- export const DEFAULT_SETTINGS = {
9
- projects: ['all'],
10
- commands: [
8
+ import { getCurrentOrganizationAlias } from './auth-storage.js';
9
+ export const DEFAULT_ORG_SETTINGS = {
10
+ 'allowed-jira-projects': ['all'],
11
+ 'allowed-commands': [
11
12
  'me',
12
13
  'projects',
13
14
  'task-with-details',
@@ -24,8 +25,12 @@ export const DEFAULT_SETTINGS = {
24
25
  'organization',
25
26
  'transition',
26
27
  'update-description',
27
- 'confl'
28
- ]
28
+ 'confluence'
29
+ ],
30
+ 'allowed-confluence-spaces': ['all']
31
+ };
32
+ export const DEFAULT_SETTINGS = {
33
+ defaults: DEFAULT_ORG_SETTINGS
29
34
  };
30
35
  const CONFIG_DIR = path.join(os.homedir(), '.jira-ai');
31
36
  const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.yaml');
@@ -33,6 +38,25 @@ let cachedSettings = null;
33
38
  export function getSettingsPath() {
34
39
  return SETTINGS_FILE;
35
40
  }
41
+ export function migrateSettings(settings) {
42
+ // Migration logic: if old structure exists, move it to defaults
43
+ if (settings.projects || settings.commands) {
44
+ const migratedDefaults = {
45
+ 'allowed-jira-projects': settings.projects || DEFAULT_ORG_SETTINGS['allowed-jira-projects'],
46
+ 'allowed-commands': settings.commands || DEFAULT_ORG_SETTINGS['allowed-commands'],
47
+ 'allowed-confluence-spaces': DEFAULT_ORG_SETTINGS['allowed-confluence-spaces']
48
+ };
49
+ const newSettings = {
50
+ ...settings,
51
+ defaults: migratedDefaults,
52
+ };
53
+ // Remove old fields
54
+ delete newSettings.projects;
55
+ delete newSettings.commands;
56
+ return newSettings;
57
+ }
58
+ return settings;
59
+ }
36
60
  export function loadSettings() {
37
61
  if (cachedSettings) {
38
62
  return cachedSettings;
@@ -78,17 +102,13 @@ export function loadSettings() {
78
102
  result.error.issues.forEach(issue => {
79
103
  console.warn(chalk.yellow(` - ${issue.path.join('.')}: ${issue.message}`));
80
104
  });
81
- // Fallback to raw settings or default if parsing fails completely
82
- const settings = rawSettings;
83
- cachedSettings = {
84
- projects: settings?.projects || DEFAULT_SETTINGS.projects,
85
- commands: settings?.commands || DEFAULT_SETTINGS.commands
86
- };
105
+ // Fallback to defaults if parsing fails completely
106
+ return DEFAULT_SETTINGS;
87
107
  }
88
- else {
89
- cachedSettings = result.data;
90
- }
91
- return cachedSettings;
108
+ let settings = result.data;
109
+ settings = migrateSettings(settings);
110
+ cachedSettings = settings;
111
+ return settings;
92
112
  }
93
113
  catch (error) {
94
114
  throw new CliError(`Error loading ${SETTINGS_FILE}: ${error instanceof Error ? error.message : String(error)}`);
@@ -108,9 +128,19 @@ export function saveSettings(settings) {
108
128
  throw new CliError(`Error saving ${SETTINGS_FILE}: ${error instanceof Error ? error.message : String(error)}`);
109
129
  }
110
130
  }
111
- export function isProjectAllowed(projectKey) {
131
+ function getEffectiveSettings(orgAlias) {
112
132
  const settings = loadSettings();
113
- const isAllowed = settings.projects.some(p => {
133
+ const alias = orgAlias || getCurrentOrganizationAlias();
134
+ if (alias && settings.organizations && settings.organizations[alias]) {
135
+ return settings.organizations[alias];
136
+ }
137
+ return settings.defaults || null;
138
+ }
139
+ export function isProjectAllowed(projectKey, orgAlias) {
140
+ const settings = getEffectiveSettings(orgAlias);
141
+ if (!settings)
142
+ return false;
143
+ const isAllowed = settings['allowed-jira-projects'].some(p => {
114
144
  if (typeof p === 'string') {
115
145
  return p === 'all' || p === projectKey;
116
146
  }
@@ -118,16 +148,18 @@ export function isProjectAllowed(projectKey) {
118
148
  });
119
149
  return isAllowed;
120
150
  }
121
- export function isCommandAllowed(commandName, projectKey) {
122
- const settings = loadSettings();
151
+ export function isCommandAllowed(commandName, projectKey, orgAlias) {
123
152
  // about, auth, and settings are always allowed
124
153
  if (['about', 'auth', 'settings'].includes(commandName)) {
125
154
  return true;
126
155
  }
156
+ const settings = getEffectiveSettings(orgAlias);
157
+ if (!settings)
158
+ return false;
127
159
  if (projectKey) {
128
- let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
160
+ let project = settings['allowed-jira-projects'].find(p => typeof p !== 'string' && p.key === projectKey);
129
161
  if (!project) {
130
- project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
162
+ project = settings['allowed-jira-projects'].find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
131
163
  }
132
164
  if (project && typeof project !== 'string' && project.commands) {
133
165
  return project.commands.includes(commandName);
@@ -135,32 +167,40 @@ export function isCommandAllowed(commandName, projectKey) {
135
167
  }
136
168
  else {
137
169
  // For visibility/global check: allowed if in global list OR in any project-specific list
138
- const allowedGlobally = settings.commands.includes('all') || settings.commands.includes(commandName);
170
+ const allowedGlobally = settings['allowed-commands'].includes('all') || settings['allowed-commands'].includes(commandName);
139
171
  if (allowedGlobally) {
140
172
  return true;
141
173
  }
142
- const allowedInAnyProject = settings.projects.some(p => typeof p !== 'string' && p.commands && p.commands.includes(commandName));
174
+ const allowedInAnyProject = settings['allowed-jira-projects'].some(p => typeof p !== 'string' && p.commands && p.commands.includes(commandName));
143
175
  if (allowedInAnyProject) {
144
176
  return true;
145
177
  }
146
178
  return false;
147
179
  }
148
- if (settings.commands.includes('all')) {
180
+ if (settings['allowed-commands'].includes('all')) {
149
181
  return true;
150
182
  }
151
- return settings.commands.includes(commandName);
183
+ return settings['allowed-commands'].includes(commandName);
152
184
  }
153
- export function getAllowedProjects() {
154
- const settings = loadSettings();
155
- return settings.projects;
185
+ export function isConfluenceSpaceAllowed(spaceKey, orgAlias) {
186
+ const settings = getEffectiveSettings(orgAlias);
187
+ if (!settings)
188
+ return false;
189
+ return settings['allowed-confluence-spaces'].some(s => s === 'all' || s === spaceKey);
156
190
  }
157
- export function getAllowedCommands() {
158
- const settings = loadSettings();
159
- return settings.commands;
191
+ export function getAllowedProjects(orgAlias) {
192
+ const settings = getEffectiveSettings(orgAlias);
193
+ return settings ? settings['allowed-jira-projects'] : [];
160
194
  }
161
- export function applyGlobalFilters(jql) {
162
- const settings = loadSettings();
163
- const allAllowed = settings.projects.some(p => p === 'all');
195
+ export function getAllowedCommands(orgAlias) {
196
+ const settings = getEffectiveSettings(orgAlias);
197
+ return settings ? settings['allowed-commands'] : [];
198
+ }
199
+ export function applyGlobalFilters(jql, orgAlias) {
200
+ const settings = getEffectiveSettings(orgAlias);
201
+ if (!settings)
202
+ return jql;
203
+ const allAllowed = settings['allowed-jira-projects'].some(p => p === 'all');
164
204
  if (allAllowed) {
165
205
  return jql;
166
206
  }
@@ -172,7 +212,7 @@ export function applyGlobalFilters(jql) {
172
212
  filterPart = orderByMatch[1].trim();
173
213
  orderByPart = ` ORDER BY ${orderByMatch[2].trim()}`;
174
214
  }
175
- const projectFilters = settings.projects.map(p => {
215
+ const projectFilters = settings['allowed-jira-projects'].map(p => {
176
216
  const key = typeof p === 'string' ? p : p.key;
177
217
  const projectJql = typeof p === 'string' ? null : p.filters?.jql;
178
218
  if (projectJql) {
@@ -190,14 +230,16 @@ export function applyGlobalFilters(jql) {
190
230
  const filterJql = filterPart.trim() ? ` AND (${filterPart})` : '';
191
231
  return `(${combinedProjectFilter})${filterJql}${orderByPart}`;
192
232
  }
193
- export function validateIssueAgainstFilters(issue, currentUserId) {
194
- const settings = loadSettings();
233
+ export function validateIssueAgainstFilters(issue, currentUserId, orgAlias) {
234
+ const settings = getEffectiveSettings(orgAlias);
235
+ if (!settings)
236
+ return false;
195
237
  const projectKey = issue.key.split('-')[0];
196
238
  // Find specific project config first
197
- let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
239
+ let project = settings['allowed-jira-projects'].find(p => typeof p !== 'string' && p.key === projectKey);
198
240
  // If not found, look for string match (exact project key or 'all')
199
241
  if (!project) {
200
- project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
242
+ project = settings['allowed-jira-projects'].find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
201
243
  }
202
244
  if (!project) {
203
245
  return false;
@@ -93,7 +93,15 @@ export const ProjectSettingSchema = z.union([
93
93
  z.string().trim().min(1),
94
94
  ProjectConfigSchema
95
95
  ]);
96
+ export const OrganizationSettingsSchema = z.object({
97
+ 'allowed-jira-projects': z.array(ProjectSettingSchema).nullish().transform(val => val || ['all']),
98
+ 'allowed-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-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description', 'confluence']),
99
+ 'allowed-confluence-spaces': z.array(z.string()).nullish().transform(val => val || ['all']),
100
+ });
96
101
  export const SettingsSchema = z.object({
97
- projects: z.array(ProjectSettingSchema).nullish().transform(val => val || ['all']),
98
- 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-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description', 'confl']),
102
+ defaults: OrganizationSettingsSchema.optional(),
103
+ organizations: z.record(z.string(), OrganizationSettingsSchema).optional(),
104
+ // Keep legacy fields for migration
105
+ projects: z.array(ProjectSettingSchema).optional(),
106
+ commands: z.array(z.string()).optional(),
99
107
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",