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
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.16.
|
|
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
|
}
|