ticketlens 0.38.21 → 0.38.22

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
@@ -529,11 +529,12 @@ A one-line summary footer is also appended automatically to `ticketlens triage`
529
529
  ### Doctor
530
530
 
531
531
  ```bash
532
- ticketlens doctor # Diagnose profile/license/connectivity/cache/queue problems
533
- ticketlens doctor --fix # Attempt safe automatic fixes (license revalidation, corrupt cache cleanup, queue flush)
532
+ ticketlens doctor # Diagnose profile/license/connectivity/cache/MCP-registration/queue problems
533
+ ticketlens doctor --fix # Attempt safe automatic fixes (license revalidation, corrupt cache cleanup, MCP registration, queue flush)
534
534
  ticketlens doctor --profile=acme # Scope checks to a single profile
535
535
  ticketlens doctor --format=json # JSON output for scripting/piping
536
536
  ticketlens doctor --format=json | jq '.ok'
537
+ ticketlens doctor --mcp # Also check the MCP server handshake (spawns a subprocess)
537
538
  ```
538
539
 
539
540
  Runs five checks — profile configuration, license freshness, tracker connectivity, attachment cache health, and the Recall sync queue — and reports pass/fail with an actionable hint per failure, instead of a raw stack trace. Free tier, fully unrestricted; no license required.
@@ -817,10 +818,11 @@ ticketlens stats --days=14 # Extend lookback window (Pro, max
817
818
  ticketlens stats --format=json # JSON output for scripting
818
819
 
819
820
  # ── Doctor ────────────────────────────────────────────────────────────────────
820
- ticketlens doctor # Diagnose profile/license/connectivity/cache/queue problems
821
+ ticketlens doctor # Diagnose profile/license/connectivity/cache/MCP-registration/queue problems
821
822
  ticketlens doctor --fix # Attempt safe automatic fixes
822
823
  ticketlens doctor --profile=acme # Scope checks to a single profile
823
824
  ticketlens doctor --format=json # JSON output for scripting/piping
825
+ ticketlens doctor --mcp # Also check the MCP server handshake (spawns a subprocess)
824
826
 
825
827
  # ── Compliance ────────────────────────────────────────────────────────────────
826
828
  ticketlens compliance <TICKET-KEY> # Check ticket requirements against local diff [Pro/Free 3/mo]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.38.21",
3
+ "version": "0.38.22",
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": {
@@ -1,4 +1,4 @@
1
- <!-- jtb-skill-version: 0.34.0 -->
1
+ <!-- jtb-skill-version: 0.34.1 -->
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.
@@ -314,16 +314,17 @@ Requires a Pro license — on Free, all seven no-op with an upgrade hint on stde
314
314
 
315
315
  ## Doctor — diagnose local/tracker problems (Free)
316
316
 
317
- `ticketlens doctor` runs five fixed checks — profile configuration, license freshness, tracker connectivity, attachment cache health, and the Recall sync queue — and returns a pass/fail report with an actionable hint per failure, instead of a raw stack trace. Free tier, fully unrestricted; nothing here is gated.
317
+ `ticketlens doctor` runs six fixed checks by default — profile configuration, license freshness, tracker connectivity, attachment cache health, MCP registration, and the Recall sync queue — and returns a pass/fail report with an actionable hint per failure, instead of a raw stack trace. Pass `--mcp` to also run a seventh, opt-in check: a real MCP server handshake (spawns `ticketlens mcp`, confirms it answers the JSON-RPC `initialize` request). Free tier, fully unrestricted; nothing here is gated.
318
318
 
319
319
  ```bash
320
- ticketlens doctor # run all checks
321
- ticketlens doctor --fix # attempt safe automatic fixes (license revalidation, corrupt cache cleanup, queue flush)
320
+ ticketlens doctor # run the six default checks
321
+ ticketlens doctor --fix # attempt safe automatic fixes (license revalidation, corrupt cache cleanup, MCP registration, queue flush)
322
322
  ticketlens doctor --profile=acme # scope checks to a single profile
323
323
  ticketlens doctor --format=json # structured output for scripting/piping
324
+ ticketlens doctor --mcp # also check the MCP server handshake (spawns a subprocess)
324
325
  ```
325
326
 
