ticketlens 0.38.52 → 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 +1 -1
- package/skills/jtb/scripts/fetch-ticket.mjs +13 -7
- package/skills/jtb/scripts/lib/cli.mjs +14 -0
- package/skills/jtb/scripts/lib/license.mjs +27 -8
- package/skills/jtb/scripts/lib/note-command.mjs +28 -13
- package/skills/jtb/scripts/lib/recall-command.mjs +10 -4
- package/skills/jtb/scripts/lib/run-history.mjs +3 -1
- package/skills/jtb/scripts/lib/ticket-command.mjs +9 -3
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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='));
|
|
@@ -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
|
|
|
@@ -130,17 +130,23 @@ export function showUpgradePrompt(requiredTier, featureFlag, { stream = process.
|
|
|
130
130
|
const LEMONSQUEEZY_ACTIVATE_URL = 'https://api.lemonsqueezy.com/v1/licenses/activate';
|
|
131
131
|
const LEMONSQUEEZY_VALIDATE_URL = 'https://api.lemonsqueezy.com/v1/licenses/validate';
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
// TL- keys are issued by TicketLens itself (LicenseIssuanceService::KEY_PREFIX), never by
|
|
134
|
+
// LemonSqueezy — sending one to the LemonSqueezy fallback always fails, confusingly (backlog #30).
|
|
135
|
+
const TL_ISSUED_KEY_RE = /^TL-/i;
|
|
136
|
+
|
|
137
|
+
function resolveLicenseUrl(action, fallback, key) {
|
|
134
138
|
const base = process.env.TICKETLENS_API_URL;
|
|
135
|
-
|
|
139
|
+
if (base) return `${base.replace(/\/$/, '')}/v1/licenses/${action}`;
|
|
140
|
+
if (TL_ISSUED_KEY_RE.test(key)) return null;
|
|
141
|
+
return fallback;
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
function resolveActivateUrl() {
|
|
139
|
-
return resolveLicenseUrl('activate', LEMONSQUEEZY_ACTIVATE_URL);
|
|
144
|
+
function resolveActivateUrl(key) {
|
|
145
|
+
return resolveLicenseUrl('activate', LEMONSQUEEZY_ACTIVATE_URL, key);
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
function resolveValidateUrl() {
|
|
143
|
-
return resolveLicenseUrl('validate', LEMONSQUEEZY_VALIDATE_URL);
|
|
148
|
+
function resolveValidateUrl(key) {
|
|
149
|
+
return resolveLicenseUrl('validate', LEMONSQUEEZY_VALIDATE_URL, key);
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
function extractTier(meta) {
|
|
@@ -154,8 +160,16 @@ export async function activateLicense(key, opts = {}) {
|
|
|
154
160
|
const { configDir = DEFAULT_CONFIG_DIR, fetcher = globalThis.fetch, instanceName } = opts;
|
|
155
161
|
const instance = instanceName || os.hostname();
|
|
156
162
|
|
|
163
|
+
const url = resolveActivateUrl(key);
|
|
164
|
+
if (url === null) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: 'TICKETLENS_API_URL is not set — required to activate a TicketLens-issued (TL-) license key. Set it to your TicketLens backend URL and try again.',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
157
171
|
try {
|
|
158
|
-
const res = await fetcher(
|
|
172
|
+
const res = await fetcher(url, {
|
|
159
173
|
method: 'POST',
|
|
160
174
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
161
175
|
body: JSON.stringify({ license_key: key, instance_name: instance }),
|
|
@@ -194,8 +208,13 @@ export async function revalidateLicense(opts = {}) {
|
|
|
194
208
|
|
|
195
209
|
const instance = instanceName || os.hostname();
|
|
196
210
|
|
|
211
|
+
const url = resolveValidateUrl(license.key);
|
|
212
|
+
if (url === null) {
|
|
213
|
+
return { success: true, tier: license.tier, cached: true };
|
|
214
|
+
}
|
|
215
|
+
|
|
197
216
|
try {
|
|
198
|
-
const res = await fetcher(
|
|
217
|
+
const res = await fetcher(url, {
|
|
199
218
|
method: 'POST',
|
|
200
219
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
201
220
|
body: JSON.stringify({ license_key: license.key, instance_name: instance }),
|
|
@@ -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
|
-
|
|
139
|
-
if (ticketKey
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
264
|
-
if (ticketKey
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
343
|
-
if (ticketKey
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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 ?
|
|
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:
|
|
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
|
-
|
|
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('--')
|
|
212
|
+
if (!ticketKey || ticketKey.startsWith('--')) {
|
|
213
213
|
stream.write(usage);
|
|
214
214
|
return null;
|
|
215
215
|
}
|
|
216
|
-
|
|
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
|
/**
|