jira-ai 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -191,6 +191,43 @@ When `authType` is set to `service_account`, jira-ai automatically:
191
191
 
192
192
  Existing configurations using standard API tokens are unaffected.
193
193
 
194
+ ## Saved Queries
195
+
196
+ Define reusable JQL queries in your `settings.yaml` under the `saved-queries` key to avoid repeating common searches.
197
+
198
+ ### Configuration
199
+
200
+ Add a `saved-queries` map to your `settings.yaml` — each key is a query name (lowercase alphanumeric with hyphens) and each value is a JQL string:
201
+
202
+ ```yaml
203
+ saved-queries:
204
+ my-open-bugs: "project = PROJ AND status = Open AND issuetype = Bug"
205
+ overdue-tasks: "project = PROJ AND duedate < now() AND status != Done"
206
+ my-assignee: "assignee = currentUser()"
207
+ ```
208
+
209
+ ### Usage
210
+
211
+ Run a saved query by name:
212
+
213
+ ```bash
214
+ jira-ai issue search --query my-open-bugs
215
+ ```
216
+
217
+ List all configured saved queries:
218
+
219
+ ```bash
220
+ jira-ai issue search --list-queries
221
+ ```
222
+
223
+ Combine with result limits:
224
+
225
+ ```bash
226
+ jira-ai issue search --query overdue-tasks --limit 10
227
+ ```
228
+
229
+ Saved queries are mutually exclusive with raw JQL — you cannot provide both a positional JQL argument and `--query` at the same time.
230
+
194
231
  ## Configuration & Restrictions
195
232
 
196
233
  Tool allows you to have very complex configutations of what Projects/Jira commands/Issue types you would have acess to thought the tool.
package/dist/cli.js CHANGED
@@ -131,14 +131,20 @@ issue
131
131
  .option('--custom-field <field=value>', 'Custom field in fieldId=value format (repeatable)', (val, prev) => [...(prev || []), val], [])
132
132
  .action(withPermission('issue.create', createTaskCommand, { schema: CreateTaskSchema }));
133
133
  issue
134
- .command('search <jql-query>')
135
- .description('Execute a JQL search query. Returns issues with key, summary, status, assignee, and priority.')
134
+ .command('search [jql-query]')
135
+ .description('Execute a JQL search query. Provide raw JQL or use --query to reference a saved query.')
136
136
  .option('-l, --limit <number>', 'Maximum number of results (default: 50)', '50')
