ticketlens 0.1.0
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/LICENSE +21 -0
- package/README.md +457 -0
- package/bin/ticketlens.mjs +376 -0
- package/package.json +37 -0
- package/skills/jtb/scripts/fetch-my-tickets.mjs +377 -0
- package/skills/jtb/scripts/fetch-ticket.mjs +682 -0
- package/skills/jtb/scripts/lib/adapters/github-adapter.mjs +112 -0
- package/skills/jtb/scripts/lib/adapters/jira-adapter.mjs +19 -0
- package/skills/jtb/scripts/lib/adf-converter.mjs +67 -0
- package/skills/jtb/scripts/lib/ansi.mjs +87 -0
- package/skills/jtb/scripts/lib/arg-validator.mjs +178 -0
- package/skills/jtb/scripts/lib/attachment-downloader.mjs +123 -0
- package/skills/jtb/scripts/lib/attention-scorer.mjs +152 -0
- package/skills/jtb/scripts/lib/banner.mjs +201 -0
- package/skills/jtb/scripts/lib/brief-assembler.mjs +137 -0
- package/skills/jtb/scripts/lib/brief-cache.mjs +137 -0
- package/skills/jtb/scripts/lib/budget-pruner.mjs +242 -0
- package/skills/jtb/scripts/lib/cache-manager.mjs +499 -0
- package/skills/jtb/scripts/lib/cli-auth.mjs +40 -0
- package/skills/jtb/scripts/lib/cli.mjs +87 -0
- package/skills/jtb/scripts/lib/code-ref-parser.mjs +113 -0
- package/skills/jtb/scripts/lib/commit-linker.mjs +42 -0
- package/skills/jtb/scripts/lib/compliance-checker.mjs +92 -0
- package/skills/jtb/scripts/lib/config-wizard.mjs +392 -0
- package/skills/jtb/scripts/lib/config.mjs +63 -0
- package/skills/jtb/scripts/lib/diff-analyzer.mjs +66 -0
- package/skills/jtb/scripts/lib/drift-tracker.mjs +120 -0
- package/skills/jtb/scripts/lib/error-classifier.mjs +119 -0
- package/skills/jtb/scripts/lib/help.mjs +253 -0
- package/skills/jtb/scripts/lib/hook-installer.mjs +81 -0
- package/skills/jtb/scripts/lib/init-wizard.mjs +508 -0
- package/skills/jtb/scripts/lib/interactive-list.mjs +257 -0
- package/skills/jtb/scripts/lib/jira-client.mjs +169 -0
- package/skills/jtb/scripts/lib/ledger.mjs +96 -0
- package/skills/jtb/scripts/lib/license.mjs +195 -0
- package/skills/jtb/scripts/lib/pr-assembler.mjs +186 -0
- package/skills/jtb/scripts/lib/profile-picker.mjs +216 -0
- package/skills/jtb/scripts/lib/profile-resolver.mjs +236 -0
- package/skills/jtb/scripts/lib/profile-switcher.mjs +147 -0
- package/skills/jtb/scripts/lib/prompt-helpers.mjs +122 -0
- package/skills/jtb/scripts/lib/requirement-extractor.mjs +52 -0
- package/skills/jtb/scripts/lib/resolve-adapter.mjs +28 -0
- package/skills/jtb/scripts/lib/schedule-wizard.mjs +153 -0
- package/skills/jtb/scripts/lib/select-prompt.mjs +106 -0
- package/skills/jtb/scripts/lib/spinner.mjs +44 -0
- package/skills/jtb/scripts/lib/styled-assembler.mjs +183 -0
- package/skills/jtb/scripts/lib/summarizer.mjs +109 -0
- package/skills/jtb/scripts/lib/sync.mjs +119 -0
- package/skills/jtb/scripts/lib/table-formatter.mjs +48 -0
- package/skills/jtb/scripts/lib/triage-exporter.mjs +93 -0
- package/skills/jtb/scripts/lib/triage-history.mjs +166 -0
- package/skills/jtb/scripts/lib/triage-push.mjs +98 -0
- package/skills/jtb/scripts/lib/usage-tracker.mjs +54 -0
- package/skills/jtb/scripts/lib/vcs-detector.mjs +12 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Budget Optimizer — prune a TicketBrief markdown string to fit within
|
|
3
|
+
* a token budget. Operates on the plain (non-ANSI) brief string.
|
|
4
|
+
*
|
|
5
|
+
* Pruning priority order:
|
|
6
|
+
* 1. Remove individual comment blocks older than 30 days
|
|
7
|
+
* 2. Remove the entire ## Attachments section
|
|
8
|
+
* 3. Truncate ## Description to first 500 chars
|
|
9
|
+
* 4. Remove comment bodies from ## Linked Tickets, keep key+summary lines
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Estimate token count for a string using the 4-chars-per-token heuristic.
|
|
16
|
+
* @param {string} text
|
|
17
|
+
* @returns {number}
|
|
18
|
+
*/
|
|
19
|
+
export function estimateTokens(text) {
|
|
20
|
+
if (!text) return 0;
|
|
21
|
+
return Math.ceil(text.length / 4);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Split a brief string into sections keyed by their `## Heading` marker.
|
|
26
|
+
* Returns an array of { heading: string|null, content: string } objects.
|
|
27
|
+
* The first element (before the first ## heading) has heading === null.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} brief
|
|
30
|
+
* @returns {Array<{ heading: string|null, content: string }>}
|
|
31
|
+
*/
|
|
32
|
+
function splitSections(brief) {
|
|
33
|
+
const lines = brief.split('\n');
|
|
34
|
+
const sections = [];
|
|
35
|
+
let current = { heading: null, lines: [] };
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (line.startsWith('## ')) {
|
|
39
|
+
sections.push(current);
|
|
40
|
+
current = { heading: line.slice(3).trim(), lines: [line] };
|
|
41
|
+
} else {
|
|
42
|
+
current.lines.push(line);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
sections.push(current);
|
|
46
|
+
return sections.map(s => ({ heading: s.heading, content: s.lines.join('\n') }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Join sections back into a brief string.
|
|
51
|
+
* @param {Array<{ heading: string|null, content: string }>} sections
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function joinSections(sections) {
|
|
55
|
+
return sections.map(s => s.content).join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse an individual comment block's date from the heading line.
|
|
60
|
+
* Expects format: `### **Author Name** (YYYY-MM-DD)`
|
|
61
|
+
* Returns a Date or null if unparseable.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} block
|
|
64
|
+
* @returns {Date|null}
|
|
65
|
+
*/
|
|
66
|
+
function parseCommentDate(block) {
|
|
67
|
+
const match = block.match(/###\s+\*\*[^*]+\*\*\s+\((\d{4}-\d{2}-\d{2})/);
|
|
68
|
+
if (!match) return null;
|
|
69
|
+
return new Date(match[1] + 'T00:00:00.000Z');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Split the Comments section content into individual comment blocks.
|
|
74
|
+
* Blocks are separated by `---`.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} sectionContent - full section content including the `## Comments` heading line
|
|
77
|
+
* @returns {string[]} array of individual comment block strings (may include leading ---\n\n)
|
|
78
|
+
*/
|
|
79
|
+
function splitCommentBlocks(sectionContent) {
|
|
80
|
+
// Strip the heading line (first line: "## Comments")
|
|
81
|
+
const withoutHeading = sectionContent.replace(/^## Comments\n\n?/, '');
|
|
82
|
+
return withoutHeading.split(/\n\n---\n\n/);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determine the bare minimum token count for a brief (key + summary line only).
|
|
87
|
+
* This is the first two non-empty lines of the brief.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} brief
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
function bareMinimumTokens(brief) {
|
|
93
|
+
const nonEmpty = brief.split('\n').filter(l => l.trim().length > 0);
|
|
94
|
+
const minText = nonEmpty.slice(0, 2).join('\n');
|
|
95
|
+
return estimateTokens(minText);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Prune a TicketBrief markdown string to fit within a token budget.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} brief - assembled brief string (plain text, no ANSI)
|
|
102
|
+
* @param {object} [opts]
|
|
103
|
+
* @param {number} [opts.budget] - token limit (integer)
|
|
104
|
+
* @param {object} [opts.stream] - writable stream (default process.stderr)
|
|
105
|
+
* @param {Date} [opts.now] - reference Date for age calculations (default new Date())
|
|
106
|
+
* @returns {{ pruned: string, dropped: string[], finalTokens: number }}
|
|
107
|
+
*/
|
|
108
|
+
export function pruneBrief(brief, { budget, stream, now } = {}) {
|
|
109
|
+
const streamOut = stream ?? process.stderr;
|
|
110
|
+
const nowDate = now ?? new Date();
|
|
111
|
+
|
|
112
|
+
// No budget specified — return unchanged
|
|
113
|
+
if (budget == null) {
|
|
114
|
+
return { pruned: brief, dropped: [], finalTokens: estimateTokens(brief) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const currentTokens = estimateTokens(brief);
|
|
118
|
+
|
|
119
|
+
// Already within budget — return unchanged
|
|
120
|
+
if (currentTokens <= budget) {
|
|
121
|
+
return { pruned: brief, dropped: [], finalTokens: currentTokens };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check bare minimum guard
|
|
125
|
+
const minTokens = bareMinimumTokens(brief);
|
|
126
|
+
if (budget < minTokens) {
|
|
127
|
+
streamOut.write(` \u26a0 Budget ${budget} too small \u2014 returning full brief. Minimum is ~${minTokens} tokens.\n`);
|
|
128
|
+
return { pruned: brief, dropped: [], finalTokens: currentTokens };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const dropped = [];
|
|
132
|
+
let sections = splitSections(brief);
|
|
133
|
+
|
|
134
|
+
// ── Priority 1: Remove old comments (> 30 days) ────────────────────────────
|
|
135
|
+
const commentIdx = sections.findIndex(s => s.heading === 'Comments');
|
|
136
|
+
if (commentIdx !== -1 && estimateTokens(joinSections(sections)) > budget) {
|
|
137
|
+
const commentsSection = sections[commentIdx];
|
|
138
|
+
const blocks = splitCommentBlocks(commentsSection.content);
|
|
139
|
+
const keptBlocks = [];
|
|
140
|
+
let removedCount = 0;
|
|
141
|
+
let removedChars = 0;
|
|
142
|
+
|
|
143
|
+
for (const block of blocks) {
|
|
144
|
+
if (!block.trim()) continue;
|
|
145
|
+
const date = parseCommentDate(block);
|
|
146
|
+
const isOld = date && (nowDate - date) > THIRTY_DAYS_MS;
|
|
147
|
+
if (isOld) {
|
|
148
|
+
removedCount++;
|
|
149
|
+
removedChars += block.length;
|
|
150
|
+
} else {
|
|
151
|
+
keptBlocks.push(block);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (removedCount > 0) {
|
|
156
|
+
dropped.push(`${removedCount} old comment${removedCount !== 1 ? 's' : ''} (\u2212${estimateTokens(' '.repeat(removedChars))}t)`);
|
|
157
|
+
if (keptBlocks.length > 0) {
|
|
158
|
+
commentsSection.content = `## Comments\n\n${keptBlocks.join('\n\n---\n\n')}`;
|
|
159
|
+
} else {
|
|
160
|
+
// Remove the entire Comments section
|
|
161
|
+
sections.splice(commentIdx, 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Priority 2: Remove Attachments section ─────────────────────────────────
|
|
167
|
+
if (estimateTokens(joinSections(sections)) > budget) {
|
|
168
|
+
const attIdx = sections.findIndex(s => s.heading === 'Attachments');
|
|
169
|
+
if (attIdx !== -1) {
|
|
170
|
+
const attContent = sections[attIdx].content;
|
|
171
|
+
const attTokens = estimateTokens(attContent);
|
|
172
|
+
// Count attachment lines (lines starting with "- ")
|
|
173
|
+
const attLines = attContent.split('\n').filter(l => l.trim().startsWith('- ')).length;
|
|
174
|
+
dropped.push(`${attLines} attachment${attLines !== 1 ? 's' : ''} (\u2212${attTokens}t)`);
|
|
175
|
+
sections.splice(attIdx, 1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Priority 3: Truncate Description to 500 chars ─────────────────────────
|
|
180
|
+
if (estimateTokens(joinSections(sections)) > budget) {
|
|
181
|
+
const descIdx = sections.findIndex(s => s.heading === 'Description');
|
|
182
|
+
if (descIdx !== -1) {
|
|
183
|
+
const descSection = sections[descIdx];
|
|
184
|
+
// Content after the heading line + blank line
|
|
185
|
+
const headingPart = '## Description\n\n';
|
|
186
|
+
const body = descSection.content.slice(headingPart.length);
|
|
187
|
+
if (body.length > 500) {
|
|
188
|
+
const oldTokens = estimateTokens(body);
|
|
189
|
+
const truncated = body.slice(0, 500);
|
|
190
|
+
const newBody = truncated + '\n\u2026[truncated]';
|
|
191
|
+
descSection.content = headingPart + newBody;
|
|
192
|
+
const savedTokens = oldTokens - estimateTokens(newBody);
|
|
193
|
+
dropped.push(`description truncated (\u2212${savedTokens}t)`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Priority 4: Remove linked ticket comment bodies ────────────────────────
|
|
199
|
+
if (estimateTokens(joinSections(sections)) > budget) {
|
|
200
|
+
const ltIdx = sections.findIndex(s => s.heading === 'Linked Tickets');
|
|
201
|
+
if (ltIdx !== -1) {
|
|
202
|
+
const ltSection = sections[ltIdx];
|
|
203
|
+
const oldTokens = estimateTokens(ltSection.content);
|
|
204
|
+
|
|
205
|
+
// For each linked ticket block, keep only the ### heading line and the
|
|
206
|
+
// **Type:** | **Status:** meta line (first two non-empty lines per block).
|
|
207
|
+
const headingLine = '## Linked Tickets';
|
|
208
|
+
const withoutHeading = ltSection.content.slice(ltSection.content.indexOf('\n') + 1).replace(/^\n/, '');
|
|
209
|
+
const ltBlocks = withoutHeading.split(/\n\n---\n\n/);
|
|
210
|
+
|
|
211
|
+
const prunedBlocks = ltBlocks.map(block => {
|
|
212
|
+
if (!block.trim()) return block;
|
|
213
|
+
const lines = block.split('\n');
|
|
214
|
+
// Keep the ### KEY: summary heading line + the meta line
|
|
215
|
+
const kept = lines.filter((l, i) => {
|
|
216
|
+
if (i === 0) return true; // ### heading
|
|
217
|
+
if (l.startsWith('**Type:**')) return true; // meta line
|
|
218
|
+
return false;
|
|
219
|
+
});
|
|
220
|
+
return kept.join('\n');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
ltSection.content = `${headingLine}\n\n${prunedBlocks.join('\n\n---\n\n')}`;
|
|
224
|
+
const saved = oldTokens - estimateTokens(ltSection.content);
|
|
225
|
+
if (saved > 0) {
|
|
226
|
+
dropped.push(`linked ticket comment bodies removed (\u2212${saved}t)`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const finalBrief = joinSections(sections);
|
|
232
|
+
const finalTokens = estimateTokens(finalBrief);
|
|
233
|
+
|
|
234
|
+
// ── Write drop report to stream ────────────────────────────────────────────
|
|
235
|
+
if (dropped.length > 0) {
|
|
236
|
+
const pruneList = dropped.join(', ');
|
|
237
|
+
streamOut.write(` \u25cb Budget: ${budget} tokens. Pruned: ${pruneList}\n`);
|
|
238
|
+
streamOut.write(` \u25cb Final estimate: ${finalTokens} tokens\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { pruned: finalBrief, dropped, finalTokens };
|
|
242
|
+
}
|