ticketlens 0.4.2 → 0.4.5

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
@@ -350,8 +350,12 @@ ticketlens sync # Pull your latest tracker profiles from the Console
350
350
 
351
351
  Use `--manual` when there is no GUI (CI runners, SSH sessions, containers).
352
352
 
353
+ `ticketlens logout` removes the stored Console auth token and disconnects this machine from your TicketLens account. Local Jira profiles and credentials are kept intact — re-run `ticketlens login` to reconnect.
354
+
353
355
  `ticketlens sync` pulls any tracker profiles you have configured in the Console and writes them locally, keeping your CLI in sync with your team settings without re-running `init`.
354
356
 
357
+ > **Console features** — `ticketlens triage --push`, `--share`, `ticketlens collisions`, and `ticketlens schedule` all require an active Console session. Run `ticketlens login` once and the token is stored automatically.
358
+
355
359
  ---
356
360
 
357
361
  ### License
@@ -587,7 +591,7 @@ ticketlens CNV1-2 --summarize # AI summary via your own API key (BYOK
587
591
  ticketlens CNV1-2 --summarize --cloud # AI summary via TicketLens API (no local key needed)
588
592
  ticketlens CNV1-2 --handoff # AI handoff brief from the ticket's comment thread (BYOK)
589
593
  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]
594
+ ticketlens CNV1-2 --compliance # Check ticket requirements against local diff [Pro/Free 3/mo]
591
595
  ticketlens triage --stale=3 # Custom stale threshold (default is 5)
592
596
  ticketlens triage --digest # POST scored triage results to digest endpoint
593
597
  ticketlens schedule # Set up a scheduled daily digest
@@ -735,11 +739,13 @@ npm test
735
739
  See [ROADMAP.md](ROADMAP.md) for the full plan.
736
740
 
737
741
  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
742
+ - **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
743
+ - **Shareable triage snapshot** (`ticketlens triage --share`) — generates a 24h signed URL; recipients open it in any browser, no account needed. Team tier
744
+ - **Compliance push** (`ticketlens triage --push`) — enriches the team snapshot with per-ticket compliance status and coverage from the local ledger. Pro tier
745
+ - **"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
746
  - **Confluence pages** — linked Confluence pages fetched automatically and included in the brief; origin-validated, non-fatal, capped at 10 pages
740
747
  - **Linear support** — `ticketlens init` → Linear; connects via GraphQL API key, fetches tickets, triage, and statuses
741
748
  - **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
749
 
744
750
  ---
745
751
 
@@ -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`);
@@ -258,13 +262,13 @@ switch (command) {
258
262
 
259
263
  if (subCmd === '--stop') {
260
264
  const { runScheduleStop } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
261
- await runScheduleStop({ licenseKey: readLicense()?.key });
265
+ await runScheduleStop({ cliToken: readCliToken() });
262
266
  break;
263
267
  }
264
268
 
265
269
  if (subCmd === '--status') {
266
270
  const { runScheduleStatus } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
267
- await runScheduleStatus({ licenseKey: readLicense()?.key });
271
+ await runScheduleStatus({ cliToken: readCliToken() });
268
272
  break;
269
273
  }
270
274
 
@@ -277,7 +281,8 @@ switch (command) {
277
281
  const { runScheduleWizard } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
278
282
  const { promptScheduleAnswers } = await import('../skills/jtb/scripts/lib/prompt-helpers.mjs');
279
283
  const answers = await promptScheduleAnswers(cmdArgs);
280
- const result = await runScheduleWizard({ answers, licenseKey: readLicense()?.key });
284
+ const result = await runScheduleWizard({ answers, cliToken: readCliToken() });
285
+ if (!result.ok) { process.exitCode = 1; break; }
281
286
 
282
287
  process.stdout.write(`✔ Digest scheduled for ${answers.time} ${answers.timezone}\n`);
283
288
  process.stdout.write(` Next delivery: ${result.nextDelivery}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.4.2",
