ticketlens 0.38.53 → 0.38.54

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.38.53",
3
+ "version": "0.38.54",
4
4
  "description": "Jira CLI for developers — fetch ticket context, triage your queue, and stop tab-switching. Zero dependencies, all local.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@ import { promptSelect } from './lib/select-prompt.mjs';
21
21
  import { printFetchHelp } from './lib/help.mjs';
22
22
  import { readTextAttachments } from './lib/handoff-assembler.mjs';
23
23
  import { handleUnknownFlags } from './lib/arg-validator.mjs';
24
- import { TICKET_KEY_PATTERN } from './lib/cli.mjs';
24
+ import { TICKET_KEY_PATTERN, normalizeTicketKey } from './lib/cli.mjs';
25
25
  import { downloadAttachments } from './lib/attachment-downloader.mjs';
26
26
  import { readBriefCache, writeBriefCache, readSummaryCache, writeSummaryCache, briefCacheAge, BRIEF_TTL_MS } from './lib/brief-cache.mjs';
27
27
  import { recordTokensSaved } from './lib/activity-counter.mjs';
@@ -552,17 +552,19 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
552
552
 
553
553
  if (args[0] === 'pr') {
554
554
  const { assemblePr } = await import('./lib/pr-assembler.mjs');
555
- const ticketKeyArg = args[1];
555
+ let ticketKeyArg = args[1];
556
556
  if (!ticketKeyArg) {
557
557
  printErrFn('Error: "pr" requires a ticket key. Usage: ticketlens pr PROJ-123\n');
558
558
  process.exitCode = 1;
559
559
  return;
560
560
  }
561
- if (!TICKET_KEY_PATTERN.test(ticketKeyArg)) {
561
+ const normalizedPr = normalizeTicketKey(ticketKeyArg);
562
+ if (!TICKET_KEY_PATTERN.test(normalizedPr)) {
562
563
  printErrFn(`Error: "${ticketKeyArg}" is not a valid ticket key. Expected format: PROJ-123\n`);
563
564
  process.exitCode = 1;
564
565
  return;
565
566
  }
567
+ ticketKeyArg = normalizeTicketKey(ticketKeyArg, { stream: errStream });
566
568
  const resolvedConfigDir = configDir ?? (await import('./lib/config.mjs')).DEFAULT_CONFIG_DIR;
567
569
 
568
570
  // Resolve connection (same pattern as compliance dispatch)
@@ -627,17 +629,19 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
627
629
  }
628
630
 
629
631
  if (args[0] === 'compliance') {
630
- const ticketKeyArg = args[1];
632
+ let ticketKeyArg = args[1];
631
633
  if (!ticketKeyArg) {
632
634
  printErrFn('Error: "compliance" requires a ticket key. Usage: ticketlens compliance PROJ-123\n');
633
635
  process.exitCode = 1;
634
636
  return;
635
637
  }
636
- if (!TICKET_KEY_PATTERN.test(ticketKeyArg)) {
638
+ const normalizedCompliance = normalizeTicketKey(ticketKeyArg);
639
+ if (!TICKET_KEY_PATTERN.test(normalizedCompliance)) {
637
640
  printErrFn(`Error: "${ticketKeyArg}" is not a valid ticket key. Expected format: PROJ-123\n`);
638
641
  process.exitCode = 1;
639
642
  return;
640
643
  }
644
+ ticketKeyArg = normalizeTicketKey(ticketKeyArg, { stream: errStream });
641
645
 
642
646
  const resolvedConfigDir = configDir ?? (await import('./lib/config.mjs')).DEFAULT_CONFIG_DIR;
643
647
 
@@ -1005,17 +1009,19 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
1005
1009
  return;
1006
1010
  }
1007
1011
 
1008
- const ticketKey = args.find(a => !a.startsWith('--'));
1012
+ let ticketKey = args.find(a => !a.startsWith('--'));
1009
1013
  if (!ticketKey) {
1010
1014
  printFetchHelp({ stream: errStream });
1011
1015
  process.exitCode = 1;
1012
1016
  return;
1013
1017
  }
1014
- if (!TICKET_KEY_PATTERN.test(ticketKey)) {
1018
+ const normalizedFetch = normalizeTicketKey(ticketKey);
1019
+ if (!TICKET_KEY_PATTERN.test(normalizedFetch)) {
1015
1020
  printErrFn(`Error: "${ticketKey}" is not a valid ticket key. Expected format: PROJ-123\n`);
1016
1021
  process.exitCode = 1;
1017
1022
  return;
1018
1023
  }
1024
+ ticketKey = normalizeTicketKey(ticketKey, { stream: errStream });
1019
1025
 
1020
1026
  // Normalize --project= alias once at entry so all recursive calls only see --profile=
1021
1027
  const projectArg = args.find(a => a.startsWith('--project='));
@@ -3,7 +3,7 @@
3
3
  * Centralised here to avoid triplicating the regex and warning logic.
4
4
  */
5
5
 
6
- export const DEFAULT_API_BASE = 'http://api.ticketlens.test';
6
+ export const DEFAULT_API_BASE = 'https://api.ticketlens.app';
7
7
  export const DEFAULT_SITE_BASE = 'https://ticketlens.app';
8
8
 
9
9
  // Matches localhost, 127.0.0.1, and any hostname ending in .test or .local,
@@ -5,6 +5,20 @@
5
5
 
6
6
  export const TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
7
7
 
8
+ /**
9
+ * Jira/Linear project and team keys are always created uppercase, so any
10
+ * lowercase input is a typo, never a distinct real key — safe to correct
11
+ * silently before TICKET_KEY_PATTERN ever sees it.
12
+ */
13
+ export function normalizeTicketKey(ticketKey, { stream } = {}) {
14
+ if (!ticketKey) return ticketKey;
15
+ const upper = ticketKey.toUpperCase();
16
+ if (upper !== ticketKey && stream) {
17
+ stream.write(` Note: normalized "${ticketKey}" to "${upper}"\n`);
18
+ }
19
+ return upper;
20
+ }
21
+
8
22
  export function parseCommand(args) {
9
23
  const first = args[0];
10
24
 
@@ -19,7 +19,7 @@ import { enqueueNote, isRetryableFailure, maybeAutoFlush } from './recall-queue.
19
19
  import { incrementDraftKept, incrementDraftDeleted } from './activity-counter.mjs';
20
20
  import { extractText } from './attachment-text.mjs';
21
21
  import { readAttachments, MAX_ATTACHMENTS } from './attachment-uploader.mjs';
22
- import { TICKET_KEY_PATTERN } from './cli.mjs';
22
+ import { TICKET_KEY_PATTERN, normalizeTicketKey } from './cli.mjs';
23
23
  import { createStyler } from './ansi.mjs';
24
24
  import { confirmDestructive } from './confirm.mjs';
25
25
 
@@ -135,10 +135,15 @@ export async function runNoteAdd(cmdArgs, {
135
135
  // to forge a fake "## heading" line when the note is later injected into a brief.
136
136
  const title = rawTitle.replace(/[\r\n]+/g, ' ');
137
137
 
138
- const ticketKey = parseFlag(cmdArgs, 'ticket');
139
- if (ticketKey && !TICKET_KEY_PATTERN.test(ticketKey)) {
140
- stream.write(` Invalid --ticket value "${ticketKey}" — expected a ticket key like PROJ-123.\n`);
141
- return { written: false };
138
+ let ticketKey = parseFlag(cmdArgs, 'ticket');
139
+ if (ticketKey) {
140
+ const normalized = normalizeTicketKey(ticketKey);
141
+ if (!TICKET_KEY_PATTERN.test(normalized)) {
142
+ stream.write(` Invalid --ticket value "${ticketKey}" — expected a ticket key like PROJ-123.\n`);
143
+ return { written: false };
144
+ }
145
+ if (normalized !== ticketKey) normalizeTicketKey(ticketKey, { stream });
146
+ ticketKey = normalized;
142
147
  }
143
148
  const tagsArg = parseFlag(cmdArgs, 'tags');
144
149
  const tags = tagsArg ? tagsArg.split(',').map(t => t.trim()).filter(Boolean) : [];
@@ -260,10 +265,15 @@ export async function runNotePatch(cmdArgs, {
260
265
  return { patched: false };
261
266
  }
262
267
 
263
- const ticketKey = parseFlag(cmdArgs, 'ticket');
264
- if (ticketKey && !TICKET_KEY_PATTERN.test(ticketKey)) {
265
- stream.write(` Invalid --ticket value "${ticketKey}" — expected a ticket key like PROJ-123.\n`);
266
- return { patched: false };
268
+ let ticketKey = parseFlag(cmdArgs, 'ticket');
269
+ if (ticketKey) {
270
+ const normalized = normalizeTicketKey(ticketKey);
271
+ if (!TICKET_KEY_PATTERN.test(normalized)) {
272
+ stream.write(` Invalid --ticket value "${ticketKey}" — expected a ticket key like PROJ-123.\n`);
273
+ return { patched: false };
274
+ }
275
+ if (normalized !== ticketKey) normalizeTicketKey(ticketKey, { stream });
276
+ ticketKey = normalized;
267
277
  }
268
278
 
269
279
  const body = await readStdin();
@@ -339,10 +349,15 @@ export async function runNoteDelete(cmdArgs, {
339
349
  return { deleted: false };
340
350
  }
341
351
 
342
- const ticketKey = parseFlag(cmdArgs, 'ticket');
343
- if (ticketKey && !TICKET_KEY_PATTERN.test(ticketKey)) {
344
- stream.write(` Invalid --ticket value "${ticketKey}" — expected a ticket key like PROJ-123.\n`);
345
- return { deleted: false };
352
+ let ticketKey = parseFlag(cmdArgs, 'ticket');
353
+ if (ticketKey) {
354
+ const normalized = normalizeTicketKey(ticketKey);
355
+ if (!TICKET_KEY_PATTERN.test(normalized)) {
356
+ stream.write(` Invalid --ticket value "${ticketKey}" — expected a ticket key like PROJ-123.\n`);
357
+ return { deleted: false };
358
+ }
359
+ if (normalized !== ticketKey) normalizeTicketKey(ticketKey, { stream });
360
+ ticketKey = normalized;
346
361
  }
347
362
 
348
363
  const forceYes = cmdArgs.includes('--yes') || cmdArgs.includes('-y');
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
14
- import { TICKET_KEY_PATTERN } from './cli.mjs';
14
+ import { TICKET_KEY_PATTERN, normalizeTicketKey } from './cli.mjs';
15
15
  import { isLicensed, showUpgradePrompt } from './license.mjs';
16
16
  import { listNotes } from './recall-vault.mjs';
17
17
  import { readCliToken } from './cli-auth.mjs';
@@ -48,17 +48,23 @@ export async function runRecall(cmdArgs, {
48
48
  return { ok: false };
49
49
  }
50
50
 
51
- const isTicketKey = TICKET_KEY_PATTERN.test(arg);
51
+ // A lowercase-typed ticket key (e.g. "proj-123") must not be mistaken for a
52
+ // free-text search phrase — normalize before classifying, but never touch
53
+ // a genuine search query's own casing.
54
+ const normalizedArg = normalizeTicketKey(arg);
55
+ const isTicketKey = TICKET_KEY_PATTERN.test(normalizedArg);
56
+ if (isTicketKey && normalizedArg !== arg) normalizeTicketKey(arg, { stream: errorStream });
57
+ const key = isTicketKey ? normalizedArg : arg;
52
58
 
53
59
  const cliToken = readCliTokenFn(configDir);
54
60
  if (cliToken) {
55
- const profile = resolveProfileFn(isTicketKey ? arg : null, { configDir, cwd: process.cwd() });
61
+ const profile = resolveProfileFn(isTicketKey ? key : null, { configDir, cwd: process.cwd() });
56
62
  const recallTeamId = profile ? loadProfileRecallTeamIdFn(profile.name, configDir) : null;
57
63
  await pullNotesFn({ cliToken, configDir, ttlMs: 0, groupId: recallTeamId ?? undefined });
58
64
  await maybeAutoFlushFn({ cliToken, configDir });
59
65
  }
60
66
 
61
- const filter = isTicketKey ? { ticketKey: arg } : { query: arg };
67
+ const filter = isTicketKey ? { ticketKey: key } : { query: key };
62
68
  const results = listNotesFn(filter, { configDir });
63
69
 
64
70
  const styled = !cmdArgs.includes('--plain') && !!stream.isTTY;
@@ -2,6 +2,7 @@ import { queryTicketHistory, DEFAULT_CONFIG_DIR } from './triage-history.mjs';
2
2
  import { isLicensed as defaultIsLicensed, showUpgradePrompt } from './license.mjs';
3
3
  import { createStyler } from './ansi.mjs';
4
4
  import { printHistoryHelp } from './help.mjs';
5
+ import { normalizeTicketKey } from './cli.mjs';
5
6
 
6
7
  /**
7
8
  * Prints a ticket's local triage-history timeline. Extracted from
@@ -26,12 +27,13 @@ export async function runHistory(args = [], opts = {}) {
26
27
  return;
27
28
  }
28
29
 
29
- const ticketKey = args[0];
30
+ let ticketKey = args[0];
30
31
  if (!ticketKey || ticketKey.startsWith('-')) {
31
32
  warn('Usage: ticketlens history TICKET-KEY\n');
32
33
  process.exitCode = 1;
33
34
  return;
34
35
  }
36
+ ticketKey = normalizeTicketKey(ticketKey, { stream: { write: warn } });
35
37
 
36
38
  const entries = queryFn(ticketKey, { configDir });
37
39
  if (entries.length === 0) {
@@ -18,7 +18,7 @@ import { checkCooldown, recordAction } from './ticket-action-cooldown.mjs';
18
18
  import { logAction } from './ticket-action-log.mjs';
19
19
  import { readMetadataCache, writeMetadataCache } from './ticket-metadata-cache.mjs';
20
20
  import { detectProjectOrTypeError, enrichCreateFailure } from './ticket-create-enrichment.mjs';
21
- import { TICKET_KEY_PATTERN } from './cli.mjs';
21
+ import { TICKET_KEY_PATTERN, normalizeTicketKey } from './cli.mjs';
22
22
  import { scoreCandidates } from './duplicate-scorer.mjs';
23
23
  import { MAX_ATTACHMENTS } from './attachment-uploader.mjs';
24
24
  import { createStyler } from './ansi.mjs';
@@ -209,11 +209,17 @@ function requireLicense(isLicensedFn, configDir, commandName, stream) {
209
209
 
210
210
  function requireTicketKey(cmdArgs, usage, stream) {
211
211
  const ticketKey = cmdArgs[0];
212
- if (!ticketKey || ticketKey.startsWith('--') || !TICKET_KEY_PATTERN.test(ticketKey)) {
212
+ if (!ticketKey || ticketKey.startsWith('--')) {
213
213
  stream.write(usage);
214
214
  return null;
215
215
  }
216
- return ticketKey;
216
+ const normalized = normalizeTicketKey(ticketKey);
217
+ if (!TICKET_KEY_PATTERN.test(normalized)) {
218
+ stream.write(usage);
219
+ return null;
220
+ }
221
+ if (normalized !== ticketKey) normalizeTicketKey(ticketKey, { stream });
222
+ return normalized;
217
223
  }
218
224
 
219
225
  /**