ticketlens 0.38.31 → 0.38.32
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
|
@@ -453,7 +453,7 @@ Every note is scanned before saving — anything shaped like a real secret (API
|
|
|
453
453
|
|
|
454
454
|
**Removing a note:** `ticketlens note delete --id="..." [--ticket=KEY]` removes a note from your local vault. Local only — if it was already pushed to a team, teammates who pulled it keep their copy; deleting it there too is a manager action from the Console (Admin > Recall).
|
|
455
455
|
|
|
456
|
-
**Any MCP-capable AI harness:** `ticketlens mcp` starts a stdio [MCP](https://modelcontextprotocol.io) server exposing `fetch`, `triage`, `compliance`, `doctor`, `recall_add`, `recall_search`, `ticket_comment`, `ticket_transition`, `ticket_assign`, `ticket_duplicates`, `ticket_link`, `ticket_update`, and `ticket_create` as native tools — any MCP-compatible AI assistant, not just Claude Code, can call them directly instead of constructing a shell command. It's a thin adapter over the exact same code as the CLI commands above — same license gate per tool, same secret scan/local vault/tracker writes, same team sync — nothing is reimplemented. `fetch` and `
|
|
456
|
+
**Any MCP-capable AI harness:** `ticketlens mcp` starts a stdio [MCP](https://modelcontextprotocol.io) server exposing `fetch`, `triage`, `compliance`, `review`, `standup`, `pr`, `doctor`, `recall_add`, `recall_search`, `ticket_comment`, `ticket_transition`, `ticket_assign`, `ticket_duplicates`, `ticket_link`, `ticket_update`, and `ticket_create` as native tools — any MCP-compatible AI assistant, not just Claude Code, can call them directly instead of constructing a shell command. It's a thin adapter over the exact same code as the CLI commands above — same license gate per tool, same secret scan/local vault/tracker writes, same team sync — nothing is reimplemented. `fetch`, `doctor`, and `standup` are Free; `triage`'s base scan is Free with some options gated Pro/Team, same as the CLI (`ticketlens triage --help`); `compliance` and `pr` are Free, sharing a 3-checks/month cap on their requirements-coverage section, Pro unlimited; `review` is Free for branch/files/ticket context, with its coverage/focus section requiring Pro as a plain license check — it does not draw from that same monthly counter; every other tool needs Pro. Point your harness's MCP config at it: `{ "command": "ticketlens", "args": ["mcp"] }` — or run `ticketlens mcp install` in a project to write that entry into its `.mcp.json` for you (creates the file if it doesn't exist, merges in if it does — never touches any other entry already there; `--dry-run` to preview first).
|
|
457
457
|
|
|
458
458
|
`note add`'s save confirmation and `recall`'s search results are styled by default in a terminal; add `--plain` to either for bare, pipe-safe output. `recall` always shows each note's file ID (e.g. `[1784135399545-fe01c4.md]`) so you can open it directly (`cat ~/.ticketlens/recall/<PREFIX>/<id>`), or pass `--full` to print the full body content inline instead. Each result shows a relative time (`2h ago`, `3d ago`) rather than a bare date — the full-precision timestamp is always in the note file's own frontmatter.
|
|
459
459
|
|
package/package.json
CHANGED
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.
|
|
1
|
+
<!-- jtb-skill-version: 0.37.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.
|
|
@@ -127,6 +127,63 @@ Display the script's stdout directly. No plan mode. Stop here.
|
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
+
### Standup subcommand
|
|
131
|
+
|
|
132
|
+
If the first argument is `standup`:
|
|
133
|
+
|
|
134
|
+
Run:
|
|
135
|
+
```bash
|
|
136
|
+
ticketlens standup $EXTRA_ARGS
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Where `$EXTRA_ARGS` are any flags passed (e.g. `--since=48 --format=pr --profile=acme`).
|
|
140
|
+
|
|
141
|
+
Summarizes recent git commits grouped by linked ticket key. `--since=N` (hours, default 24) or a git-compatible date expression (`--since="3 days ago"`); `--format=standup` (default) renders a per-ticket standup update, `--format=pr` renders the same grouped commits as PR-body-style markdown. Read-only, fully free tier — no license gate on any option.
|
|
142
|
+
|
|
143
|
+
Display the script's stdout directly. No plan mode. Stop here.
|
|
144
|
+
|
|
145
|
+
If this harness has TicketLens's MCP server configured (a tool named `standup` — often shown as `mcp__ticketlens__standup` — visible in your tool list), prefer it over the bash form: same output, no license gate, no shell command to construct. It accepts `since`/`format`/`profile`.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### PR subcommand
|
|
150
|
+
|
|
151
|
+
If the first argument is `pr`:
|
|
152
|
+
|
|
153
|
+
Run:
|
|
154
|
+
```bash
|
|
155
|
+
ticketlens pr PROJ-123 $EXTRA_ARGS
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Where `$EXTRA_ARGS` are any flags passed (e.g. `--profile=acme`). Requires a ticket key as the first positional argument.
|
|
159
|
+
|
|
160
|
+
Assembles a ready-to-paste PR description: what changed (from commits linked to the ticket), linked tickets, and — if the ticket has acceptance criteria — a requirements-coverage section. Read-only. The requirements-coverage section reuses the same Free-tier 3-checks/month counter as `--compliance`/`ticketlens compliance` — running `pr` on a ticket with acceptance criteria counts against that shared monthly limit; Pro removes the cap.
|
|
161
|
+
|
|
162
|
+
Display the script's stdout directly. No plan mode. Stop here.
|
|
163
|
+
|
|
164
|
+
If this harness has TicketLens's MCP server configured (a tool named `pr` — often shown as `mcp__ticketlens__pr` — visible in your tool list), prefer it over the bash form: same output, same shared compliance-counter caveat, no shell command to construct. It accepts `ticket`/`profile`.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### Review subcommand
|
|
169
|
+
|
|
170
|
+
If the first argument is `review`:
|
|
171
|
+
|
|
172
|
+
Run:
|
|
173
|
+
```bash
|
|
174
|
+
ticketlens review $EXTRA_ARGS
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Where `$EXTRA_ARGS` are any flags passed (e.g. `--base=develop --profile=acme`). No required positional argument — operates on the current branch.
|
|
178
|
+
|
|
179
|
+
Assembles PR review context from the current git branch: changed files, linked-ticket summaries from commits/branch name, and (Pro) a requirements-coverage / review-focus section extracted from the diff against acceptance criteria. `--base=BRANCH` (or its alias `--branch=BRANCH`) sets the diff base, auto-detecting `main`/`master`/`develop` when omitted. Read-only — never modifies the tracker or the repo. Free tier gets branch info, changed files, and ticket context; Pro adds the requirements-coverage and review-focus sections — same split as `--compliance`.
|
|
180
|
+
|
|
181
|
+
Display the script's stdout directly. No plan mode. Stop here.
|
|
182
|
+
|
|
183
|
+
If this harness has TicketLens's MCP server configured (a tool named `review` — often shown as `mcp__ticketlens__review` — visible in your tool list), prefer it over the bash form: same tier gate (Free: branch/files/tickets, Pro: +coverage/focus), no shell command to construct. It accepts `base`/`branch`/`profile` — `base` and `branch` are aliases; if both are given, `base` wins.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
130
187
|
### Fetch ticket workflow (default)
|
|
131
188
|
|
|
132
189
|
### Step 1: Validate environment
|
|
@@ -336,17 +336,20 @@ async function countRecallInjection(recallNotes, configDir, opts) {
|
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
339
|
+
// setInterval won't fire while spawnSync blocks the event loop, so we draw
|
|
340
|
+
// synchronously on update() and only use setInterval during async fetch phases.
|
|
341
|
+
// Both callers pass run()'s errStream, so the spinner shares the same isTTY and
|
|
342
|
+
// write target as everything else the command emits: the real process.stderr for
|
|
343
|
+
// a CLI user, the captured printErr under MCP (where the draws must not leak).
|
|
344
|
+
function makeSpinner(s, { isTTY, write }) {
|
|
345
|
+
if (!isTTY) return { update: () => {}, startAnim: () => {}, done: () => {} };
|
|
343
346
|
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
344
347
|
let fi = 0, msg = '', lastW = 0, timerId = null, finished = false;
|
|
345
348
|
|
|
346
349
|
const draw = () => {
|
|
347
350
|
const raw = ` ${s.brand(frames[fi % frames.length])} ${s.dim(msg)}`;
|
|
348
351
|
const vis = raw.replace(/\x1b\[[0-9;]*m/g, '');
|
|
349
|
-
|
|
352
|
+
write(`\r${raw}${' '.repeat(Math.max(0, lastW - vis.length))}`);
|
|
350
353
|
lastW = vis.length;
|
|
351
354
|
};
|
|
352
355
|
|
|
@@ -360,7 +363,7 @@ function makeSpinner(s) {
|
|
|
360
363
|
if (finished) return;
|
|
361
364
|
finished = true;
|
|
362
365
|
if (timerId) { clearInterval(timerId); timerId = null; }
|
|
363
|
-
|
|
366
|
+
write('\r\x1b[2K');
|
|
364
367
|
},
|
|
365
368
|
};
|
|
366
369
|
}
|
|
@@ -485,10 +488,10 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
485
488
|
}
|
|
486
489
|
|
|
487
490
|
const printFn = opts.print ?? ((chunk) => process.stdout.write(chunk));
|
|
488
|
-
// Mirrors printFn above —
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
491
|
+
// Mirrors printFn above — the bare ticket-fetch, compliance, pr, review, and standup
|
|
492
|
+
// paths all use this now (each has an MCP tool). `ledger` above still writes directly
|
|
493
|
+
// to the real process.stderr (no MCP tool yet); `install-hooks` is CLI-only and never
|
|
494
|
+
// gets one.
|
|
492
495
|
const printErrFn = opts.printErr ?? ((chunk) => process.stderr.write(chunk));
|
|
493
496
|
// Reused on every recursive self-call in the bare-fetch path (profile-prompt retries)
|
|
494
497
|
// so an injected print/printErr survives the retry instead of silently reverting to
|
|
@@ -549,12 +552,12 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
549
552
|
const { assemblePr } = await import('./lib/pr-assembler.mjs');
|
|
550
553
|
const ticketKeyArg = args[1];
|
|
551
554
|
if (!ticketKeyArg) {
|
|
552
|
-
|
|
555
|
+
printErrFn('Error: "pr" requires a ticket key. Usage: ticketlens pr PROJ-123\n');
|
|
553
556
|
process.exitCode = 1;
|
|
554
557
|
return;
|
|
555
558
|
}
|
|
556
559
|
if (!TICKET_KEY_PATTERN.test(ticketKeyArg)) {
|
|
557
|
-
|
|
560
|
+
printErrFn(`Error: "${ticketKeyArg}" is not a valid ticket key. Expected format: PROJ-123\n`);
|
|
558
561
|
process.exitCode = 1;
|
|
559
562
|
return;
|
|
560
563
|
}
|
|
@@ -568,27 +571,33 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
568
571
|
configDir: resolvedConfigDir,
|
|
569
572
|
profileName: profileNamePr,
|
|
570
573
|
cwd: process.cwd(),
|
|
571
|
-
onWarning: (w) =>
|
|
574
|
+
onWarning: (w) => printErrFn(w + '\n'),
|
|
572
575
|
onProfileNotFound: () => {},
|
|
573
576
|
});
|
|
574
577
|
|
|
575
578
|
const hasAuthPr = connPr.pat || (connPr.email && connPr.apiToken);
|
|
576
579
|
if (!connPr.baseUrl || !hasAuthPr) {
|
|
577
|
-
|
|
580
|
+
printErrFn('Error: No Jira credentials found. Run \'ticketlens init\' or set JIRA_BASE_URL + JIRA_API_TOKEN.\n');
|
|
578
581
|
process.exitCode = 1;
|
|
579
582
|
return;
|
|
580
583
|
}
|
|
581
584
|
|
|
582
585
|
const adapterPr = resolveAdapter(connPr, { fetcher });
|
|
586
|
+
const complianceRunnerPr = opts.runComplianceCheck ?? runComplianceCheck;
|
|
583
587
|
|
|
584
588
|
try {
|
|
585
589
|
const md = await assemblePr(ticketKeyArg, {
|
|
586
590
|
configDir: resolvedConfigDir,
|
|
587
591
|
fetchTicketFn: (key, fOpts = {}) => adapterPr.fetchTicket(key, fOpts),
|
|
592
|
+
// assemblePr's own runComplianceCheckFn default has no stream override
|
|
593
|
+
// (compliance-checker.mjs's `stream = process.stderr`), so the Pro-gate
|
|
594
|
+
// upgrade prompt would leak to real stderr unless threaded here — same
|
|
595
|
+
// treatment as the compliance dispatch block above (`stream: errStream`).
|
|
596
|
+
runComplianceCheckFn: (o) => complianceRunnerPr({ ...o, stream: errStream }),
|
|
588
597
|
});
|
|
589
598
|
printFn(md + '\n');
|
|
590
599
|
} catch (err) {
|
|
591
|
-
|
|
600
|
+
printErrFn(`Error: ${err.message}\n`);
|
|
592
601
|
process.exitCode = 1;
|
|
593
602
|
}
|
|
594
603
|
return;
|
|
@@ -710,7 +719,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
710
719
|
const execFn = opts.execFn ?? spawnSync;
|
|
711
720
|
const cwd = process.cwd();
|
|
712
721
|
|
|
713
|
-
const sErr = createStyler({ isTTY
|
|
722
|
+
const sErr = createStyler({ isTTY });
|
|
714
723
|
|
|
715
724
|
// Validate flags before any git work
|
|
716
725
|
const reviewFlags = args.slice(1);
|
|
@@ -721,28 +730,28 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
721
730
|
// Detect --profile-NAME typo (dash instead of =)
|
|
722
731
|
const profileDashM = flag.match(/^--profile-(.+)$/);
|
|
723
732
|
if (profileDashM) {
|
|
724
|
-
|
|
733
|
+
printErrFn(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--profile=${profileDashM[1]}`)}?\n`);
|
|
725
734
|
process.exitCode = 1;
|
|
726
735
|
return;
|
|
727
736
|
}
|
|
728
737
|
const baseDashM = flag.match(/^--base-(.+)$/);
|
|
729
738
|
if (baseDashM) {
|
|
730
|
-
|
|
739
|
+
printErrFn(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--base=${baseDashM[1]}`)}?\n`);
|
|
731
740
|
process.exitCode = 1;
|
|
732
741
|
return;
|
|
733
742
|
}
|
|
734
743
|
const branchDashM = flag.match(/^--branch-(.+)$/);
|
|
735
744
|
if (branchDashM) {
|
|
736
|
-
|
|
745
|
+
printErrFn(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--branch=${branchDashM[1]}`)}?\n`);
|
|
737
746
|
process.exitCode = 1;
|
|
738
747
|
return;
|
|
739
748
|
}
|
|
740
|
-
|
|
749
|
+
printErrFn(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Usage: ${sErr.cyan('ticketlens review [--base=BRANCH] [--branch=BRANCH] [--profile=NAME]')}\n`);
|
|
741
750
|
process.exitCode = 1;
|
|
742
751
|
return;
|
|
743
752
|
}
|
|
744
753
|
|
|
745
|
-
const spinner = makeSpinner(sErr);
|
|
754
|
+
const spinner = makeSpinner(sErr, errStream);
|
|
746
755
|
const stderrNotes = [];
|
|
747
756
|
|
|
748
757
|
// Resolve base branch: --base=BRANCH or auto-detect main/master/develop
|
|
@@ -756,7 +765,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
756
765
|
const verifyR = execFn('git', ['rev-parse', '--verify', baseBranch], { encoding: 'utf8', cwd, timeout: 5_000 });
|
|
757
766
|
if (verifyR.status !== 0) {
|
|
758
767
|
spinner.done();
|
|
759
|
-
|
|
768
|
+
printErrFn(`${sErr.red('✖')} Branch "${baseBranch}" not found in this repository.\n`);
|
|
760
769
|
process.exitCode = 1;
|
|
761
770
|
return;
|
|
762
771
|
}
|
|
@@ -794,10 +803,10 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
794
803
|
const profiles = loadProfiles(resolvedConfigDir);
|
|
795
804
|
if (!profiles?.profiles?.[profileNameR]) {
|
|
796
805
|
spinner.done();
|
|
797
|
-
|
|
806
|
+
printErrFn(`${sErr.red('✖')} Profile "${profileNameR}" not found.\n`);
|
|
798
807
|
const names = Object.keys(profiles?.profiles ?? {});
|
|
799
|
-
if (names.length > 0)
|
|
800
|
-
else
|
|
808
|
+
if (names.length > 0) printErrFn(` ${sErr.dim('Available:')} ${names.join(', ')}\n`);
|
|
809
|
+
else printErrFn(` Run ${sErr.cyan('ticketlens init')} to configure a profile.\n`);
|
|
801
810
|
process.exitCode = 1;
|
|
802
811
|
return;
|
|
803
812
|
}
|
|
@@ -839,8 +848,8 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
839
848
|
}
|
|
840
849
|
|
|
841
850
|
spinner.done();
|
|
842
|
-
for (const note of stderrNotes)
|
|
843
|
-
if (stderrNotes.length > 0)
|
|
851
|
+
for (const note of stderrNotes) printErrFn(note + '\n');
|
|
852
|
+
if (stderrNotes.length > 0) printErrFn('\n');
|
|
844
853
|
|
|
845
854
|
const isLic = opts.isLicensedFn ?? ((tier) => isLicensed(tier, resolvedConfigDir));
|
|
846
855
|
const assembleFn = opts.assemblePrReviewFn ?? assemblePrReview;
|
|
@@ -867,7 +876,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
867
876
|
const resolvedConfigDir = configDir ?? (await import('./lib/config.mjs')).DEFAULT_CONFIG_DIR;
|
|
868
877
|
const execFn = opts.execFn ?? spawnSync;
|
|
869
878
|
const cwd = process.cwd();
|
|
870
|
-
const sErr = createStyler({ isTTY
|
|
879
|
+
const sErr = createStyler({ isTTY });
|
|
871
880
|
|
|
872
881
|
const standupFlags = args.slice(1);
|
|
873
882
|
|
|
@@ -878,29 +887,29 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
878
887
|
|
|
879
888
|
const sinceDashM = flag.match(/^--since-(.+)$/);
|
|
880
889
|
if (sinceDashM) {
|
|
881
|
-
|
|
890
|
+
printErrFn(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--since=${sinceDashM[1]}`)}?\n`);
|
|
882
891
|
process.exitCode = 1;
|
|
883
892
|
return;
|
|
884
893
|
}
|
|
885
894
|
const formatDashM = flag.match(/^--format-(.+)$/);
|
|
886
895
|
if (formatDashM) {
|
|
887
|
-
|
|
896
|
+
printErrFn(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--format=${formatDashM[1]}`)}?\n`);
|
|
888
897
|
process.exitCode = 1;
|
|
889
898
|
return;
|
|
890
899
|
}
|
|
891
900
|
const profileDashM = flag.match(/^--profile-(.+)$/);
|
|
892
901
|
if (profileDashM) {
|
|
893
|
-
|
|
902
|
+
printErrFn(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--profile=${profileDashM[1]}`)}?\n`);
|
|
894
903
|
process.exitCode = 1;
|
|
895
904
|
return;
|
|
896
905
|
}
|
|
897
906
|
const formatValueM = flag.match(/^--format=(.+)$/);
|
|
898
907
|
if (formatValueM) {
|
|
899
|
-
|
|
908
|
+
printErrFn(`${sErr.red('✖')} Invalid --format value: "${formatValueM[1]}". Expected: standup or pr\n`);
|
|
900
909
|
process.exitCode = 1;
|
|
901
910
|
return;
|
|
902
911
|
}
|
|
903
|
-
|
|
912
|
+
printErrFn(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Usage: ${sErr.cyan('ticketlens standup [--since=N] [--format=standup|pr] [--profile=NAME]')}\n`);
|
|
904
913
|
process.exitCode = 1;
|
|
905
914
|
return;
|
|
906
915
|
}
|
|
@@ -921,16 +930,16 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
921
930
|
if (profileNameS) {
|
|
922
931
|
const profiles = loadProfiles(resolvedConfigDir);
|
|
923
932
|
if (!profiles?.profiles?.[profileNameS]) {
|
|
924
|
-
|
|
933
|
+
printErrFn(`${sErr.red('✖')} Profile "${profileNameS}" not found.\n`);
|
|
925
934
|
const names = Object.keys(profiles?.profiles ?? {});
|
|
926
|
-
if (names.length > 0)
|
|
927
|
-
else
|
|
935
|
+
if (names.length > 0) printErrFn(` ${sErr.dim('Available:')} ${names.join(', ')}\n`);
|
|
936
|
+
else printErrFn(` Run ${sErr.cyan('ticketlens init')} to configure a profile.\n`);
|
|
928
937
|
process.exitCode = 1;
|
|
929
938
|
return;
|
|
930
939
|
}
|
|
931
940
|
}
|
|
932
941
|
|
|
933
|
-
const spinner = makeSpinner(sErr);
|
|
942
|
+
const spinner = makeSpinner(sErr, errStream);
|
|
934
943
|
const stderrNotes = [];
|
|
935
944
|
|
|
936
945
|
spinner.update('Scanning git log…');
|
|
@@ -977,8 +986,8 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
977
986
|
}
|
|
978
987
|
|
|
979
988
|
spinner.done();
|
|
980
|
-
for (const note of stderrNotes)
|
|
981
|
-
if (stderrNotes.length > 0)
|
|
989
|
+
for (const note of stderrNotes) printErrFn(note + '\n');
|
|
990
|
+
if (stderrNotes.length > 0) printErrFn('\n');
|
|
982
991
|
|
|
983
992
|
const isPlain = standupFlags.includes('--plain');
|
|
984
993
|
const assembleStandupFn = opts.assembleStandupFn ?? assembleStandup;
|
|
@@ -645,17 +645,21 @@ export function printMcpHelp({ stream = process.stdout } = {}) {
|
|
|
645
645
|
` ${s.bold(s.brand('ticketlens'))} ${s.bold('mcp')}`,
|
|
646
646
|
'',
|
|
647
647
|
` Start an MCP (Model Context Protocol) stdio server exposing CLI actions as`,
|
|
648
|
-
` native tools — ${s.cyan('fetch')}, ${s.cyan('triage')}, ${s.cyan('compliance')}, ${s.cyan('
|
|
649
|
-
` ${s.cyan('
|
|
650
|
-
` MCP-compatible AI harness, not just Claude Code. Thin
|
|
651
|
-
` code as ${s.cyan('TICKET-KEY')}/${s.cyan('doctor')}/${s.cyan('triage')}/${s.cyan('compliance')}/${s.cyan('
|
|
652
|
-
`
|
|
653
|
-
`
|
|
654
|
-
`
|
|
655
|
-
`
|
|
656
|
-
`
|
|
657
|
-
`
|
|
658
|
-
`
|
|
648
|
+
` native tools — ${s.cyan('fetch')}, ${s.cyan('triage')}, ${s.cyan('compliance')}, ${s.cyan('review')}, ${s.cyan('standup')}, ${s.cyan('pr')}, ${s.cyan('doctor')}, ${s.cyan('recall_add')}, ${s.cyan('recall_search')},`,
|
|
649
|
+
` ${s.cyan('ticket_comment')}, ${s.cyan('ticket_transition')}, ${s.cyan('ticket_assign')}, ${s.cyan('ticket_duplicates')}, ${s.cyan('ticket_link')}, ${s.cyan('ticket_update')},`,
|
|
650
|
+
` ${s.cyan('ticket_create')} — for any MCP-compatible AI harness, not just Claude Code. Thin`,
|
|
651
|
+
` adapter over the same code as ${s.cyan('TICKET-KEY')}/${s.cyan('doctor')}/${s.cyan('triage')}/${s.cyan('compliance')}/${s.cyan('review')}/${s.cyan('standup')}/${s.cyan('pr')}/${s.cyan('note add')}/`,
|
|
652
|
+
` ${s.cyan('recall')}/${s.cyan('comment')}/${s.cyan('transition')}/${s.cyan('assign')}/${s.cyan('duplicates')}/${s.cyan('link')}/${s.cyan('update')}/${s.cyan('create')} above.`,
|
|
653
|
+
` ${s.cyan('fetch')}, ${s.cyan('doctor')}, and ${s.cyan('standup')} are Free; ${s.cyan('triage')} is Free with some Pro/Team-gated`,
|
|
654
|
+
` options (see ${s.cyan('ticketlens triage --help')}); ${s.cyan('compliance')} and ${s.cyan('pr')} are Free, sharing a`,
|
|
655
|
+
` 3-checks/month cap on their requirements-coverage section, Pro unlimited;`,
|
|
656
|
+
` ${s.cyan('review')} is Free for branch/files/ticket context — its coverage/focus section`,
|
|
657
|
+
` requires Pro as a plain license check, not a draw on that same counter; every`,
|
|
658
|
+
` other tool needs Pro. ${s.cyan('ticket_transition')} is destructive when called with`,
|
|
659
|
+
` \`target\`+\`confirm: true\`; ${s.cyan('ticket_assign')} is currently self-assign only;`,
|
|
660
|
+
` ${s.cyan('ticket_duplicates')} is read-only; ${s.cyan('ticket_link')} on GitHub closes the source issue as a`,
|
|
661
|
+
` duplicate — different semantics than Jira/Linear's relationship-only add;`,
|
|
662
|
+
` ${s.cyan('ticket_update')} has no priority field on GitHub and can partially succeed;`,
|
|
659
663
|
` ${s.cyan('ticket_create')} has no ticket key to target — --profile/the default profile picks the`,
|
|
660
664
|
` tracker, and it fabricates a real item, the highest blast radius of this family.`,
|
|
661
665
|
` Long-running — exits when the client closes stdin.`,
|
|
@@ -78,6 +78,42 @@ const TOOLS = [
|
|
|
78
78
|
required: ['ticket'],
|
|
79
79
|
},
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
name: 'review',
|
|
83
|
+
description: 'Assemble PR review context from the current git branch — changed files, linked-ticket summaries, and (Pro) a requirements-coverage / review-focus section extracted from the diff against acceptance criteria. Read-only; never modifies the tracker or the repo. Free tier gets branch info, changed files, and ticket context; TicketLens Pro adds the requirements-coverage and review-focus sections — same split as the `compliance` tool.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
base: { type: 'string', description: 'Base branch to diff against. Auto-detects main/master/develop when omitted. Alias of `branch` — if both are given, `base` wins.' },
|
|
88
|
+
branch: { type: 'string', description: 'Alias for `base` — same effect, provided for parity with the CLI\'s `--branch=` flag.' },
|
|
89
|
+
profile: { type: 'string', description: 'Connection profile to target, overriding folder-based inference and the default profile.' },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'standup',
|
|
95
|
+
description: 'Summarize recent git commits grouped by linked ticket — a standup update or, with format:"pr", PR-body-style formatting. Read-only, fully free tier — no license gate on any option.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
since: { type: 'string', description: 'How far back to scan — an integer number of hours ("24") or a git-compatible date expression ("3 days ago"). Defaults to 24 hours.' },
|
|
100
|
+
format: { type: 'string', enum: ['standup', 'pr'], description: 'Output shape: "standup" (default) groups commits under a per-ticket standup update; "pr" renders the same grouped commits as PR-body-style markdown.' },
|
|
101
|
+
profile: { type: 'string', description: 'Connection profile to target, overriding folder-based inference and the default profile.' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'pr',
|
|
107
|
+
description: 'Assemble a ready-to-paste PR description for a ticket — what changed (from linked commits), linked tickets, and (if the ticket has acceptance criteria) a requirements-coverage section. Read-only. The requirements-coverage section reuses the same Free-tier 3-checks/month counter as the `compliance` tool — calling `pr` on a ticket with acceptance criteria counts against that shared monthly limit; TicketLens Pro removes the cap.',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
ticket: { type: 'string', description: 'Ticket key, e.g. PROJ-123.' },
|
|
112
|
+
profile: { type: 'string', description: 'Connection profile to target, overriding folder-based inference and the default profile.' },
|
|
113
|
+
},
|
|
114
|
+
required: ['ticket'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
81
117
|
{
|
|
82
118
|
name: 'doctor',
|
|
83
119
|
description: 'Diagnose common TicketLens problems: profile configuration, license freshness, tracker connectivity, attachment cache health, MCP registration, and the Recall sync queue. Always returns structured JSON. Free tier, fully unrestricted — including fix.',
|
|
@@ -249,8 +285,8 @@ function buildFetchArgs({ ticket, profile, depth }) {
|
|
|
249
285
|
|
|
250
286
|
/**
|
|
251
287
|
* Shared by every dispatch that goes through fetch-ticket.mjs's `run()`
|
|
252
|
-
* (currently `fetch
|
|
253
|
-
*
|
|
288
|
+
* (currently `fetch`, `compliance`, `review`, `standup`, and `pr` — all but
|
|
289
|
+
* `fetch` are subcommands of that same function). `run()` has no {ok} return value
|
|
254
290
|
* (unlike every other wrapped function) — failure is signaled by mutating
|
|
255
291
|
* process.exitCode, which is unsafe to read in a long-lived server (one
|
|
256
292
|
* failed call would poison the whole process's exit code forever). Success
|
|
@@ -371,6 +407,51 @@ async function callCompliance(args, deps) {
|
|
|
371
407
|
return callFetchTicketRun(buildComplianceArgs, args, deps, 'compliance check failed');
|
|
372
408
|
}
|
|
373
409
|
|
|
410
|
+
/**
|
|
411
|
+
* `review`/`standup`/`pr` are all subcommands of the same fetch-ticket.mjs
|
|
412
|
+
* `run()` that `callFetch`/`callCompliance` already wrap — reuse
|
|
413
|
+
* `runFetchTicketFn`, no new dependency. Each subcommand's error/progress
|
|
414
|
+
* paths (including makeSpinner's writes) were threaded through opts.printErr
|
|
415
|
+
* as part of adding these tools — see fetch-ticket.mjs's `review`/`standup`/
|
|
416
|
+
* `pr` dispatch blocks and makeSpinner's injectable {isTTY, write}.
|
|
417
|
+
*/
|
|
418
|
+
function buildReviewArgs({ base, branch, profile }) {
|
|
419
|
+
const args = ['review'];
|
|
420
|
+
if (base) args.push(`--base=${base}`);
|
|
421
|
+
else if (branch) args.push(`--branch=${branch}`);
|
|
422
|
+
if (profile) args.push(`--profile=${profile}`);
|
|
423
|
+
return args;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function callReview(args, deps) {
|
|
427
|
+
return callFetchTicketRun(buildReviewArgs, args, deps, 'review failed');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function buildStandupArgs({ since, format, profile }) {
|
|
431
|
+
const args = ['standup'];
|
|
432
|
+
if (since !== undefined) args.push(`--since=${since}`);
|
|
433
|
+
if (format) args.push(`--format=${format}`);
|
|
434
|
+
if (profile) args.push(`--profile=${profile}`);
|
|
435
|
+
return args;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function callStandup(args, deps) {
|
|
439
|
+
return callFetchTicketRun(buildStandupArgs, args, deps, 'standup failed');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildPrArgs({ ticket, profile }) {
|
|
443
|
+
const args = ['pr', ticket];
|
|
444
|
+
if (profile) args.push(`--profile=${profile}`);
|
|
445
|
+
return args;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function callPr(args, deps) {
|
|
449
|
+
if (!args.ticket) {
|
|
450
|
+
return { isError: true, content: [{ type: 'text', text: 'Missing required argument: ticket' }] };
|
|
451
|
+
}
|
|
452
|
+
return callFetchTicketRun(buildPrArgs, args, deps, 'pr failed');
|
|
453
|
+
}
|
|
454
|
+
|
|
374
455
|
function buildDoctorArgs({ fix, profile }) {
|
|
375
456
|
const args = ['--format=json'];
|
|
376
457
|
if (fix === true) args.push('--fix');
|
|
@@ -597,6 +678,9 @@ async function handleToolsCall(params, deps) {
|
|
|
597
678
|
if (name === 'fetch') return callFetch(args, deps);
|
|
598
679
|
if (name === 'triage') return callTriage(args, deps);
|
|
599
680
|
if (name === 'compliance') return callCompliance(args, deps);
|
|
681
|
+
if (name === 'review') return callReview(args, deps);
|
|
682
|
+
if (name === 'standup') return callStandup(args, deps);
|
|
683
|
+
if (name === 'pr') return callPr(args, deps);
|
|
600
684
|
if (name === 'doctor') return callDoctor(args, deps);
|
|
601
685
|
if (name === 'recall_add') return callRecallAdd(args, deps);
|
|
602
686
|
if (name === 'recall_search') return callRecallSearch(args, deps);
|