137
+ .option('--query <name>', 'Use a saved query by name (mutually exclusive with positional JQL)')
138
+ .option('--list-queries', 'List all available saved queries')
137
139
  .action(withPermission('issue.search', runJqlCommand, {
138
140
  schema: RunJqlSchema,
139
141
  validateArgs: (args) => {
140
- if (typeof args[0] !== 'string' || args[0].trim() === '') {
141
- throw new CliError('JQL query cannot be empty');
142
+ const jqlQuery = args[0];
143
+ const opts = args[args.length - 2];
144
+ const hasQuery = opts && opts.query;
145
+ const hasListQueries = opts && opts.listQueries;
146
+ if (!hasQuery && !hasListQueries && (typeof jqlQuery !== 'string' || jqlQuery.trim() === '')) {
147
+ throw new CliError('JQL query cannot be empty. Provide a JQL query, use --query <name>, or use --list-queries.');
142
148
  }
143
149
  }
144
150
  }));
@@ -1,11 +1,35 @@
1
1
  import { searchIssuesByJql } from '../lib/jira-client.js';
2
2
  import { outputResult } from '../lib/json-mode.js';
3
+ import { getSavedQuery, listSavedQueries } from '../lib/settings.js';
4
+ import { CliError } from '../types/errors.js';
3
5
  export async function runJqlCommand(jqlQuery, options) {
4
- // Parse and validate limit parameter
6
+ // Handle --list-queries
7
+ if (options.listQueries) {
8
+ const queries = listSavedQueries();
9
+ outputResult({ queries });
10
+ return;
11
+ }
12
+ // Mutual exclusion: can't have both positional JQL and --query
13
+ if (jqlQuery && jqlQuery.trim() !== '' && options.query) {
14
+ throw new CliError('Cannot specify both JQL query and --query. Use one or the other.');
15
+ }
16
+ let resolvedJql;
17
+ if (options.query) {
18
+ const savedJql = getSavedQuery(options.query);
19
+ if (savedJql === undefined) {
20
+ const available = listSavedQueries().map((q) => q.name).join(', ');
21
+ const availableMsg = available ? available : '(none)';
22
+ throw new CliError(`Saved query '${options.query}' not found. Available: ${availableMsg}`);
23
+ }
24
+ resolvedJql = savedJql;
25
+ }
26
+ else {
27
+ resolvedJql = jqlQuery;
28
+ }
5
29
  let maxResults = options.limit || 50;
6
30
  if (maxResults > 1000) {
7
31
  maxResults = 1000;
8
32
  }
9
- const issues = await searchIssuesByJql(jqlQuery, maxResults);
33
+ const issues = await searchIssuesByJql(resolvedJql, maxResults);
10
34
  outputResult(issues);
11
35
  }
@@ -4,6 +4,16 @@ import os from 'os';
4
4
  import yaml from 'js-yaml';
5
5
  import { CliError } from '../types/errors.js';
6
6
  import { SettingsSchema } from './validation.js';
7
+ export function getSavedQuery(name) {
8
+ const settings = loadSettings();
9
+ return settings.savedQueries?.[name];
10
+ }
11
+ export function listSavedQueries() {
12
+ const settings = loadSettings();
13
+ if (!settings.savedQueries)
14
+ return [];
15
+ return Object.entries(settings.savedQueries).map(([name, jql]) => ({ name, jql }));
16
+ }
7
17
  // Mapping from old flat command names to new hierarchical paths
8
18
  export const LEGACY_COMMAND_MAP = {
9
19
  'me': 'user.me',
@@ -113,6 +113,8 @@ export const UpdateDescriptionSchema = z.object({
113
113
  });
114
114
  export const RunJqlSchema = z.object({
115
115
  limit: NumericStringSchema.optional(),
116
+ query: z.string().optional(),
117
+ listQueries: z.boolean().optional(),
116
118
  });
117
119
  export const TimeframeSchema = z.string().regex(/^\d+d$/, 'Timeframe must be in format like "7d" or "30d"');
118
120
  export const GetPersonWorklogSchema = z.object({
@@ -157,10 +159,12 @@ export const OrganizationSettingsSchema = z.object({
157
159
  'allowed-commands': z.array(z.string()).nullish().transform(val => val || DEFAULT_ALLOWED_COMMANDS),
158
160
  'allowed-confluence-spaces': z.array(z.string()).nullish().transform(val => val || ['all']),
159
161
  });
162
+ export const SavedQueryNameSchema = z.string().regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/, 'Query name must be lowercase alphanumeric with hyphens (e.g., "my-query"), cannot start or end with a hyphen');
160
163
  export const SettingsSchema = z.object({
161
164
  defaults: OrganizationSettingsSchema.optional(),
162
165
  projects: z.array(ProjectSettingSchema).optional(),
163
166
  commands: z.array(z.string()).optional(),
167
+ savedQueries: z.record(SavedQueryNameSchema, z.string().min(1)).optional(),
164
168
  });
165
169
  // =============================================================================
166
170
  // EPIC VALIDATION SCHEMAS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
package/settings.yaml CHANGED
@@ -14,3 +14,12 @@ commands:
14
14
  - run-jql
15
15
  - list-issue-types
16
16
  - project-statuses
17
+
18
+ # Saved Queries: Define reusable JQL queries by name.
19
+ # Keys must be lowercase alphanumeric with optional hyphens (e.g., "my-query").
20
+ # Values are JQL query strings. Saved queries are used with:
21
+ # jira-ai issue search --query <name>
22
+ # jira-ai issue search --list-queries
23
+ # saved-queries:
24
+ # my-open-bugs: "project = PROJ AND status = Open AND issuetype = Bug"
25
+ # overdue-tasks: "project = PROJ AND duedate < now() AND status != Done"