ticketlens 0.21.0 → 0.21.2
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 +2 -1
- package/scripts/postinstall.mjs +12 -0
- package/skills/jtb/SKILL.md +2 -0
- package/skills/jtb/hooks/recall-nudge-lib.mjs +72 -0
- package/skills/jtb/hooks/recall-nudge-post-tool.mjs +52 -0
- package/skills/jtb/hooks/recall-nudge-stop.mjs +48 -0
- package/skills/jtb/scripts/lib/cli-auth.mjs +26 -2
- package/skills/jtb/scripts/lib/hooks-setup.mjs +115 -0
- package/skills/jtb/scripts/lib/license.mjs +10 -0
- package/skills/jtb/scripts/lib/login-flow.mjs +6 -1
- package/skills/jtb/scripts/lib/sync.mjs +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticketlens",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
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": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin/",
|
|
12
12
|
"scripts/",
|
|
13
13
|
"skills/jtb/SKILL.md",
|
|
14
|
+
"skills/jtb/hooks/",
|
|
14
15
|
"skills/jtb/scripts/lib/",
|
|
15
16
|
"skills/jtb/scripts/fetch-ticket.mjs",
|
|
16
17
|
"skills/jtb/scripts/fetch-my-tickets.mjs"
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -62,6 +62,18 @@ if (updated === 0 && skipped === TARGETS.length) {
|
|
|
62
62
|
console.log(' To install: ticketlens update-skill');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Recall nudge hooks — best-effort, never breaks the install.
|
|
66
|
+
try {
|
|
67
|
+
const { setupRecallHooks } = await import('../skills/jtb/scripts/lib/hooks-setup.mjs');
|
|
68
|
+
for (const r of setupRecallHooks()) {
|
|
69
|
+
if (r.status === 'installed') console.log(` ✔ Recall nudge hooks installed in ${r.label}`);
|
|
70
|
+
else if (r.status === 'error') console.warn(` ⚠ Recall nudge hooks: ${r.label} — ${r.reason}`);
|
|
71
|
+
// 'unchanged' and 'skipped' are silent — nothing changed, nothing to report.
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Silent on failure — never breaks the install.
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
// First-run banner + tl-alias status. Best-effort: npm >=7 hides this output
|
|
66
78
|
// unless the user passes --foreground-scripts, so the guaranteed channel is
|
|
67
79
|
// the first bare `tl`/`ticketlens` run added in a later phase. Never throws —
|
package/skills/jtb/SKILL.md
CHANGED
|
@@ -210,6 +210,8 @@ Do not capture: routine/obvious information, anything already fully covered by t
|
|
|
210
210
|
|
|
211
211
|
Capture **at the moment you confirm the insight**, not deferred to the end of the session — don't ask the user first, just do it. It requires a Pro license; on a Free account it will no-op with an upgrade hint on stderr, which is fine to ignore. It never blocks or fails the session.
|
|
212
212
|
|
|
213
|
+
Immediately before dispatching the call, say one line in your response: `🔖 Recall-flag: <one-line reason>`. This is a fixed marker some harness setups scan for to confirm captures are actually happening — always emit it right when you decide to capture, even if you go on to call `note add` in the same breath.
|
|
214
|
+
|
|
213
215
|
### How to dispatch the call
|
|
214
216
|
|
|
215
217
|
If your environment lets you dispatch a tool call without waiting for its result — a background task, an async subagent, a fire-and-forget job — use that mechanism for this call, so it doesn't block your primary work while the note syncs to the team. If you have no such mechanism, call it synchronously; it's fast, and it never blocks or fails your session either way, so there's no harm in the fallback.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Recall nudge hooks (recall-nudge-post-tool.mjs,
|
|
3
|
+
* recall-nudge-stop.mjs). Both read the same Claude Code hook stdin JSON
|
|
4
|
+
* and the same session transcript — kept in one place so the detection
|
|
5
|
+
* logic can't drift between the two hooks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/;
|
|
13
|
+
export const RECALL_FLAG_RE = /🔖\s*Recall-flag:/;
|
|
14
|
+
export const NOTE_ADD_RE = /\bticketlens\s+note\s+add\b|\/jtb\s+note\b/;
|
|
15
|
+
|
|
16
|
+
export function readStdinJson() {
|
|
17
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function statePath(sessionId) {
|
|
26
|
+
return path.join(os.tmpdir(), `ticketlens-recall-nudge-${sessionId || 'unknown'}.json`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readState(sessionId) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(statePath(sessionId), 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return { ticketToolCalls: 0, lastNudgeAt: 0 };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeState(sessionId, state) {
|
|
38
|
+
try {
|
|
39
|
+
fs.writeFileSync(statePath(sessionId), JSON.stringify(state));
|
|
40
|
+
} catch { /* best-effort — a lost nudge counter is not worth failing the hook over */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads the transcript (JSONL) and returns simple booleans about what
|
|
45
|
+
* happened this session. Best-effort: any read/parse failure returns all
|
|
46
|
+
* false rather than throwing — a broken transcript must never block Claude.
|
|
47
|
+
*/
|
|
48
|
+
export function scanTranscript(transcriptPath) {
|
|
49
|
+
const result = { sawTicketKey: false, sawRecallFlag: false, sawNoteAdd: false };
|
|
50
|
+
let lines;
|
|
51
|
+
try {
|
|
52
|
+
lines = fs.readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean);
|
|
53
|
+
} catch {
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
let text;
|
|
59
|
+
try {
|
|
60
|
+
const entry = JSON.parse(line);
|
|
61
|
+
// Transcript entries vary by role/type; flatten whatever text exists.
|
|
62
|
+
text = JSON.stringify(entry);
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (TICKET_KEY_RE.test(text)) result.sawTicketKey = true;
|
|
67
|
+
if (RECALL_FLAG_RE.test(text)) result.sawRecallFlag = true;
|
|
68
|
+
if (NOTE_ADD_RE.test(text)) result.sawNoteAdd = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse hook (matcher: Bash) — mid-session Recall nudge.
|
|
4
|
+
*
|
|
5
|
+
* Non-blocking by design: this only prints an advisory reminder to stdout
|
|
6
|
+
* every NUDGE_EVERY ticket-related Bash calls, and only if Claude hasn't
|
|
7
|
+
* self-flagged a Recall-worthy insight (🔖 Recall-flag:) since the last one.
|
|
8
|
+
* Never exits non-zero — a mid-session nudge must never interrupt real work.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readStdinJson, statePath, readState, writeState, TICKET_KEY_RE, scanTranscript } from './recall-nudge-lib.mjs';
|
|
12
|
+
|
|
13
|
+
const NUDGE_EVERY = 8; // ticket-related Bash calls between nudges
|
|
14
|
+
|
|
15
|
+
const input = readStdinJson();
|
|
16
|
+
const command = input?.tool_input?.command ?? '';
|
|
17
|
+
const sessionId = input?.session_id;
|
|
18
|
+
const transcriptPath = input?.transcript_path;
|
|
19
|
+
|
|
20
|
+
if (!TICKET_KEY_RE.test(command) || !sessionId) {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const state = readState(sessionId);
|
|
25
|
+
state.ticketToolCalls = (state.ticketToolCalls ?? 0) + 1;
|
|
26
|
+
|
|
27
|
+
// If Claude already self-flagged since we last reset, don't nag — reset the
|
|
28
|
+
// counter so the next nudge only fires after another full quiet stretch.
|
|
29
|
+
if (transcriptPath) {
|
|
30
|
+
const { sawRecallFlag } = scanTranscript(transcriptPath);
|
|
31
|
+
if (sawRecallFlag) {
|
|
32
|
+
state.ticketToolCalls = 0;
|
|
33
|
+
writeState(sessionId, state);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (state.ticketToolCalls >= NUDGE_EVERY) {
|
|
39
|
+
state.ticketToolCalls = 0;
|
|
40
|
+
state.lastNudgeAt = Date.now();
|
|
41
|
+
writeState(sessionId, state);
|
|
42
|
+
process.stdout.write(
|
|
43
|
+
'Reminder (not a request for action right now): if something non-obvious was ' +
|
|
44
|
+
'confirmed in the last stretch of ticket work — a gotcha, a root cause, a ' +
|
|
45
|
+
'decision with non-obvious rationale — capture it now via `ticketlens note add` ' +
|
|
46
|
+
'per the jtb skill\'s Recall guidance, then keep going.\n',
|
|
47
|
+
);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
writeState(sessionId, state);
|
|
52
|
+
process.exit(0);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stop hook — end-of-session Recall check.
|
|
4
|
+
*
|
|
5
|
+
* Blocks (exit 2) at most ONCE per session — never traps the user in a
|
|
6
|
+
* loop regardless of how Claude responds. Two cases force a check:
|
|
7
|
+
* 1. Claude flagged something (🔖 Recall-flag:) but never called note add
|
|
8
|
+
* — a broken promise, the strongest signal something was missed.
|
|
9
|
+
* 2. Ticket work happened all session with zero flags and zero notes
|
|
10
|
+
* — the weaker "did anything ever get considered?" catch.
|
|
11
|
+
* Anything else (no ticket work at all, or a note was already added) exits
|
|
12
|
+
* clean — this must never be the reason a session can't end.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readStdinJson, readState, writeState, scanTranscript } from './recall-nudge-lib.mjs';
|
|
16
|
+
|
|
17
|
+
const input = readStdinJson();
|
|
18
|
+
const sessionId = input?.session_id;
|
|
19
|
+
const transcriptPath = input?.transcript_path;
|
|
20
|
+
|
|
21
|
+
if (!sessionId || !transcriptPath) process.exit(0);
|
|
22
|
+
|
|
23
|
+
const state = readState(sessionId);
|
|
24
|
+
if (state.stopChecked) process.exit(0); // already asked once this session — respect the answer
|
|
25
|
+
|
|
26
|
+
const { sawTicketKey, sawRecallFlag, sawNoteAdd } = scanTranscript(transcriptPath);
|
|
27
|
+
|
|
28
|
+
if (!sawTicketKey || sawNoteAdd) {
|
|
29
|
+
process.exit(0); // no ticket work, or already captured — nothing to force
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
state.stopChecked = true;
|
|
33
|
+
writeState(sessionId, state);
|
|
34
|
+
|
|
35
|
+
if (sawRecallFlag) {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
'You flagged something as Recall-worthy (🔖 Recall-flag:) earlier this session but ' +
|
|
38
|
+
'never called `ticketlens note add`. Do that now, or say explicitly why it turned ' +
|
|
39
|
+
'out not to qualify — then you can finish.\n',
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
'This session touched ticket work but nothing was ever captured to Recall. If a ' +
|
|
44
|
+
'non-obvious insight, gotcha, or decision came up, capture it now via ' +
|
|
45
|
+
'`ticketlens note add`. If genuinely nothing qualified, just say so — then finish.\n',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
process.exit(2);
|
|
@@ -25,13 +25,37 @@ export function readCliToken(configDir = DEFAULT_CONFIG_DIR) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Server-asserted tier (personal or team-seat) synced alongside the token on
|
|
29
|
+
// every login/sync — separate from the signed, offline license.json file.
|
|
30
|
+
// Lets team-invited members get Pro-equivalent CLI access without a personal
|
|
31
|
+
// TL- key, mirroring what the manager already pays for.
|
|
32
|
+
export function readCliTokenTier(configDir = DEFAULT_CONFIG_DIR) {
|
|
33
|
+
const p = cliTokenPath(configDir);
|
|
34
|
+
if (!existsSync(p)) return null;
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
37
|
+
return typeof data.tier === 'string' ? data.tier : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function saveCliToken(token, configDir = DEFAULT_CONFIG_DIR, tier = undefined) {
|
|
29
44
|
mkdirSync(configDir, { recursive: true });
|
|
30
45
|
const p = cliTokenPath(configDir);
|
|
31
|
-
|
|
46
|
+
const payload = tier === undefined ? { token } : { token, tier };
|
|
47
|
+
writeFileSync(p, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
32
48
|
chmodSync(p, 0o600);
|
|
33
49
|
}
|
|
34
50
|
|
|
51
|
+
// Updates just the cached tier without touching the token — used by `tl sync`
|
|
52
|
+
// to keep entitlement fresh between logins.
|
|
53
|
+
export function saveCliTokenTier(tier, configDir = DEFAULT_CONFIG_DIR) {
|
|
54
|
+
const token = readCliToken(configDir);
|
|
55
|
+
if (!token) return;
|
|
56
|
+
saveCliToken(token, configDir, tier);
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
export function deleteCliToken(configDir = DEFAULT_CONFIG_DIR) {
|
|
36
60
|
const p = cliTokenPath(configDir);
|
|
37
61
|
if (existsSync(p)) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installs the Recall nudge hooks (recall-nudge-post-tool.mjs,
|
|
3
|
+
* recall-nudge-stop.mjs) into any detected Claude Code settings.json —
|
|
4
|
+
* run from postinstall.mjs on every install/update so a user never has to
|
|
5
|
+
* wire this by hand. Idempotent: safe to run on every `npm install`.
|
|
6
|
+
*
|
|
7
|
+
* Never touches an existing settings.json's other content, and never
|
|
8
|
+
* throws — a malformed or unreadable settings.json is skipped, not
|
|
9
|
+
* overwritten, so a broken install can never corrupt the user's config.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
13
|
+
import { join, dirname } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const HOOKS_DIR = join(__dirname, '..', '..', 'hooks');
|
|
19
|
+
const POST_TOOL_SCRIPT = join(HOOKS_DIR, 'recall-nudge-post-tool.mjs');
|
|
20
|
+
const STOP_SCRIPT = join(HOOKS_DIR, 'recall-nudge-stop.mjs');
|
|
21
|
+
|
|
22
|
+
// Distinctive substring used to find/replace our own entries on re-install —
|
|
23
|
+
// never matches anything a user could plausibly have written by hand.
|
|
24
|
+
const MARKER = 'recall-nudge-post-tool.mjs';
|
|
25
|
+
const STOP_MARKER = 'recall-nudge-stop.mjs';
|
|
26
|
+
|
|
27
|
+
const CLAUDE_DIRS = [
|
|
28
|
+
join(homedir(), '.claude'),
|
|
29
|
+
join(homedir(), '.claude-work'),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function makeHookEntry(scriptPath, matcher) {
|
|
33
|
+
return {
|
|
34
|
+
matcher,
|
|
35
|
+
hooks: [
|
|
36
|
+
{
|
|
37
|
+
type: 'command',
|
|
38
|
+
command: `node "${scriptPath}"`,
|
|
39
|
+
timeout: 5,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function upsertHookEntry(list, marker, entry) {
|
|
46
|
+
const idx = list.findIndex(h =>
|
|
47
|
+
(h.hooks || []).some(inner => typeof inner.command === 'string' && inner.command.includes(marker)),
|
|
48
|
+
);
|
|
49
|
+
if (idx === -1) {
|
|
50
|
+
list.push(entry);
|
|
51
|
+
return 'added';
|
|
52
|
+
}
|
|
53
|
+
if (JSON.stringify(list[idx]) === JSON.stringify(entry)) return 'unchanged';
|
|
54
|
+
list[idx] = entry;
|
|
55
|
+
return 'updated';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function installInto(settingsPath) {
|
|
59
|
+
let settings = {};
|
|
60
|
+
if (existsSync(settingsPath)) {
|
|
61
|
+
try {
|
|
62
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
63
|
+
} catch {
|
|
64
|
+
return { status: 'skipped', reason: 'malformed settings.json — left untouched' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
settings.hooks ??= {};
|
|
69
|
+
settings.hooks.PostToolUse ??= [];
|
|
70
|
+
settings.hooks.Stop ??= [];
|
|
71
|
+
|
|
72
|
+
const postResult = upsertHookEntry(
|
|
73
|
+
settings.hooks.PostToolUse,
|
|
74
|
+
MARKER,
|
|
75
|
+
makeHookEntry(POST_TOOL_SCRIPT, 'Bash'),
|
|
76
|
+
);
|
|
77
|
+
const stopResult = upsertHookEntry(
|
|
78
|
+
settings.hooks.Stop,
|
|
79
|
+
STOP_MARKER,
|
|
80
|
+
makeHookEntry(STOP_SCRIPT, '*'),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (postResult === 'unchanged' && stopResult === 'unchanged') {
|
|
84
|
+
return { status: 'unchanged' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tmpPath = `${settingsPath}.${process.pid}.tmp`;
|
|
88
|
+
writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
89
|
+
// Atomic on POSIX — avoids ever leaving settings.json half-written.
|
|
90
|
+
renameSync(tmpPath, settingsPath);
|
|
91
|
+
|
|
92
|
+
return { status: 'installed', postResult, stopResult };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @returns {{ label: string, status: string }[]} one entry per Claude
|
|
97
|
+
* directory found on this machine, for the caller to log.
|
|
98
|
+
*/
|
|
99
|
+
export function setupRecallHooks() {
|
|
100
|
+
const results = [];
|
|
101
|
+
|
|
102
|
+
for (const dir of CLAUDE_DIRS) {
|
|
103
|
+
if (!existsSync(dir)) continue;
|
|
104
|
+
const settingsPath = join(dir, 'settings.json');
|
|
105
|
+
try {
|
|
106
|
+
mkdirSync(dir, { recursive: true });
|
|
107
|
+
const result = installInto(settingsPath);
|
|
108
|
+
results.push({ label: dir, ...result });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
results.push({ label: dir, status: 'error', reason: err.message });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
@@ -5,6 +5,7 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
6
6
|
import { createStyler } from './ansi.mjs';
|
|
7
7
|
import { siteBase } from './api-utils.mjs';
|
|
8
|
+
import { readCliTokenTier } from './cli-auth.mjs';
|
|
8
9
|
|
|
9
10
|
// Mixed into the HMAC key so that knowing the license key alone is not sufficient
|
|
10
11
|
// to forge a valid signature — an attacker also needs this constant from the source.
|
|
@@ -100,6 +101,15 @@ export function isLicensed(tier, configDir = DEFAULT_CONFIG_DIR) {
|
|
|
100
101
|
if (required === 0) return true;
|
|
101
102
|
if (process.env.TICKETLENS_SKIP_LICENSE === 'true') return true;
|
|
102
103
|
|
|
104
|
+
if (checkOwnLicense(required, configDir)) return true;
|
|
105
|
+
|
|
106
|
+
// Team-seat entitlement, server-asserted and synced via login/`tl sync` —
|
|
107
|
+
// covers invited team members who never activated a personal TL- key.
|
|
108
|
+
const syncedTier = LICENSE_TIERS[readCliTokenTier(configDir)] ?? 0;
|
|
109
|
+
return syncedTier >= required;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function checkOwnLicense(required, configDir) {
|
|
103
113
|
const license = readLicense(configDir);
|
|
104
114
|
if (!license) return false;
|
|
105
115
|
|
|
@@ -83,7 +83,12 @@ export async function runLogin({
|
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
let tier;
|
|
87
|
+
try {
|
|
88
|
+
tier = (await res.json())?.tier;
|
|
89
|
+
} catch { /* tier is best-effort — token is still valid */ }
|
|
90
|
+
|
|
91
|
+
saveCliTokenFn(token, undefined, tier);
|
|
87
92
|
stream.write(`\x1b[A\r\x1b[2K ${s.green('✔')} Logged in.\n`);
|
|
88
93
|
|
|
89
94
|
// Flow 1: pull team Jira config for Pro/Team members (silently skipped for Free)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Server wins on all non-credential fields (shape, prefixes, URLs, etc.).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readCliToken } from './cli-auth.mjs';
|
|
8
|
+
import { readCliToken, saveCliTokenTier } from './cli-auth.mjs';
|
|
9
9
|
import {
|
|
10
10
|
loadProfiles,
|
|
11
11
|
loadCredentials,
|
|
@@ -88,6 +88,8 @@ export async function syncProfiles({
|
|
|
88
88
|
let json;
|
|
89
89
|
try { json = await res.json(); } catch { return { error: 'invalid-json' }; }
|
|
90
90
|
|
|
91
|
+
if (typeof json?.tier === 'string') saveCliTokenTier(json.tier, configDir);
|
|
92
|
+
|
|
91
93
|
const remoteProfiles = Array.isArray(json?.profiles) ? json.profiles : [];
|
|
92
94
|
const localConfig = loadProfiles(configDir) || { profiles: {} };
|
|
93
95
|
const localCreds = loadCredentials(configDir);
|