3
+ "version": "0.4.5",
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": {
@@ -18,6 +18,7 @@ import { promptProfileSelect } from './lib/profile-picker.mjs';
18
18
  import { printTriageHelp } from './lib/help.mjs';
19
19
  import { handleUnknownFlags } from './lib/arg-validator.mjs';
20
20
  import { isLicensed, showUpgradePrompt, revalidateIfStale, readLicense } from './lib/license.mjs';
21
+ import { readCliToken } from './lib/cli-auth.mjs';
21
22
 
22
23
  const DEFAULT_STATUSES = ['In Progress', 'Code Review', 'QA'];
23
24
 
@@ -334,37 +335,20 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
334
335
  return;
335
336
  }
336
337
 
337
- // Interactive mode: TTY + not --plain + not --static
338
- const wantInteractive = process.stdout.isTTY && !args.includes('--plain') && !args.includes('--static');
339
- if (wantInteractive && process.stdin.setRawMode) {
340
- const result = await runInteractiveList(sorted, { baseUrl: conn.baseUrl, staleDays, styled: true });
341
- if (result === 'switch') {
342
- const cleanArgs = args.filter(a => !a.startsWith('--profile=') && !a.startsWith('--project='));
343
- return run(cleanArgs, { ...opts, env, fetcher, configDir });
344
- }
345
- // Fall through to push/share if those flags were passed
346
- if (!pushFlag && !shareFlag) return;
347
- } else {
348
- const useStyled = args.includes('--styled') || (!args.includes('--plain') && process.stdout.isTTY);
349
- const summary = useStyled
350
- ? styleTriageSummary(sorted, { styled: true, staleDays, baseUrl: conn.baseUrl })
351
- : assembleTriageSummary(sorted, { staleDays, baseUrl: conn.baseUrl });
352
- process.stdout.write(summary + '\n');
353
- }
354
-
338
+ // push/share run first so their result is visible before (or instead of) the TUI
355
339
  if (pushFlag) {
356
340
  const { pushTriageSnapshot } = await import('./lib/triage-push.mjs');
357
341
  const { scanCurrentBranch } = await import('./lib/branch-scanner.mjs');
358
342
  const pushFn = opts.pushFn ?? pushTriageSnapshot;
359
343
  const scanFn = opts.scanFn ?? scanCurrentBranch;
360
- const licenseKey = readLicense(configDir)?.key ?? null;
344
+ const cliToken = opts.cliToken ?? readCliToken(configDir) ?? null;
361
345
  const printFn = opts.print ?? ((s) => process.stdout.write(s));
362
346
  await pushFn({
363
347
  sorted,
364
348
  rawTicketMap,
365
349
  profile: profileName ?? 'default',
366
350
  baseUrl: conn.baseUrl,
367
- licenseKey,
351
+ cliToken,
368
352
  gitBranches: scanFn(),
369
353
  fetcher,
370
354
  print: printFn,
@@ -374,18 +358,35 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
374
358
  if (shareFlag) {
375
359
  const { shareTriageSnapshot } = await import('./lib/triage-share.mjs');
376
360
  const shareFn = opts.shareFn ?? shareTriageSnapshot;
377
- const licenseKey = readLicense(configDir)?.key ?? null;
361
+ const cliToken = opts.cliToken ?? readCliToken(configDir) ?? null;
378
362
  const printFn = opts.print ?? ((s) => process.stdout.write(s));
379
363
  await shareFn({
380
364
  sorted,
381
365
  rawTicketMap,
382
366
  profile: profileName ?? 'default',
383
367
  baseUrl: conn.baseUrl,
384
- licenseKey,
368
+ cliToken,
385
369
  fetcher,
386
370
  print: printFn,
387
371
  });
388
372
  }
373
+
374
+ // Interactive mode: TTY + not --plain + not --static
375
+ const wantInteractive = process.stdout.isTTY && !args.includes('--plain') && !args.includes('--static');
376
+ if (wantInteractive && process.stdin.setRawMode) {
377
+ const result = await runInteractiveList(sorted, { baseUrl: conn.baseUrl, staleDays, styled: true });
378
+ if (result === 'switch') {
379
+ const cleanArgs = args.filter(a => !a.startsWith('--profile=') && !a.startsWith('--project='));
380
+ return run(cleanArgs, { ...opts, env, fetcher, configDir });
381
+ }
382
+ return;
383
+ }
384
+
385
+ const useStyled = args.includes('--styled') || (!args.includes('--plain') && process.stdout.isTTY);
386
+ const summary = useStyled
387
+ ? styleTriageSummary(sorted, { styled: true, staleDays, baseUrl: conn.baseUrl })
388
+ : assembleTriageSummary(sorted, { staleDays, baseUrl: conn.baseUrl });
389
+ process.stdout.write(summary + '\n');
389
390
  }
390
391
 
391
392
  // Run if invoked directly
@@ -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 {
@@ -1,5 +1,6 @@
1
- import { readLicense } from './license.mjs';
1
+ import { readCliToken } from './cli-auth.mjs';
2
2
  import { formatCollisions } from './collision-reporter.mjs';
3
+ import { DEFAULT_CONFIG_DIR } from './config.mjs';
3
4
 
4
5
  import { apiBase, warnIfInsecure } from './api-utils.mjs';
5
6
 
@@ -10,35 +11,37 @@ const COLLISIONS_PATH = '/v1/triage/collisions';
10
11
  *
11
12
  * @param {string[]} args CLI argument array (e.g. ['--json', '--plain'])
12
13
  * @param {object} [opts]
13
- * @param {Function} [opts.fetcher] Injectable fetch
14
- * @param {Function} [opts.print] Output function
15
- * @param {Function} [opts.readLicenseFn] Injectable license reader
14
+ * @param {Function} [opts.fetcher] Injectable fetch
15
+ * @param {Function} [opts.print] Output function
16
+ * @param {Function} [opts.readCliTokenFn] Injectable CLI token reader
17
+ * @param {string} [opts.configDir] Config directory override
16
18
  * @returns {Promise<{ ok: boolean, status?: number }>}
17
19
  */
18
20
  export async function runCollisions(args = [], opts = {}) {
19
21
  const jsonFlag = args.includes('--json');
20
22
  const plainFlag = args.includes('--plain');
21
- const fetcher = opts.fetcher ?? globalThis.fetch;
22
- const print = opts.print ?? ((s) => process.stdout.write(s));
23
- const warn = opts.warn ?? ((s) => process.stderr.write(s));
24
- const readLicenseFn = opts.readLicenseFn ?? (() => readLicense());
23
+ const fetcher = opts.fetcher ?? globalThis.fetch;
24
+ const print = opts.print ?? ((s) => process.stdout.write(s));
25
+ const warn = opts.warn ?? ((s) => process.stderr.write(s));
26
+ const configDir = opts.configDir ?? DEFAULT_CONFIG_DIR;
27
+ const readCliTokenFn = opts.readCliTokenFn ?? ((dir) => readCliToken(dir));
25
28
  warnIfInsecure(apiBase(), warn);
26
29
 
27
- const licenseKey = readLicenseFn()?.key ?? null;
28
- if (!licenseKey) {
29
- print('✗ collisions requires an active Team license (ticketlens activate <key>)\n');
30
+ const cliToken = readCliTokenFn(configDir) ?? null;
31
+ if (!cliToken) {
32
+ print('✗ collisions requires Console access. Run ticketlens login first.\n');
30
33
  return { ok: false };
31
34
  }
32
35
 
33
36
  try {
34
37
  const res = await fetcher(`${apiBase()}${COLLISIONS_PATH}`, {
35
- headers: { Authorization: `Bearer ${licenseKey}` },
38
+ headers: { Authorization: `Bearer ${cliToken}` },
36
39
  signal: AbortSignal.timeout(15_000),
37
40
  });
38
41
 
39
42
  if (!res.ok) {
40
43
  if (res.status === 401) {
41
- print('✗ Invalid license key\n');
44
+ print('✗ Session expired. Run ticketlens login to reconnect.\n');
42
45
  return { ok: false, status: 401 };
43
46
  }
44
47
  if (res.status === 403) {
@@ -52,12 +52,17 @@ export function buildCronLine({ hour, minute, ticketlensBin }) {
52
52
  export async function runScheduleWizard({
53
53
  answers,
54
54
  fetcher = globalThis.fetch,
55
- licenseKey,
55
+ cliToken,
56
56
  configDir,
57
57
  platform = osPlatform(),
58
58
  writeLocalJob = defaultWriteLocalJob,
59
59
  timeoutMs = 10_000,
60
+ print = s => process.stdout.write(s),
60
61
  }) {
62
+ if (!cliToken) {
63
+ print('✗ schedule requires Console access. Run ticketlens login first.\n');
64
+ return { ok: false };
65
+ }
61
66
  const { time, email, timezone } = answers;
62
67
  const [hourStr, minuteStr] = time.split(':');
63
68
  const hour = parseInt(hourStr, 10);
@@ -68,7 +73,7 @@ export async function runScheduleWizard({
68
73
  signal: AbortSignal.timeout(timeoutMs),
69
74
  headers: {
70
75
  'Content-Type': 'application/json',
71
- 'Authorization': `Bearer ${licenseKey}`,
76
+ 'Authorization': `Bearer ${cliToken}`,
72
77
  },
73
78
  body: JSON.stringify({ email, timezone, deliverAt: time }),
74
79
  });
@@ -91,11 +96,20 @@ export async function runScheduleWizard({
91
96
  return data;
92
97
  }
93
98
 
94
- export async function runScheduleStop({ fetcher = globalThis.fetch, licenseKey, platform = osPlatform() }) {
99
+ export async function runScheduleStop({
100
+ fetcher = globalThis.fetch,
101
+ cliToken,
102
+ platform = osPlatform(),
103
+ print = s => process.stdout.write(s),
104
+ }) {
105
+ if (!cliToken) {
106
+ print('✗ schedule requires Console access. Run ticketlens login first.\n');
107
+ return;
108
+ }
95
109
  const res = await fetcher(SCHEDULE_URL, {
96
110
  method: 'DELETE',
97
111
  signal: AbortSignal.timeout(10_000),
98
- headers: { 'Authorization': `Bearer ${licenseKey}` },
112
+ headers: { 'Authorization': `Bearer ${cliToken}` },
99
113
  });
100
114
  if (!res.ok) throw new Error(`Schedule API error ${res.status}`);
101
115
 
@@ -114,11 +128,19 @@ export async function runScheduleStop({ fetcher = globalThis.fetch, licenseKey,
114
128
  process.stdout.write('✔ Digest schedule removed.\n');
115
129
  }
116
130
 
117
- export async function runScheduleStatus({ fetcher = globalThis.fetch, licenseKey }) {
131
+ export async function runScheduleStatus({
132
+ fetcher = globalThis.fetch,
133
+ cliToken,
134
+ print = s => process.stdout.write(s),
135
+ }) {
136
+ if (!cliToken) {
137
+ print('✗ schedule requires Console access. Run ticketlens login first.\n');
138
+ return;
139
+ }
118
140
  const res = await fetcher(SCHEDULE_URL, {
119
141
  method: 'GET',
120
142
  signal: AbortSignal.timeout(10_000),
121
- headers: { 'Authorization': `Bearer ${licenseKey}` },
143
+ headers: { 'Authorization': `Bearer ${cliToken}` },
122
144
  });
123
145
  if (!res.ok) {
124
146
  process.stdout.write('No active digest schedule found.\n');
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared ticket payload builder used by triage-push and triage-share.
3
+ */
4
+
5
+ export function buildTicketPayload(scored, rawMap, baseUrl) {
6
+ const raw = rawMap?.get(scored.ticketKey);
7
+ return {
8
+ key: scored.ticketKey,
9
+ summary: scored.summary ?? null,
10
+ status: scored.status ?? null,
11
+ assignee: raw?.assignee ?? null,
12
+ attention_score: null,
13
+ flags: scored.urgency === 'clear' ? [] : [scored.urgency],
14
+ compliance_coverage: null,
15
+ compliance_status: 'unknown',
16
+ url: baseUrl ? `${baseUrl}/browse/${scored.ticketKey}` : null,
17
+ last_updated: raw?.updated ?? null,
18
+ };
19
+ }
@@ -6,8 +6,8 @@
6
6
  import { readLedger } from './ledger.mjs';
7
7
  import { isLicensed } from './license.mjs';
8
8
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
9
-
10
9
  import { apiBase, warnIfInsecure } from './api-utils.mjs';
10
+ import { buildTicketPayload } from './ticket-payload.mjs';
11
11
 
12
12
  const PUSH_PATH = '/v1/triage/push';
13
13
 
@@ -16,27 +16,10 @@ const PUSH_PATH = '/v1/triage/push';
16
16
  // https://api.ticketlens.com → https://app.ticketlens.com/console/queue
17
17
  export function queueUrl(base) {
18
18
  const noApi = base.replace(/^(https?:\/\/)api\./, '$1');
19
- // Production: ticketlens.com → app.ticketlens.com
20
- const console = /ticketlens\.com/.test(noApi)
19
+ const consoleBase = /ticketlens\.com/.test(noApi)
21
20
  ? noApi.replace('://', '://app.')
22
21
  : noApi;
23
- return console + '/console/queue';
24
- }
25
-
26
- function buildTicketPayload(scored, rawMap, baseUrl) {
27
- const raw = rawMap?.get(scored.ticketKey);
28
- return {
29
- key: scored.ticketKey,
30
- summary: scored.summary ?? null,
31
- status: scored.status ?? null,
32
- assignee: raw?.assignee ?? null,
33
- attention_score: null,
34
- flags: scored.urgency === 'clear' ? [] : [scored.urgency],
35
- compliance_coverage: null,
36
- compliance_status: 'unknown',
37
- url: baseUrl ? `${baseUrl}/browse/${scored.ticketKey}` : null,
38
- last_updated: raw?.updated ?? null,
39
- };
22
+ return consoleBase + '/console/queue';
40
23
  }
41
24
 
42
25
  /**
@@ -47,7 +30,7 @@ function buildTicketPayload(scored, rawMap, baseUrl) {
47
30
  * @param {Map} [opts.rawTicketMap] - Map<key, normalizedTicket>
48
31
  * @param {string} [opts.profile] - Resolved profile name (max 100 chars)
49
32
  * @param {string} [opts.baseUrl] - Jira base URL for URL construction
50
- * @param {string} [opts.licenseKey] - Bearer token for the API
33
+ * @param {string} [opts.cliToken] - CLI session token for the API
51
34
  * @param {string} [opts.capturedAt] - ISO 8601 timestamp (defaults to now)
52
35
  * @param {Array} [opts.gitBranches] - Branch metadata from scanCurrentBranch (null = not in git repo)
53
36
  * @param {Function} [opts.fetcher] - Injectable fetch (default: globalThis.fetch)
@@ -59,7 +42,7 @@ export async function pushTriageSnapshot({
59
42
  rawTicketMap = new Map(),
60
43
  profile,
61
44
  baseUrl,
62
- licenseKey,
45
+ cliToken,
63
46
  capturedAt,
64
47
  gitBranches,
65
48
  fetcher = globalThis.fetch,
@@ -70,8 +53,8 @@ export async function pushTriageSnapshot({
70
53
  configDir = DEFAULT_CONFIG_DIR,
71
54
  } = {}) {
72
55
  warnIfInsecure(apiBase(), warn);
73
- if (!licenseKey) {
74
- print('✗ --push requires an active Team license (ticketlens activate <key>)\n');
56
+ if (!cliToken) {
57
+ print('✗ --push requires Console access. Run ticketlens login first.\n');
75
58
  return { ok: false };
76
59
  }
77
60
 
@@ -110,7 +93,7 @@ export async function pushTriageSnapshot({
110
93
  method: 'POST',
111
94
  headers: {
112
95
  'Content-Type': 'application/json',
113
- 'Authorization': `Bearer ${licenseKey}`,
96
+ 'Authorization': `Bearer ${cliToken}`,
114
97
  },
115
98
  body: JSON.stringify(payload),
116
99
  signal: AbortSignal.timeout(15_000),
@@ -121,6 +104,11 @@ export async function pushTriageSnapshot({
121
104
  return { ok: true, status: res.status };
122
105
  }
123
106
 
107
+ if (res.status === 401) {
108
+ print('✗ Session expired. Run ticketlens login to reconnect.\n');
109
+ return { ok: false, status: res.status };
110
+ }
111
+
124
112
  if (res.status === 403) {
125
113
  print('✗ --push requires a Team license\n');
126
114
  return { ok: false, status: res.status };
@@ -29,7 +29,7 @@ function buildTicketPayload(scored, rawMap, baseUrl) {
29
29
  * @param {Map} [opts.rawTicketMap] - Map<key, normalizedTicket>
30
30
  * @param {string} [opts.profile] - Resolved profile name (max 100 chars)
31
31
  * @param {string} [opts.baseUrl] - Jira base URL
32
- * @param {string} [opts.licenseKey] - Bearer token
32
+ * @param {string} [opts.cliToken] - CLI session token
33
33
  * @param {string} [opts.capturedAt] - ISO 8601 timestamp
34
34
  * @param {Function} [opts.fetcher] - Injectable fetch
35
35
  * @param {Function} [opts.print] - Output fn
@@ -40,15 +40,15 @@ export async function shareTriageSnapshot({
40
40
  rawTicketMap = new Map(),
41
41
  profile,
42
42
  baseUrl,
43
- licenseKey,
43
+ cliToken,
44
44
  capturedAt,
45
45
  fetcher = globalThis.fetch,
46
46
  print = (s) => process.stdout.write(s),
47
47
  warn = (s) => process.stderr.write(s),
48
48
  } = {}) {
49
49
  warnIfInsecure(apiBase(), warn);
50
- if (!licenseKey) {
51
- print('✗ --share requires an active Team license (ticketlens activate <key>)\n');
50
+ if (!cliToken) {
51
+ print('✗ --share requires Console access. Run ticketlens login first.\n');
52
52
  return { ok: false };
53
53
  }
54
54
 
@@ -63,7 +63,7 @@ export async function shareTriageSnapshot({
63
63
  method: 'POST',
64
64
  headers: {
65
65
  'Content-Type': 'application/json',
66
- 'Authorization': `Bearer ${licenseKey}`,
66
+ 'Authorization': `Bearer ${cliToken}`,
67
67
  },
68
68
  body: JSON.stringify(payload),
69
69
  signal: AbortSignal.timeout(15_000),
@@ -75,6 +75,11 @@ export async function shareTriageSnapshot({
75
75
  return { ok: true, status: res.status };
76
76
  }
77
77
 
78
+ if (res.status === 401) {
79
+ print('✗ Session expired. Run ticketlens login to reconnect.\n');
80
+ return { ok: false, status: res.status };
81
+ }
82
+
78
83
  if (res.status === 403) {
79
84
  print('✗ --share requires a Team license\n');
80
85
  return { ok: false, status: res.status };