ticketlens 0.42.0 → 0.42.2
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 +3 -2
- package/package.json +1 -1
- package/skills/jtb/SKILL.md +3 -3
- package/skills/jtb/scripts/lib/adapters/jira-adapter.mjs +18 -12
- package/skills/jtb/scripts/lib/api-utils.mjs +1 -1
- package/skills/jtb/scripts/lib/help.mjs +3 -3
- package/skills/jtb/scripts/lib/jira-client.mjs +11 -1
- package/skills/jtb/scripts/lib/mcp-tool-schemas.mjs +2 -2
- package/skills/jtb/scripts/lib/ticket-command.mjs +7 -12
package/README.md
CHANGED
|
@@ -499,7 +499,7 @@ Write directly to the ticket in its real tracker — Jira, GitHub, or Linear —
|
|
|
499
499
|
|
|
500
500
|
`ticketlens transition` with just a ticket key lists the tracker's current valid options without changing anything (Jira: real workflow transitions for that issue; GitHub: open/closed; Linear: team-scoped workflow states). Add both `--target` and `--confirm` to execute — `--confirm` is a deliberate two-step gate: a behavioral nudge and forensic trail, not a hard security guarantee. Every write, once resolved, is re-validated against the tracker's current state immediately before executing — never a blind write against a stale option.
|
|
501
501
|
|
|
502
|
-
`ticketlens assign --to=me` self-assigns immediately, no `--confirm` needed. Any other `--to` (a name or email) assigns to another developer
|
|
502
|
+
`ticketlens assign --to=me` self-assigns immediately, no `--confirm` needed. Any other `--to` (a name or email) assigns to another developer on Jira (Cloud and Server/DC) — GitHub/Linear refuse cleanly. It searches Jira's real assignable-user list first: without `--confirm` it only lists the matching candidate(s), never assigns; with `--confirm` it executes only if exactly one candidate still matches (0 or 2+ always refuses). Matches are cached locally per project for 3 days.
|
|
503
503
|
|
|
504
504
|
`ticketlens worklog` (or `tl worklog`) logs time on Jira tickets, always as you.
|
|
505
505
|
|
|
@@ -1121,7 +1121,8 @@ npm test
|
|
|
1121
1121
|
See [ROADMAP.md](ROADMAP.md) for the full plan.
|
|
1122
1122
|
|
|
1123
1123
|
Recently shipped:
|
|
1124
|
-
- **`ticketlens
|
|
1124
|
+
- **`ticketlens update --priority`** — a bad priority name now tells you the project's real, current valid options instead of a bare tracker error
|
|
1125
|
+
- **`ticketlens assign --to="name or email"`** — assign a ticket to another developer, not just yourself. Jira (Cloud + Server/DC); searches real assignable users first, lists matches, executes only on one confirmed match. Pro tier
|
|
1125
1126
|
- **Console: Recall "select all N matching" bulk delete** — Gmail-style banner deletes every note matching your search/filters across all pages, not just the current one
|
|
1126
1127
|
- **Recall Stop-hook fix** — the end-of-session Recall reminder no longer fires because of file writes; only real ticket writes (comment, transition, assign, update) arm it. Also hardened against malformed transcripts and a session-id path-collision bug found by adversarial testing
|
|
1127
1128
|
- **Console: no repeat request on same-page menu clicks** — sidebar links, the header gear and Settings tabs skip the request when they point at the page you are on
|
package/package.json
CHANGED
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.
|
|
1
|
+
<!-- jtb-skill-version: 0.47.0 -->
|
|
2
2
|
---
|
|
3
3
|
name: jtb
|
|
4
4
|
description: Fetch a Jira ticket's full context (description, comments, linked issues, code references) and assemble a structured TicketBrief for implementation planning. Use when user types /jtb, mentions a Jira ticket key, or wants to plan work from a Jira ticket.
|
|
@@ -438,7 +438,7 @@ ticketlens transition PROD-1234 # list valid transi
|
|
|
438
438
|
ticketlens transition PROD-1234 --target="Done" --confirm # execute
|
|
439
439
|
ticketlens assign PROD-1234 --to=me # assign to yourself — immediate, no --confirm
|
|
440
440
|
ticketlens assign PROD-1234 --to="Jane Dev" # search assignable users — read-only, lists match(es)
|
|
441
|
-
ticketlens assign PROD-1234 --to="Jane Dev" --confirm # execute — only if exactly one match (Jira
|
|
441
|
+
ticketlens assign PROD-1234 --to="Jane Dev" --confirm # execute — only if exactly one match (Jira only)
|
|
442
442
|
ticketlens duplicates PROD-1234 # find likely duplicates — read-only
|
|
443
443
|
ticketlens link PROD-1234 PROD-5678 # list valid link types — read-only
|
|
444
444
|
ticketlens link PROD-1234 PROD-5678 --type="Duplicate" --confirm # execute the link
|
|
@@ -453,7 +453,7 @@ ticketlens worklog PROD-1234=1h30m PROD-5678=45m --comment="Sprint work" --confi
|
|
|
453
453
|
|
|
454
454
|
`transition` called with just a ticket key never mutates anything — it lists the tracker's current valid options (Jira: real workflow transitions for that issue; GitHub: open/closed; Linear: team-scoped workflow states). Only add `--target` **and** `--confirm` once the target has actually been confirmed with the user — `--confirm` is a deliberate two-step gate, not a formality to route around. Never guess a `--target` value; always list first, then use one of the names shown.
|
|
455
455
|
|
|
456
|
-
`assign` — `--to=me` self-assigns immediately, no `--confirm` needed. Any other `--to` (a name or email) assigns to another developer
|
|
456
|
+
`assign` — `--to=me` self-assigns immediately, no `--confirm` needed. Any other `--to` (a name or email) assigns to another developer on Jira (Cloud and Server/DC) — GitHub and Linear refuse cleanly rather than guess at unverified API behavior. It searches Jira's real assignable-user list first: called without `--confirm` it never assigns, only lists the matching candidate(s) (name + accountId, cached locally per project for 3 days); called again with the same `--to` and `--confirm`, it executes only if exactly one candidate still matches — 0 or 2+ matches always refuses, never guesses which person was meant. Never attempt a workaround (e.g. via `comment`) for a tracker `assign` refuses (GitHub/Linear) — tell the user it isn't supported yet.
|
|
457
457
|
|
|
458
458
|
`duplicates` lists likely-duplicate tickets in the same project. On Jira, any ticket already linked as a "Duplicate" is always listed first — that's a confirmed relationship a human already recorded, not a heuristic. Everything else is ranked by local title/description overlap — no tracker scores similarity server-side, so treat those as a nudge for the user to check manually, never as a confirmed duplicate to act on unprompted (e.g. don't auto-close or auto-comment based on a match). `--threshold=N` (0–1, default 0.35) tightens or loosens what counts as a text-match — it has no effect on Jira-linked duplicates, which are always shown. An empty result is the same approximation in the other direction — the local scorer can miss a real duplicate too, so don't treat "no likely duplicates found" as proof none exist.
|
|
459
459
|
|
|
@@ -85,27 +85,33 @@ export function createJiraAdapter(conn, { fetcher = globalThis.fetch } = {}) {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Resolves a free-text name/email into candidate assignable users —
|
|
88
|
-
* read-only, never assigns. Cloud
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* read-only, never assigns. Cloud (v3) and Server/DC (v2) both use
|
|
89
|
+
* `GET .../user/assignable/search`; `fetchAssignableUsers` is already
|
|
90
|
+
* apiVersion-generic (ROADMAP 65) and normalizes both shapes into
|
|
91
|
+
* {accountId, name, displayName} — Cloud populates accountId, Server/DC
|
|
92
|
+
* populates name, this method just passes that through unmodified.
|
|
91
93
|
* Caching (3-day TTL, per project+query) is the caller's job — same
|
|
92
94
|
* split as `listIssueTypes`, which is also cache-agnostic here.
|
|
93
95
|
*/
|
|
94
96
|
async searchAssignableUsers(key, query, opts = {}) {
|
|
95
|
-
if (apiVersion !== 3) {
|
|
96
|
-
throw new Error('Assigning to another developer needs Jira Cloud — Server/DC is not supported yet.');
|
|
97
|
-
}
|
|
98
97
|
const users = await fetchAssignableUsers(key, query, { ...base, ...opts });
|
|
99
|
-
return users.map(u => ({ accountId: u.accountId, displayName: u.displayName ?? u.name ?? u.accountId }));
|
|
98
|
+
return users.map(u => ({ accountId: u.accountId, name: u.name, displayName: u.displayName ?? u.name ?? u.accountId }));
|
|
100
99
|
},
|
|
101
100
|
|
|
102
101
|
/**
|
|
103
|
-
* Executes an assignment to a resolved
|
|
104
|
-
*
|
|
105
|
-
*
|
|
102
|
+
* Executes an assignment to a resolved candidate (from
|
|
103
|
+
* `searchAssignableUsers`) — same apiVersion field-resolution pattern
|
|
104
|
+
* `assignToSelf` already uses: accountId for Cloud, name for Server/DC
|
|
105
|
+
* (ROADMAP 65). Never sends a null identity field — Jira's PUT treats
|
|
106
|
+
* that as "unassign", not an error (same reasoning as `assignToSelf`).
|
|
106
107
|
*/
|
|
107
|
-
async assignToUser(key,
|
|
108
|
-
|
|
108
|
+
async assignToUser(key, candidate, opts = {}) {
|
|
109
|
+
const field = apiVersion === 3 ? 'accountId' : 'name';
|
|
110
|
+
const value = candidate[field];
|
|
111
|
+
if (!value) {
|
|
112
|
+
throw new Error(`Cannot resolve candidate's ${field} — Jira did not return it for this connection.`);
|
|
113
|
+
}
|
|
114
|
+
await assignIssue(key, { [field]: value }, { ...base, ...opts });
|
|
109
115
|
},
|
|
110
116
|
|
|
111
117
|
/**
|
|
@@ -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 = '
|
|
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,
|
|
@@ -671,7 +671,7 @@ export function printMcpHelp({ stream = process.stdout } = {}) {
|
|
|
671
671
|
` "not available" instead of an empty result — every other tool needs Pro.`,
|
|
672
672
|
` ${s.cyan('ticket_transition')} is destructive when called with`,
|
|
673
673
|
` \`target\`+\`confirm: true\`; ${s.cyan('ticket_assign')}'s \`to: "me"\` self-assigns immediately,`,
|
|
674
|
-
` no confirm needed — assigning to another developer (Jira
|
|
674
|
+
` no confirm needed — assigning to another developer (Jira only) resolves`,
|
|
675
675
|
` \`to\` first and needs \`confirm: true\` once it matches exactly one candidate;`,
|
|
676
676
|
` ${s.cyan('ticket_worklog')} is Jira-only, logs as you only, and writes nothing without`,
|
|
677
677
|
` \`confirm: true\` (it previews instead) — a worklog cannot be deleted through TicketLens;`,
|
|
@@ -782,8 +782,8 @@ export function printAssignHelp({ stream = process.stdout } = {}) {
|
|
|
782
782
|
'',
|
|
783
783
|
` Assign a ticket directly in its tracker (Jira/GitHub/Linear). ${s.dim('[Pro]')}`,
|
|
784
784
|
` ${s.brand('--to=me')} self-assigns immediately, no --confirm needed. Any other`,
|
|
785
|
-
` ${s.brand('--to')} (a name or email) assigns to another developer
|
|
786
|
-
`
|
|
785
|
+
` ${s.brand('--to')} (a name or email) assigns to another developer on Jira`,
|
|
786
|
+
` (Cloud and Server/DC). Resolves the name/email against real assignable users first:`,
|
|
787
787
|
` without --confirm it lists the match(es) instead of assigning; with`,
|
|
788
788
|
` --confirm it executes only if exactly one candidate matched. Results are`,
|
|
789
789
|
` cached locally per project for 3 days.`,
|
|
@@ -622,6 +622,14 @@ export async function assignIssue(ticketKey, assignee, opts = {}) {
|
|
|
622
622
|
* published spec — there is no "list everyone assignable" mode), so this
|
|
623
623
|
* throws early on an empty query rather than sending a request guaranteed
|
|
624
624
|
* to fail.
|
|
625
|
+
*
|
|
626
|
+
* Server/DC (v2) also gets the legacy `username` param alongside `query` —
|
|
627
|
+
* live-verified against a real Server/DC instance (ROADMAP 65): that
|
|
628
|
+
* instance silently ignores `query` and only filters on `username`, so
|
|
629
|
+
* `query`-only returned every assignable user regardless of input.
|
|
630
|
+
* Sending both is safe on Cloud too (Cloud simply never receives the extra
|
|
631
|
+
* param, this branch is v2-only) and on any Server/DC version that does
|
|
632
|
+
* honor `query` (an extra recognized param narrows further, never loosens).
|
|
625
633
|
*/
|
|
626
634
|
export async function fetchAssignableUsers(ticketKey, query, opts = {}) {
|
|
627
635
|
const { env = process.env, fetcher = globalThis.fetch, lookup = defaultLookupFor(fetcher), apiVersion = 2, timeoutMs = 10_000, allowPrivateIp = false, maxResults = 20 } = opts;
|
|
@@ -630,7 +638,9 @@ export async function fetchAssignableUsers(ticketKey, query, opts = {}) {
|
|
|
630
638
|
}
|
|
631
639
|
validateBaseUrl(env.JIRA_BASE_URL, allowPrivateIp);
|
|
632
640
|
const baseUrl = env.JIRA_BASE_URL.replace(/\/$/, '');
|
|
633
|
-
const
|
|
641
|
+
const paramFields = { issueKey: ticketKey, query, maxResults: String(maxResults) };
|
|
642
|
+
if (apiVersion === 2) paramFields.username = query;
|
|
643
|
+
const params = new URLSearchParams(paramFields);
|
|
634
644
|
const url = `${baseUrl}/rest/api/${apiVersion}/user/assignable/search?${params}`;
|
|
635
645
|
|
|
636
646
|
const fetchOpts = { headers: { ...buildAuthHeader(env), 'Content-Type': 'application/json' } };
|
|
@@ -239,12 +239,12 @@ export const TOOLS = [
|
|
|
239
239
|
},
|
|
240
240
|
{
|
|
241
241
|
name: 'ticket_assign',
|
|
242
|
-
description: 'Assign a ticket in its tracker (Jira/GitHub/Linear). `to: "me"` self-assigns immediately, no confirm needed. Any other `to` (a name or email) assigns to another developer
|
|
242
|
+
description: 'Assign a ticket in its tracker (Jira/GitHub/Linear). `to: "me"` self-assigns immediately, no confirm needed. Any other `to` (a name or email) assigns to another developer on Jira (Cloud and Server/DC); GitHub and Linear are refused. Resolves `to` against real assignable users before writing: called without `confirm: true`, it never assigns — it returns the matching candidate(s) (name + accountId) instead, so the exact person can be confirmed first. Called again with the same `to` and `confirm: true`, it executes only if `to` still resolves to exactly one candidate; 0 or 2+ matches always refuses, never guesses. Matches are cached locally per ticket\'s project for 3 days. Requires a TicketLens Pro license.',
|
|
243
243
|
inputSchema: {
|
|
244
244
|
type: 'object',
|
|
245
245
|
properties: {
|
|
246
246
|
ticket: { type: 'string', description: 'Ticket key, e.g. PROJ-123.' },
|
|
247
|
-
to: { type: 'string', description: '"me" to self-assign, or a name/email to search for and assign to another developer (Jira
|
|
247
|
+
to: { type: 'string', description: '"me" to self-assign, or a name/email to search for and assign to another developer (Jira only).' },
|
|
248
248
|
confirm: { type: 'boolean', description: 'Must be true, alongside a `to` that resolves to exactly one match, to actually execute an assign-to-other. Not needed for `to: "me"`.' },
|
|
249
249
|
},
|
|
250
250
|
required: ['ticket', 'to'],
|
|
@@ -538,10 +538,9 @@ export async function resolveAssigneeCandidates(adapter, ticketKey, query, {
|
|
|
538
538
|
|
|
539
539
|
/**
|
|
540
540
|
* `--to=me` self-assigns immediately — unchanged fast path, no discovery,
|
|
541
|
-
* no confirm. Any other `--to` resolves a real person first (ROADMAP 61
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* one candidate matches AND --confirm is given, mirroring `transition`'s
|
|
541
|
+
* no confirm. Any other `--to` resolves a real person first (ROADMAP 61,
|
|
542
|
+
* extended to Server/DC by ROADMAP 65) and only executes when exactly one
|
|
543
|
+
* candidate matches AND --confirm is given, mirroring `transition`'s
|
|
545
544
|
* list-then-confirm shape — notifying a colleague deserves the same
|
|
546
545
|
* reviewed-before-write gate as a workflow-state change.
|
|
547
546
|
*
|
|
@@ -611,10 +610,6 @@ export async function runTicketAssign(cmdArgs, {
|
|
|
611
610
|
stream.write(` Assigning to another developer is Jira-only right now — ${adapter.type} is not supported.\n`);
|
|
612
611
|
return { ok: false };
|
|
613
612
|
}
|
|
614
|
-
if (conn.auth !== 'cloud') {
|
|
615
|
-
stream.write(' Assigning to another developer needs Jira Cloud — Server/DC is not supported yet.\n');
|
|
616
|
-
return { ok: false };
|
|
617
|
-
}
|
|
618
613
|
|
|
619
614
|
const s = createStyler({ isTTY: stream.isTTY });
|
|
620
615
|
let candidates;
|
|
@@ -632,14 +627,14 @@ export async function runTicketAssign(cmdArgs, {
|
|
|
632
627
|
|
|
633
628
|
if (candidates.length > 1) {
|
|
634
629
|
stream.write(` ${candidates.length} users match "${to}" — narrow the query:\n\n`);
|
|
635
|
-
for (const c of candidates) stream.write(` ${s.brand('●')} ${c.displayName} ${s.dim(c.accountId)}\n`);
|
|
630
|
+
for (const c of candidates) stream.write(` ${s.brand('●')} ${c.displayName} ${s.dim(c.accountId ?? c.name)}\n`);
|
|
636
631
|
return { ok: false };
|
|
637
632
|
}
|
|
638
633
|
|
|
639
634
|
const [candidate] = candidates;
|
|
640
635
|
|
|
641
636
|
if (!cmdArgs.includes('--confirm')) {
|
|
642
|
-
stream.write(` Match: ${s.bold(candidate.displayName)} ${s.dim(candidate.accountId)}\n`);
|
|
637
|
+
stream.write(` Match: ${s.bold(candidate.displayName)} ${s.dim(candidate.accountId ?? candidate.name)}\n`);
|
|
643
638
|
stream.write(cliHints
|
|
644
639
|
? `\n Run again with --to="${to}" --confirm to execute.\n`
|
|
645
640
|
: `\n Call again with to="${to}" and confirm: true to execute.\n`);
|
|
@@ -655,8 +650,8 @@ export async function runTicketAssign(cmdArgs, {
|
|
|
655
650
|
}
|
|
656
651
|
|
|
657
652
|
try {
|
|
658
|
-
await adapter.assignToUser(ticketKey, candidate
|
|
659
|
-
logActionFn({ ticketKey, action: 'assign', actor, tracker: adapter.type, detail: { assignee: candidate.displayName, accountId: candidate.accountId } }, { configDir });
|
|
653
|
+
await adapter.assignToUser(ticketKey, candidate);
|
|
654
|
+
logActionFn({ ticketKey, action: 'assign', actor, tracker: adapter.type, detail: { assignee: candidate.displayName, accountId: candidate.accountId, name: candidate.name } }, { configDir });
|
|
660
655
|
stream.write(` ${s.green('✔')} ${s.brand(s.bold(ticketKey))} assigned to ${s.bold(candidate.displayName)}.\n`);
|
|
661
656
|
return { ok: true };
|
|
662
657
|
} catch (err) {
|