ticketlens 0.4.2 → 0.4.4

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
@@ -587,7 +587,7 @@ ticketlens CNV1-2 --summarize # AI summary via your own API key (BYOK
587
587
  ticketlens CNV1-2 --summarize --cloud # AI summary via TicketLens API (no local key needed)
588
588
  ticketlens CNV1-2 --handoff # AI handoff brief from the ticket's comment thread (BYOK)
589
589
  ticketlens CNV1-2 --handoff --cloud # AI handoff brief via TicketLens API
590
- ticketlens CNV1-2 --compliance # Check ticket requirements against local diff [Free 3/mo]
590
+ ticketlens CNV1-2 --compliance # Check ticket requirements against local diff [Pro/Free 3/mo]
591
591
  ticketlens triage --stale=3 # Custom stale threshold (default is 5)
592
592
  ticketlens triage --digest # POST scored triage results to digest endpoint
593
593
  ticketlens schedule # Set up a scheduled daily digest
@@ -735,11 +735,13 @@ npm test
735
735
  See [ROADMAP.md](ROADMAP.md) for the full plan.
736
736
 
737
737
  Recently shipped:
738
- - **"TicketLens for PRs"** (`ticketlens review`) — assembles a code-review context brief from your current branch: extracts linked ticket keys from the branch name and commits, fetches each ticket, and outputs a structured brief with branch, changed files, and ticket context. Styled ANSI output, spinner, flag validation with typo hints, and base-branch safety check
738
+ - **Collision detection** (`ticketlens collisions`) — shows which files your branch shares with teammates' in-flight branches; `--push` auto-sends git branch data to the team snapshot. Team tier
739
+ - **Shareable triage snapshot** (`ticketlens triage --share`) — generates a 24h signed URL; recipients open it in any browser, no account needed. Team tier
740
+ - **Compliance push** (`ticketlens triage --push`) — enriches the team snapshot with per-ticket compliance status and coverage from the local ledger. Pro tier
741
+ - **"TicketLens for PRs"** (`ticketlens review`) — assembles a code-review context brief from your current branch: linked tickets, changed files, and ticket context in one brief
739
742
  - **Confluence pages** — linked Confluence pages fetched automatically and included in the brief; origin-validated, non-fatal, capped at 10 pages
740
743
  - **Linear support** — `ticketlens init` → Linear; connects via GraphQL API key, fetches tickets, triage, and statuses
741
744
  - **GitHub Issues support** — `ticketlens init` → GitHub Issues; PAT-based, same normalized ticket shape
742
- - **Tracker-aware config** — `ticketlens config` shows the right labels and skips irrelevant prompts per tracker type; always re-validates the connection
743
745
 
744
746
  ---
745
747
 
@@ -203,13 +203,17 @@ switch (command) {
203
203
  process.stdin.setRawMode(true);
204
204
  process.stdin.resume();
205
205
  process.stdin.setEncoding('utf8');
206
- process.stdin.once('data', char => {
207
- process.stdin.setRawMode(false);
208
- process.stdin.pause();
206
+ const cleanup = () => { process.stdin.setRawMode(false); process.stdin.pause(); };
207
+ const onData = char => {
208
+ process.stdin.removeListener('end', onEnd);
209
+ cleanup();
209
210
  process.stderr.write('\n');
210
211
  if (char === '\x03') process.exit(0);
211
212
  res(char === 'y' || char === 'Y');
212
- });
213
+ };
214
+ const onEnd = () => { process.stdin.removeListener('data', onData); cleanup(); res(false); };
215
+ process.stdin.once('data', onData);
216
+ process.stdin.once('end', onEnd);
213
217
  });
214
218
  if (!answer) {
215
219
  process.stderr.write(` ${s.dim('Cancelled.')}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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": {
@@ -82,11 +82,19 @@ export function clearBriefCache(ticketKey, profileName, configDir = DEFAULT_CONF
82
82
  } catch { /* non-fatal */ }
83
83
  }
84
84
 
85
+ // Memoize getBriefCacheEntries per configDir within a single process invocation.
86
+ // 5s TTL is well beyond the lifetime of any CLI run; prevents repeated stat+read+parse
87
+ // when multiple callers (cache list, cache prune, cache status) hit the same directory.
88
+ const _entriesCache = new Map(); // configDir → { entries, time }
89
+ const _ENTRIES_TTL_MS = 5_000;
90
+
85
91
  /**
86
92
  * Returns all brief cache entries across all profiles.
87
93
  * Each entry: { profileName, ticketKey, filePath, size, mtimeMs, fetchedAt, depth }
88
94
  */
89
95
  export function getBriefCacheEntries(configDir = DEFAULT_CONFIG_DIR) {
96
+ const cached = _entriesCache.get(configDir);
97
+ if (cached && Date.now() - cached.time < _ENTRIES_TTL_MS) return cached.entries;
90
98
  const cacheDir = path.join(configDir, 'cache');
91
99
  if (!fs.existsSync(cacheDir)) return [];
92
100
 
@@ -120,6 +128,7 @@ export function getBriefCacheEntries(configDir = DEFAULT_CONFIG_DIR) {
120
128
  }
121
129
  }
122
130
 
131
+ _entriesCache.set(configDir, { entries, time: Date.now() });
123
132
  return entries;
124
133
  }
125
134
 
@@ -8,8 +8,9 @@ import { join } from 'node:path';
8
8
  import { createHmac, randomBytes } from 'node:crypto';
9
9
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
10
10
 
11
- const LEDGER_FILE = 'ledger.jsonl';
12
- const KEY_FILE = 'ledger-key';
11
+ const LEDGER_FILE = 'ledger.jsonl';
12
+ const KEY_FILE = 'ledger-key';
13
+ const MAX_LEDGER_RECORDS = 5_000;
13
14
 
14
15
  /**
15
16
  * Append one compliance record to ledger.jsonl.
@@ -25,6 +26,7 @@ export function appendLedger(record, { configDir = DEFAULT_CONFIG_DIR, fsModule
25
26
 
26
27
  const line = JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n';
27
28
  fsModule.appendFileSync(join(configDir, LEDGER_FILE), line, 'utf8');
29
+ _rotateIfNeeded(configDir, fsModule);
28
30
  }
29
31
 
30
32
  /**
@@ -84,6 +86,23 @@ export function exportLedger(format, { configDir = DEFAULT_CONFIG_DIR, fsModule
84
86
 
85
87
  // ── Internal helpers ─────────────────────────────────────────────────────────
86
88
 
89
+ function _rotateIfNeeded(configDir, fsModule) {
90
+ const ledgerPath = join(configDir, LEDGER_FILE);
91
+ // Cheap size check first — each JSONL record averages ~180 bytes, so 5000 records ≈ 900 KB.
92
+ // Skip the full read when the file is well under the threshold.
93
+ try {
94
+ if (fsModule.statSync(ledgerPath).size < 900_000) return;
95
+ } catch { return; }
96
+
97
+ let raw;
98
+ try { raw = fsModule.readFileSync(ledgerPath, 'utf8'); } catch { return; }
99
+
100
+ const lines = raw.split('\n').filter(l => l.trim().length > 0);
101
+ if (lines.length > MAX_LEDGER_RECORDS) {
102
+ fsModule.writeFileSync(ledgerPath, lines.slice(-MAX_LEDGER_RECORDS).join('\n') + '\n', 'utf8');
103
+ }
104
+ }
105
+
87
106
  function _getOrCreateKey(configDir, fsModule) {
88
107
  const keyPath = join(configDir, KEY_FILE);
89
108
  try {