ticketlens 0.1.24 → 0.1.25
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
|
@@ -19,6 +19,7 @@ import { classifyError } from './lib/error-classifier.mjs';
|
|
|
19
19
|
import { promptProfileSelect, promptProfileMismatch, promptSwitchProfile, promptMultipleMatches } from './lib/profile-picker.mjs';
|
|
20
20
|
import { promptSelect } from './lib/select-prompt.mjs';
|
|
21
21
|
import { printFetchHelp } from './lib/help.mjs';
|
|
22
|
+
import { readTextAttachments } from './lib/handoff-assembler.mjs';
|
|
22
23
|
import { handleUnknownFlags } from './lib/arg-validator.mjs';
|
|
23
24
|
import { TICKET_KEY_PATTERN } from './lib/cli.mjs';
|
|
24
25
|
import { downloadAttachments } from './lib/attachment-downloader.mjs';
|
|
@@ -104,11 +105,22 @@ function resolveAiProvider(args, credentials) {
|
|
|
104
105
|
return credentials?.aiProvider ?? undefined;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Append text-readable attachment content to a brief for AI consumption only.
|
|
110
|
+
* The returned string is only sent to the AI — the displayed brief is unchanged.
|
|
111
|
+
*/
|
|
112
|
+
function augmentBriefForAi(brief, localAttachments) {
|
|
113
|
+
const textFiles = readTextAttachments(localAttachments);
|
|
114
|
+
if (textFiles.length === 0) return brief;
|
|
115
|
+
const sections = textFiles.map(({ filename, content }) => `=== Attachment: ${filename} ===\n${content}`);
|
|
116
|
+
return brief + `\n\n--- Attached Documents (${textFiles.length} text-readable) ---\n\n${sections.join('\n\n')}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
/**
|
|
108
120
|
* Apply --summarize to a brief string.
|
|
109
121
|
* Returns the modified brief, or null if the caller should exit (license gate / consent refused).
|
|
110
122
|
*/
|
|
111
|
-
async function applySummarize(brief, args, opts, configDir, conn, licensedFn, upgradeFn) {
|
|
123
|
+
async function applySummarize(brief, args, opts, configDir, conn, licensedFn, upgradeFn, ticket) {
|
|
112
124
|
if (!licensedFn('pro', configDir)) {
|
|
113
125
|
upgradeFn('pro', '--summarize');
|
|
114
126
|
process.exitCode = 1;
|
|
@@ -142,7 +154,8 @@ async function applySummarize(brief, args, opts, configDir, conn, licensedFn, up
|
|
|
142
154
|
const credentials = opts.credentials ?? loadCredentials(configDir);
|
|
143
155
|
const licenseKey = readLicense(configDir)?.key;
|
|
144
156
|
const provider = opts.provider ?? resolveAiProvider(args, credentials);
|
|
145
|
-
const
|
|
157
|
+
const aiInput = augmentBriefForAi(brief, ticket?.localAttachments);
|
|
158
|
+
const summary = await summarizerFn({ brief: aiInput, mode, credentials, licenseKey, provider });
|
|
146
159
|
const divider = '─'.repeat(60);
|
|
147
160
|
return brief + `\n\n${divider}\n─── AI Summary ${'─'.repeat(45)}\n${summary}\n${divider}\n`;
|
|
148
161
|
} catch (err) {
|
|
@@ -955,7 +968,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
955
968
|
if (args.includes('--check')) brief = applyCheck(brief, opts);
|
|
956
969
|
|
|
957
970
|
if (args.includes('--summarize')) {
|
|
958
|
-
brief = await applySummarize(brief, args, opts, configDir, conn, licensedFn, upgradeFn);
|
|
971
|
+
brief = await applySummarize(brief, args, opts, configDir, conn, licensedFn, upgradeFn, cached.ticket);
|
|
959
972
|
if (brief === null) return;
|
|
960
973
|
}
|
|
961
974
|
|
|
@@ -1141,7 +1154,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
1141
1154
|
if (args.includes('--check')) output = applyCheck(output, opts);
|
|
1142
1155
|
|
|
1143
1156
|
if (args.includes('--summarize')) {
|
|
1144
|
-
output = await applySummarize(output, args, opts, configDir, conn, licensedFn, upgradeFn);
|
|
1157
|
+
output = await applySummarize(output, args, opts, configDir, conn, licensedFn, upgradeFn, ticket);
|
|
1145
1158
|
if (output === null) return;
|
|
1146
1159
|
}
|
|
1147
1160
|
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const HANDOFF_PROMPT = `Build a structured handoff brief from this Jira ticket.
|
|
2
5
|
The developer receiving this ticket has never seen it before — they need to get up to speed immediately.
|
|
6
|
+
Use the description, comments, attached documents, and linked Confluence pages as context.
|
|
3
7
|
|
|
4
8
|
Respond in exactly this format (use the exact headings):
|
|
5
9
|
|
|
6
10
|
### What was attempted
|
|
7
|
-
[2–5 bullet points of concrete work already done
|
|
11
|
+
[2–5 bullet points of concrete work already done. Be specific — mention code paths, methods, or files if referenced.]
|
|
8
12
|
|
|
9
13
|
### Current blockers
|
|
10
|
-
[Bullet points of unresolved issues, errors, or dependencies blocking progress. Write "None identified" if the
|
|
14
|
+
[Bullet points of unresolved issues, errors, or dependencies blocking progress. Write "None identified" if the path is clear.]
|
|
11
15
|
|
|
12
16
|
### Open questions
|
|
13
17
|
[Bullet points of unanswered questions or decisions not yet made. Write "None identified" if everything is resolved.]
|
|
@@ -16,16 +20,46 @@ Respond in exactly this format (use the exact headings):
|
|
|
16
20
|
[1–2 sentences on the best starting point for the incoming developer.]
|
|
17
21
|
|
|
18
22
|
Rules:
|
|
19
|
-
- Be specific and factual. Reference actual details from the
|
|
20
|
-
- Do not invent anything not present in the
|
|
23
|
+
- Be specific and factual. Reference actual details from the ticket content.
|
|
24
|
+
- Do not invent anything not present in the provided context.
|
|
21
25
|
- Keep each bullet under 20 words.
|
|
22
|
-
- If there are no comments,
|
|
26
|
+
- If there are no comments, base your analysis on the description and attached documents.
|
|
23
27
|
|
|
24
28
|
`;
|
|
25
29
|
|
|
30
|
+
const TEXT_EXTENSIONS = new Set(['.txt', '.md', '.markdown', '.log', '.csv', '.json', '.xml', '.html', '.htm', '.rst', '.yaml', '.yml']);
|
|
31
|
+
const MAX_CHARS_PER_FILE = 4000;
|
|
32
|
+
const MAX_TOTAL_CHARS = 12000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read text content from downloaded local attachments.
|
|
36
|
+
* Skips binary files (images, PDFs, Office docs). Caps content per file and in total.
|
|
37
|
+
* @param {Array} localAttachments - from ticket.localAttachments
|
|
38
|
+
* @returns {Array<{filename: string, content: string}>}
|
|
39
|
+
*/
|
|
40
|
+
export function readTextAttachments(localAttachments = []) {
|
|
41
|
+
let totalChars = 0;
|
|
42
|
+
const result = [];
|
|
43
|
+
for (const att of localAttachments) {
|
|
44
|
+
if (!att.localPath || att.skipReason === 'error') continue;
|
|
45
|
+
if (!TEXT_EXTENSIONS.has(extname(att.filename).toLowerCase())) continue;
|
|
46
|
+
try {
|
|
47
|
+
const raw = readFileSync(att.localPath, 'utf8');
|
|
48
|
+
const trimmed = raw.trim();
|
|
49
|
+
if (!trimmed) continue;
|
|
50
|
+
const remaining = MAX_TOTAL_CHARS - totalChars;
|
|
51
|
+
if (remaining <= 0) break;
|
|
52
|
+
const content = trimmed.slice(0, Math.min(MAX_CHARS_PER_FILE, remaining));
|
|
53
|
+
totalChars += content.length;
|
|
54
|
+
result.push({ filename: att.filename, content });
|
|
55
|
+
} catch { /* unreadable — skip */ }
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
/**
|
|
27
61
|
* Build the text input sent to the AI for handoff analysis.
|
|
28
|
-
*
|
|
62
|
+
* Includes description, Confluence pages, text-readable attachments, and comment thread.
|
|
29
63
|
*
|
|
30
64
|
* @param {object} ticket - Normalized ticket object from jira-client.normalizeTicket
|
|
31
65
|
* @returns {string}
|
|
@@ -40,6 +74,33 @@ export function buildHandoffInput(ticket) {
|
|
|
40
74
|
if (ticket.reporter) lines.push(`Reporter: ${ticket.reporter}`);
|
|
41
75
|
lines.push('');
|
|
42
76
|
|
|
77
|
+
if (ticket.description) {
|
|
78
|
+
lines.push('--- Description ---');
|
|
79
|
+
lines.push(ticket.description.replace(/\r/g, ''));
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (ticket.confluencePages?.length > 0) {
|
|
84
|
+
lines.push(`--- Confluence Pages (${ticket.confluencePages.length}) ---`);
|
|
85
|
+
for (const p of ticket.confluencePages) {
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(`### ${p.title ?? p.url}`);
|
|
88
|
+
if (p.text) lines.push(p.text);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const textAttachments = readTextAttachments(ticket.localAttachments);
|
|
94
|
+
if (textAttachments.length > 0) {
|
|
95
|
+
lines.push(`--- Attached Documents (${textAttachments.length} text-readable) ---`);
|
|
96
|
+
for (const { filename, content } of textAttachments) {
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push(`=== ${filename} ===`);
|
|
99
|
+
lines.push(content);
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
|
|
43
104
|
const comments = ticket.comments ?? [];
|
|
44
105
|
lines.push(`--- Comments (${comments.length} total) ---`);
|
|
45
106
|
|