326
- If this harness has TicketLens's MCP server configured (a tool named `doctor` — often shown as `mcp__ticketlens__doctor` — visible in your tool list), prefer it over the bash form: it always requests the JSON report internally and returns it as the tool's text content, so you get a structured result to reason over directly instead of parsing CLI stdout. It accepts the same `fix`/`profile` options as the CLI flags above.
327
+ If this harness has TicketLens's MCP server configured (a tool named `doctor` — often shown as `mcp__ticketlens__doctor` — visible in your tool list), prefer it over the bash form: it always requests the JSON report internally and returns it as the tool's text content, so you get a structured result to reason over directly instead of parsing CLI stdout. It accepts the same `fix`/`profile` options as the CLI flags above (the `--mcp` handshake check is CLI-only — deliberately not exposed as an MCP tool option, since a successful MCP tool call already proves the handshake works).
327
328
 
328
329
  A report with `ok: false` is a successful tool call describing failures, not a tool error — read the `checks[]` array for what's failing and act on each entry's `hint`, don't treat the call itself as having failed.
329
330
 
@@ -10,8 +10,8 @@ import { getVersion } from './config.mjs';
10
10
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
11
11
  const visibleLength = (str) => str.replace(ANSI_RE, '').length;
12
12
 
13
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
14
- const SPINNER_INTERVAL = 80;
13
+ export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
14
+ export const SPINNER_INTERVAL = 80;
15
15
 
16
16
 
