ticketlens 0.9.16 → 0.9.18
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/bin/ticketlens.mjs +12 -1
- package/package.json +1 -1
- package/skills/jtb/SKILL.md +1 -1
- package/skills/jtb/scripts/fetch-my-tickets.mjs +2 -1
- package/skills/jtb/scripts/fetch-ticket.mjs +4 -1
- package/skills/jtb/scripts/lib/activity-counter.mjs +45 -3
- package/skills/jtb/scripts/lib/browser-login.mjs +19 -4
- package/skills/jtb/scripts/lib/run-collisions.mjs +3 -3
- package/skills/jtb/scripts/lib/schedule-wizard.mjs +1 -1
- package/skills/jtb/scripts/lib/triage-push.mjs +3 -3
- package/skills/jtb/scripts/lib/triage-share.mjs +3 -3
package/bin/ticketlens.mjs
CHANGED
|
@@ -34,15 +34,26 @@ import { browserLogin } from '../skills/jtb/scripts/lib/browser-login.mjs';
|
|
|
34
34
|
import { syncProfiles, getApiBase, getConsoleBase } from '../skills/jtb/scripts/lib/sync.mjs';
|
|
35
35
|
import { promptSecret, promptText } from '../skills/jtb/scripts/lib/prompt-helpers.mjs';
|
|
36
36
|
import { checkForUpdate, getUpdateHint } from '../skills/jtb/scripts/lib/update-check.mjs';
|
|
37
|
-
import { incrementInvocation } from '../skills/jtb/scripts/lib/activity-counter.mjs';
|
|
37
|
+
import { incrementInvocation, incrementCommand } from '../skills/jtb/scripts/lib/activity-counter.mjs';
|
|
38
38
|
import { DEFAULT_CONFIG_DIR } from '../skills/jtb/scripts/lib/config.mjs';
|
|
39
39
|
|
|
40
|
+
const TRACKED_COMMANDS = new Set([
|
|
41
|
+
'triage', 'fetch', 'get', 'compliance', 'review', 'standup',
|
|
42
|
+
'pr', 'ledger', 'stats', 'collisions', 'history', 'schedule',
|
|
43
|
+
'brief', 'sync',
|
|
44
|
+
]);
|
|
45
|
+
|
|
40
46
|
const args = process.argv.slice(2);
|
|
41
47
|
const { command, args: cmdArgs } = parseCommand(args);
|
|
42
48
|
|
|
43
49
|
// Best-effort invocation counter — non-fatal
|
|
44
50
|
try { incrementInvocation(DEFAULT_CONFIG_DIR); } catch { /* non-fatal */ }
|
|
45
51
|
|
|
52
|
+
// Per-command + per-flag tracking — non-fatal
|
|
53
|
+
if (TRACKED_COMMANDS.has(command)) {
|
|
54
|
+
try { incrementCommand(DEFAULT_CONFIG_DIR, command, cmdArgs); } catch { /* non-fatal */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
// Fire-and-forget: silently refresh license.json at startup if >7 days since last validation
|
|
47
58
|
revalidateIfStale();
|
|
48
59
|
// Fire-and-forget: refresh the cached latest npm version once per 24h
|
package/package.json
CHANGED
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.9.
|
|
1
|
+
<!-- jtb-skill-version: 0.9.18 -->
|
|
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.
|
|
@@ -11,6 +11,7 @@ import { styleTriageSummary } from './lib/styled-assembler.mjs';
|
|
|
11
11
|
import { resolveConnection, loadProfiles, saveProfile } from './lib/profile-resolver.mjs';
|
|
12
12
|
import { resolveAdapter } from './lib/resolve-adapter.mjs';
|
|
13
13
|
import { incrementTriageRun, readAndResetActivity } from './lib/activity-counter.mjs';
|
|
14
|
+
import { DEFAULT_CONFIG_DIR } from './lib/config.mjs';
|
|
14
15
|
import { writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
15
16
|
import { resolve as resolvePath, dirname } from 'node:path';
|
|
16
17
|
import { createSpinner } from './lib/spinner.mjs';
|
|
@@ -485,7 +486,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
485
486
|
const cliToken = opts.cliToken ?? readCliToken(configDir) ?? null;
|
|
486
487
|
const printFn = opts.print ?? ((s) => process.stdout.write(s));
|
|
487
488
|
let cliActivity = null;
|
|
488
|
-
try { cliActivity = readAndResetActivity(configDir); } catch { /* non-fatal */ }
|
|
489
|
+
try { cliActivity = readAndResetActivity(configDir ?? DEFAULT_CONFIG_DIR); } catch { /* non-fatal */ }
|
|
489
490
|
await pushFn({
|
|
490
491
|
sorted,
|
|
491
492
|
rawTicketMap,
|
|
@@ -12,7 +12,7 @@ import { extractCodeReferences } from './lib/code-ref-parser.mjs';
|
|
|
12
12
|
import { assembleBrief } from './lib/brief-assembler.mjs';
|
|
13
13
|
import { styleBrief } from './lib/styled-assembler.mjs';
|
|
14
14
|
import { resolveConnection, loadProfiles, loadCredentials, saveProfile } from './lib/profile-resolver.mjs';
|
|
15
|
-
import { buildJiraEnv } from './lib/config.mjs';
|
|
15
|
+
import { buildJiraEnv, DEFAULT_CONFIG_DIR } from './lib/config.mjs';
|
|
16
16
|
import { resolveAdapter } from './lib/resolve-adapter.mjs';
|
|
17
17
|
import { createSession } from './lib/banner.mjs';
|
|
18
18
|
import { classifyError } from './lib/error-classifier.mjs';
|
|
@@ -24,6 +24,7 @@ import { handleUnknownFlags } from './lib/arg-validator.mjs';
|
|
|
24
24
|
import { TICKET_KEY_PATTERN } from './lib/cli.mjs';
|
|
25
25
|
import { downloadAttachments } from './lib/attachment-downloader.mjs';
|
|
26
26
|
import { readBriefCache, writeBriefCache, readSummaryCache, writeSummaryCache, briefCacheAge, BRIEF_TTL_MS } from './lib/brief-cache.mjs';
|
|
27
|
+
import { recordTokensSaved } from './lib/activity-counter.mjs';
|
|
27
28
|
import { readCliToken } from './lib/cli-auth.mjs';
|
|
28
29
|
import { resolveTemplate } from './lib/template-resolver.mjs';
|
|
29
30
|
import { parseAge } from './lib/cache-manager.mjs';
|
|
@@ -1054,6 +1055,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
1054
1055
|
}
|
|
1055
1056
|
}
|
|
1056
1057
|
|
|
1058
|
+
recordTokensSaved(configDir ?? DEFAULT_CONFIG_DIR, 'fetch', Math.round(brief.length / 4));
|
|
1057
1059
|
printFn(brief + '\n');
|
|
1058
1060
|
return;
|
|
1059
1061
|
}
|
|
@@ -1241,6 +1243,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
1241
1243
|
}
|
|
1242
1244
|
}
|
|
1243
1245
|
|
|
1246
|
+
recordTokensSaved(configDir ?? DEFAULT_CONFIG_DIR, 'fetch', Math.round(output.length / 4));
|
|
1244
1247
|
printFn(output + '\n');
|
|
1245
1248
|
|
|
1246
1249
|
// Contextual upsell: after a deep traversal with a substantial graph, nudge toward --summarize
|
|
@@ -24,7 +24,7 @@ function read(configDir) {
|
|
|
24
24
|
try {
|
|
25
25
|
return JSON.parse(fs.readFileSync(path.join(configDir, ACTIVITY_FILE), 'utf8'));
|
|
26
26
|
} catch {
|
|
27
|
-
return { fetch_count: 0, triage_run_count: 0, invocations: 0 };
|
|
27
|
+
return { fetch_count: 0, triage_run_count: 0, invocations: 0, commands: {} };
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -59,12 +59,53 @@ export function incrementInvocation(configDir) {
|
|
|
59
59
|
increment(configDir, 'invocations');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Records one invocation of a named command, plus each --flag present in
|
|
64
|
+
* flagArgs. Flag values are stripped so "--depth=2" tracks as "--depth".
|
|
65
|
+
*
|
|
66
|
+
* @param {string} configDir
|
|
67
|
+
* @param {string} command - e.g. "triage", "fetch"
|
|
68
|
+
* @param {string[]} flagArgs - the raw args passed to the command
|
|
69
|
+
*/
|
|
70
|
+
export function incrementCommand(configDir, command, flagArgs = []) {
|
|
71
|
+
const data = read(configDir);
|
|
72
|
+
if (!data.commands) data.commands = {};
|
|
73
|
+
if (!data.commands[command]) data.commands[command] = { count: 0 };
|
|
74
|
+
|
|
75
|
+
data.commands[command].count += 1;
|
|
76
|
+
|
|
77
|
+
for (const arg of flagArgs) {
|
|
78
|
+
if (!arg.startsWith('-')) continue;
|
|
79
|
+
const flag = arg.replace(/=.*$/, '');
|
|
80
|
+
data.commands[command][flag] = (data.commands[command][flag] ?? 0) + 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
write(configDir, data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Accumulates estimated tokens saved for a named command.
|
|
88
|
+
* Called from fetch-ticket.mjs after assembling the brief.
|
|
89
|
+
* Best-effort — swallows write errors like all other counters.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} configDir
|
|
92
|
+
* @param {string} command - e.g. "fetch"
|
|
93
|
+
* @param {number} tokens - estimated tokens saved (brief.length / 4)
|
|
94
|
+
*/
|
|
95
|
+
export function recordTokensSaved(configDir, command, tokens) {
|
|
96
|
+
const data = read(configDir);
|
|
97
|
+
if (!data.commands) data.commands = {};
|
|
98
|
+
if (!data.commands[command]) data.commands[command] = { count: 0 };
|
|
99
|
+
data.commands[command].tokens_saved = (data.commands[command].tokens_saved ?? 0) + tokens;
|
|
100
|
+
write(configDir, data);
|
|
101
|
+
}
|
|
102
|
+
|
|
62
103
|
/**
|
|
63
104
|
* Returns the current counters and resets them to zero.
|
|
64
105
|
* Call only after a confirmed successful push.
|
|
65
106
|
*
|
|
66
107
|
* @param {string} configDir
|
|
67
|
-
* @returns {{ fetch_count: number, triage_run_count: number, invocations: number }}
|
|
108
|
+
* @returns {{ fetch_count: number, triage_run_count: number, invocations: number, commands: object }}
|
|
68
109
|
*/
|
|
69
110
|
export function readAndResetActivity(configDir) {
|
|
70
111
|
const data = read(configDir);
|
|
@@ -72,7 +113,8 @@ export function readAndResetActivity(configDir) {
|
|
|
72
113
|
fetch_count: data.fetch_count ?? 0,
|
|
73
114
|
triage_run_count: data.triage_run_count ?? 0,
|
|
74
115
|
invocations: data.invocations ?? 0,
|
|
116
|
+
commands: data.commands ?? {},
|
|
75
117
|
};
|
|
76
|
-
write(configDir, { fetch_count: 0, triage_run_count: 0, invocations: 0 });
|
|
118
|
+
write(configDir, { fetch_count: 0, triage_run_count: 0, invocations: 0, commands: {} });
|
|
77
119
|
return snapshot;
|
|
78
120
|
}
|
|
@@ -3,6 +3,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { hostname as osHostname } from 'node:os';
|
|
5
5
|
import { getConsoleBase } from './sync.mjs';
|
|
6
|
+
import { readLicense } from './license.mjs';
|
|
6
7
|
|
|
7
8
|
const PORT_MIN = 49152;
|
|
8
9
|
const PORT_MAX = 65535;
|
|
@@ -102,20 +103,34 @@ export function startLocalServer(port, expectedState, timeoutMs = TIMEOUT_MS) {
|
|
|
102
103
|
/**
|
|
103
104
|
* Full browser login flow. Opens the authorize page in the default browser,
|
|
104
105
|
* waits for the callback, and returns the plaintext CLI token.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} [opts]
|
|
108
|
+
* @param {Function} [opts.readLicenseFn] - Injectable license reader (default: readLicense)
|
|
109
|
+
* @param {Function} [opts.openBrowserFn] - Injectable browser opener (default: openBrowser)
|
|
110
|
+
* @param {Function} [opts.startServerFn] - Injectable local server (default: startLocalServer)
|
|
105
111
|
*/
|
|
106
|
-
export async function browserLogin(
|
|
112
|
+
export async function browserLogin({
|
|
113
|
+
readLicenseFn = () => { try { return readLicense(); } catch { return null; } },
|
|
114
|
+
openBrowserFn = openBrowser,
|
|
115
|
+
startServerFn = (port, state) => startLocalServer(port, state),
|
|
116
|
+
} = {}) {
|
|
107
117
|
const state = generateState();
|
|
108
118
|
const port = pickPort();
|
|
109
119
|
const hostname = osHostname();
|
|
110
120
|
|
|
111
121
|
const consoleBase = getConsoleBase();
|
|
112
|
-
|
|
122
|
+
let url = `${consoleBase}/console/auth/cli`
|
|
113
123
|
+ `?port=${port}`
|
|
114
124
|
+ `&state=${encodeURIComponent(state)}`
|
|
115
125
|
+ `&hostname=${encodeURIComponent(hostname)}`;
|
|
116
126
|
|
|
117
|
-
const
|
|
118
|
-
|
|
127
|
+
const license = readLicenseFn();
|
|
128
|
+
if (license?.email) {
|
|
129
|
+
url += `&email=${encodeURIComponent(license.email)}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const tokenPromise = startServerFn(port, state);
|
|
133
|
+
openBrowserFn(url);
|
|
119
134
|
|
|
120
135
|
return tokenPromise;
|
|
121
136
|
}
|
|
@@ -2,7 +2,7 @@ import { readCliToken } from './cli-auth.mjs';
|
|
|
2
2
|
import { formatCollisions } from './collision-reporter.mjs';
|
|
3
3
|
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
4
4
|
import { apiBase, warnIfInsecure } from './api-utils.mjs';
|
|
5
|
-
import { red, yellow, bold, cyan } from './ansi.mjs';
|
|
5
|
+
import { red, yellow, bold, cyan, dim } from './ansi.mjs';
|
|
6
6
|
|
|
7
7
|
const COLLISIONS_PATH = '/v1/triage/collisions';
|
|
8
8
|
|
|
@@ -41,11 +41,11 @@ export async function runCollisions(args = [], opts = {}) {
|
|
|
41
41
|
|
|
42
42
|
if (!res.ok) {
|
|
43
43
|
if (res.status === 401) {
|
|
44
|
-
print(` ${red('✗')} Session expired
|
|
44
|
+
print(` ${red('✗')} Session expired — run ${cyan('ticketlens login')} to reconnect.\n\n`);
|
|
45
45
|
return { ok: false, status: 401 };
|
|
46
46
|
}
|
|
47
47
|
if (res.status === 403) {
|
|
48
|
-
print(` ${red('✗')} ${bold('collisions')} requires a Team license\n`);
|
|
48
|
+
print(` ${red('✗')} ${bold('collisions')} requires a Team license.\n ${dim('→ Log in with a team account:')} ${cyan('ticketlens login')}\n\n`);
|
|
49
49
|
return { ok: false, status: 403 };
|
|
50
50
|
}
|
|
51
51
|
print(` ${yellow('⚠')} Failed to fetch collisions (${res.status})\n`);
|
|
@@ -107,7 +107,7 @@ export async function runScheduleWizard({
|
|
|
107
107
|
|
|
108
108
|
if (!res.ok) {
|
|
109
109
|
if (res.status === 401) {
|
|
110
|
-
print(` ${red('✗')} Session expired
|
|
110
|
+
print(` ${red('✗')} Session expired — run ${cyan('ticketlens login')} to reconnect.\n\n`);
|
|
111
111
|
return { ok: false, status: 401 };
|
|
112
112
|
}
|
|
113
113
|
if (res.status === 403) {
|
|
@@ -8,7 +8,7 @@ import { isLicensed } from './license.mjs';
|
|
|
8
8
|
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
9
9
|
import { apiBase, warnIfInsecure } from './api-utils.mjs';
|
|
10
10
|
import { buildTicketPayload } from './ticket-payload.mjs';
|
|
11
|
-
import { red, green, yellow, bold, cyan } from './ansi.mjs';
|
|
11
|
+
import { red, green, yellow, bold, cyan, dim } from './ansi.mjs';
|
|
12
12
|
|
|
13
13
|
const PUSH_PATH = '/v1/triage/push';
|
|
14
14
|
|
|
@@ -109,12 +109,12 @@ export async function pushTriageSnapshot({
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
if (res.status === 401) {
|
|
112
|
-
print(` ${red('✗')} Session expired
|
|
112
|
+
print(` ${red('✗')} Session expired.\n ${dim('→ Run')} ${cyan('ticketlens login')} ${dim('to reconnect.')}\n\n`);
|
|
113
113
|
return { ok: false, status: res.status };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
if (res.status === 403) {
|
|
117
|
-
print(` ${red('✗')} ${bold('--push')} requires a Team license\n`);
|
|
117
|
+
print(` ${red('✗')} ${bold('--push')} requires a Team license.\n ${dim('→ Log in with a team account:')} ${cyan('ticketlens login')}\n\n`);
|
|
118
118
|
return { ok: false, status: res.status };
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { apiBase, warnIfInsecure } from './api-utils.mjs';
|
|
7
|
-
import { red, green, yellow, bold, cyan } from './ansi.mjs';
|
|
7
|
+
import { red, green, yellow, bold, cyan, dim } from './ansi.mjs';
|
|
8
8
|
|
|
9
9
|
const SHARE_PATH = '/v1/triage/share';
|
|
10
10
|
|
|
@@ -77,12 +77,12 @@ export async function shareTriageSnapshot({
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (res.status === 401) {
|
|
80
|
-
print(` ${red('✗')} Session expired
|
|
80
|
+
print(` ${red('✗')} Session expired — run ${cyan('ticketlens login')} to reconnect.\n\n`);
|
|
81
81
|
return { ok: false, status: res.status };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if (res.status === 403) {
|
|
85
|
-
print(` ${red('✗')} ${bold('--share')} requires a Team license\n`);
|
|
85
|
+
print(` ${red('✗')} ${bold('--share')} requires a Team license.\n ${dim('→ Log in with a team account:')} ${cyan('ticketlens login')}\n\n`);
|
|
86
86
|
return { ok: false, status: res.status };
|
|
87
87
|
}
|
|
88
88
|
|