ticketlens 0.16.0 → 0.16.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
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.16.0 -->
1
+ <!-- jtb-skill-version: 0.16.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.
@@ -15,6 +15,7 @@
15
15
 
16
16
  import fs from 'node:fs';
17
17
  import path from 'node:path';
18
+ import { createHash } from 'node:crypto';
18
19
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
19
20
  import { apiBase, warnIfInsecure } from './api-utils.mjs';
20
21
  import { upsertPulledNote, rebuildIndex, deleteNote } from './recall-vault.mjs';
@@ -23,7 +24,9 @@ import { red, yellow, dim, cyan } from './ansi.mjs';
23
24
  const PUSH_PATH = '/v1/recall/push';
24
25
  const PULL_PATH = '/v1/recall/pull';
25
26
  export const RECALL_PULL_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours, matches BRIEF_TTL_MS
27
+ export const RECALL_ENTITLEMENT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours — entitlement changes are owner-driven and rare
26
28
  const PULL_STATE_FILE = 'recall-pull-state.json';
29
+ const ENTITLEMENT_STATE_FILE = 'recall-entitlement-state.json';
27
30
 
28
31
  function pullStatePath(configDir) {
29
32
  return path.join(configDir, PULL_STATE_FILE);
@@ -45,6 +48,36 @@ function writeLastPulledAt(configDir, isoTimestamp) {
45
48
  }
46
49
  }
47
50
 
51
+ function entitlementCachePath(configDir) {
52
+ return path.join(configDir, ENTITLEMENT_STATE_FILE);
53
+ }
54
+
55
+ // Never store the raw token — only a hash, used solely to detect an account
56
+ // switch (e.g. login as someone else) so a stale cache never suppresses a
57
+ // different account's push. Found via Local Live Test: configDir alone is
58
+ // not a valid cache key — cli-token.json can change without configDir changing.
59
+ function hashToken(cliToken) {
60
+ return createHash('sha256').update(cliToken).digest('hex');
61
+ }
62
+
63
+ function readEntitlementCheckedAt(configDir, cliToken) {
64
+ try {
65
+ const cached = JSON.parse(fs.readFileSync(entitlementCachePath(configDir), 'utf8'));
66
+ if (cached.tokenHash !== hashToken(cliToken)) return null;
67
+ return cached.checkedAt ?? null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function writeEntitlementCheckedAt(configDir, isoTimestamp, cliToken) {
74
+ try {
75
+ fs.writeFileSync(entitlementCachePath(configDir), JSON.stringify({ checkedAt: isoTimestamp, tokenHash: hashToken(cliToken) }), 'utf8');
76
+ } catch {
77
+ // Non-fatal — worst case the next save warns again instead of staying silent.
78
+ }
79
+ }
80
+
48
81
  /**
49
82
  * POSTs one locally-authored note to the team backend.
50
83
  *
@@ -61,13 +94,22 @@ export async function pushNote(note, {
61
94
  cliToken,
62
95
  configDir = DEFAULT_CONFIG_DIR,
63
96
  timeoutMs = 15_000,
97
+ ttlMs = RECALL_ENTITLEMENT_TTL_MS,
64
98
  fetcher = globalThis.fetch,
65
99
  warn = (s) => process.stderr.write(s),
100
+ now = () => Date.now(),
66
101
  } = {}) {
67
102
  if (!cliToken) {
68
103
  return { ok: false };
69
104
  }
70
105
 
106
+ // A non-entitled account gets this same 403 on every save until the owner
107
+ // flips the Recall grant — without this, the warning below repeats forever.
108
+ const checkedAt = readEntitlementCheckedAt(configDir, cliToken);
109
+ if (checkedAt && now() - new Date(checkedAt).getTime() < ttlMs) {
110
+ return { ok: false, skipped: true };
111
+ }
112
+
71
113
  warnIfInsecure(apiBase(), warn);
72
114
 
73
115
  let res;
@@ -110,9 +152,12 @@ export async function pushNote(note, {
110
152
  let reason;
111
153
  try { reason = (await res.json())?.error; } catch { /* fall through to the generic message below */ }
112
154
  if (reason === 'No team found') {
155
+ // Deliberately never cached: joining a team is a fast, self-directed fix,
156
+ // unlike entitlement (which waits on the owner) — the nudge stays useful.
113
157
  warn(` ${yellow('⚠')} You're not on a team yet, so there's nowhere to sync this note — saved locally only.\n`);
114
158
  } else {
115
159
  warn(` ${yellow('⚠')} Your plan doesn't include team Recall sync — saved locally only.\n`);
160
+ writeEntitlementCheckedAt(configDir, new Date(now()).toISOString(), cliToken);
116
161
  }
117
162
  return { ok: false, status: res.status };
118
163
  }