17
17
  function buildBox(lines, { s, borderColor = 'cyan' }) {
@@ -8,6 +8,7 @@ import fs from 'node:fs';
8
8
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
9
9
  import { handleUnknownFlags } from './arg-validator.mjs';
10
10
  import { createStyler } from './ansi.mjs';
11
+ import { SPINNER_FRAMES, SPINNER_INTERVAL } from './banner.mjs';
11
12
  import {
12
13
  checkProfileConfig, checkLicenseFreshness, checkConnectivity,
13
14
  checkCacheHealth, checkRecallQueue, checkMcpRegistration, checkMcpHandshake,
@@ -25,23 +26,32 @@ const NOOP_STREAM = { write: () => true };
25
26
  // (e.g. "Profile configuration") gets folded to start a sentence mid-line.
26
27
  const lowerFirst = (str) => (/^[A-Z]{2,}/.test(str) ? str : str.charAt(0).toLowerCase() + str.slice(1));
27
28
 
28
- function renderPlain(checks, { fixed, skipped, stream }) {
29
- const s = createStyler({ isTTY: stream.isTTY });
30
- stream.write('\n');
31
- for (const check of checks) {
32
- const icon = check.ok ? s.green('✔') : s.red('✖');
33
- stream.write(` ${icon} ${check.label}: ${check.message}\n`);
34
- if (!check.ok && check.hint) {
35
- for (const line of check.hint.split('\n')) stream.write(` ${s.dim(line)}\n`);
36
- }
29
+ function renderCheckRow(check, s) {
30
+ const icon = check.ok ? s.green('✔') : s.red('✖');
31
+ let text = ` ${icon} ${check.label}: ${check.message}\n`;
32
+ if (!check.ok && check.hint) {
33
+ for (const line of check.hint.split('\n')) text += ` ${s.dim(line)}\n`;
37
34
  }
35
+ return text;
36
+ }
37
+
38
+ function renderTrailer({ fixed, skipped }, s) {
39
+ let text = '';
38
40
  if (fixed.length > 0) {
39
- stream.write(`\n ${s.green('Fixed:')} ${fixed.join(', ')}\n`);
41
+ text += `\n ${s.green('Fixed:')} ${fixed.join(', ')}\n`;
40
42
  }
41
43
  if (skipped.length > 0) {
42
- stream.write(`\n ${s.yellow('Skipped:')}\n`);
43
- for (const sk of skipped) stream.write(` ${sk.id}: ${sk.reason}\n`);
44
+ text += `\n ${s.yellow('Skipped:')}\n`;
45
+ for (const sk of skipped) text += ` ${sk.id}: ${sk.reason}\n`;
44
46
  }
47
+ return text;
48
+ }
49
+
50
+ function renderPlain(checks, { fixed, skipped, stream }) {
51
+ const s = createStyler({ isTTY: stream.isTTY });
52
+ stream.write('\n');
53
+ for (const check of checks) stream.write(renderCheckRow(check, s));
54
+ stream.write(renderTrailer({ fixed, skipped }, s));
45
55
  stream.write('\n');
46
56
  }
47
57
 
@@ -142,14 +152,52 @@ export async function runDoctor(args, {
142
152
  ];
143
153
 
144
154
  const showProgress = format === 'plain' && stream.isTTY;
145
- const s = createStyler({ isTTY: stream.isTTY });
155
+ const loaderStyler = createStyler({ isTTY: stream.isTTY });
156
+ const outStyler = createStyler({ isTTY: out.isTTY });
146
157
  const rawResults = [];
147
- for (const { label, run } of checkList) {
148
- if (showProgress) stream.write(` ${s.dim(`○ Checking ${lowerFirst(label)}…`)}\n`);
149
- try {
150
- rawResults.push(await run());
151
- } finally {
152
- if (showProgress) stream.write('\x1b[A\r\x1b[2K');
158
+
159
+ const writeSpinnerLine = (label, frame) => {
160
+ stream.write(` ${loaderStyler.brand(SPINNER_FRAMES[frame])} ${loaderStyler.dim(`Checking ${lowerFirst(label)}…`)}\n`);
161
+ };
162
+
163
+ // Ctrl+C during a slow check (e.g. tracker connectivity) must not leave the
164
+ // user's real terminal cursor permanently hidden — Node's default SIGINT
165
+ // handling terminates before pending finally blocks on an in-flight await
166
+ // run, so this needs its own listener. Same pattern as init-wizard.mjs.
167
+ const onSigint = () => { stream.write('\x1b[?25h'); process.exit(130); };
168
+ if (showProgress) {
169
+ out.write('\n');
170
+ stream.write('\x1b[?25l'); // hide cursor for the whole check-running phase
171
+ process.on('SIGINT', onSigint);
172
+ }
173
+ try {
174
+ for (const { label, run } of checkList) {
175
+ let frame = 0;
176
+ let timer;
177
+ if (showProgress) {
178
+ writeSpinnerLine(label, frame);
179
+ timer = setInterval(() => {
180
+ frame = (frame + 1) % SPINNER_FRAMES.length;
181
+ stream.write('\x1b[A\r\x1b[2K');
182
+ writeSpinnerLine(label, frame);
183
+ }, SPINNER_INTERVAL);
184
+ }
185
+ let result;
186
+ try {
187
+ result = await run();
188
+ } finally {
189
+ if (showProgress) {
190
+ clearInterval(timer);
191
+ stream.write('\x1b[A\r\x1b[2K');
192
+ }
193
+ }
194
+ rawResults.push(result);
195
+ if (showProgress) out.write(renderCheckRow(result, outStyler));
196
+ }
197
+ } finally {
198
+ if (showProgress) {
199
+ stream.write('\x1b[?25h'); // restore cursor
200
+ process.removeListener('SIGINT', onSigint);
153
201
  }
154
202
  }
155
203
 
@@ -177,6 +225,15 @@ export async function runDoctor(args, {
177
225
  return { ok };
178
226
  }
179
227
 
180
- renderPlain(checks, { fixed, skipped, stream: out });
228
+ if (showProgress) {
229
+ // Rows already streamed to `out` progressively during the check loop
230
+ // (each check's raw, first-observed state — a later `--fix` never
231
+ // rewrites an already-shown row; the trailing Fixed:/Skipped: block
232
+ // below is the sole signal of what got repaired).
233
+ out.write(renderTrailer({ fixed, skipped }, outStyler));
234
+ out.write('\n');
235
+ } else {
236
+ renderPlain(checks, { fixed, skipped, stream: out });
237
+ }
181
238
  return { ok };
182
239
  }
@@ -1240,6 +1240,7 @@ export function printDoctorHelp({ stream = process.stdout } = {}) {
1240
1240
  ` ${s.bold('OPTIONS')}`,
1241
1241
  '',
1242
1242
  ` ${s.brand('--fix')} Attempt safe, non-destructive repairs for failing checks`,
1243
+ ` ${s.dim('(rows show pre-fix status; see the Fixed: summary for what was repaired)')}`,
1243
1244
  ` ${s.brand('--format')}=${s.dim('plain')} Human-readable output ${s.dim('(default)')}`,
1244
1245
  ` ${s.brand('--format')}=${s.dim('json')} JSON output for scripting/piping`,
1245
1246
  ` ${s.brand('--profile')}=${s.dim('NAME')} Scope profile/connectivity/cache checks to one profile`,