ticketlens 0.4.6 → 0.4.8
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
CHANGED
|
@@ -285,8 +285,11 @@ switch (command) {
|
|
|
285
285
|
if (!result.ok) { process.exitCode = 1; break; }
|
|
286
286
|
|
|
287
287
|
const s = createStyler({ isTTY: process.stdout.isTTY });
|
|
288
|
-
process.stdout.write(` ${s.green('✔')}
|
|
289
|
-
process.stdout.write(`
|
|
288
|
+
process.stdout.write(` ${s.green('✔')} ${s.bold('Digest scheduled')}\n\n`);
|
|
289
|
+
process.stdout.write(` ${s.dim('Time: ')} ${s.cyan(answers.time)} ${s.dim(answers.timezone)}\n`);
|
|
290
|
+
process.stdout.write(` ${s.dim('Email: ')} ${answers.email}\n`);
|
|
291
|
+
process.stdout.write(` ${s.dim('Next delivery:')} ${s.cyan(result.nextDelivery)}\n`);
|
|
292
|
+
process.stdout.write('\n');
|
|
290
293
|
break;
|
|
291
294
|
}
|
|
292
295
|
|
package/package.json
CHANGED
|
@@ -17,20 +17,19 @@ import { runInteractiveList } from './lib/interactive-list.mjs';
|
|
|
17
17
|
import { promptProfileSelect } from './lib/profile-picker.mjs';
|
|
18
18
|
import { printTriageHelp } from './lib/help.mjs';
|
|
19
19
|
import { handleUnknownFlags } from './lib/arg-validator.mjs';
|
|
20
|
-
import { isLicensed, showUpgradePrompt, revalidateIfStale
|
|
20
|
+
import { isLicensed, showUpgradePrompt, revalidateIfStale } from './lib/license.mjs';
|
|
21
21
|
import { readCliToken } from './lib/cli-auth.mjs';
|
|
22
|
+
import { apiBase } from './lib/api-utils.mjs';
|
|
22
23
|
|
|
23
24
|
const DEFAULT_STATUSES = ['In Progress', 'Code Review', 'QA'];
|
|
24
25
|
|
|
25
|
-
async function defaultDigestDeliverer(payload) {
|
|
26
|
-
const
|
|
27
|
-
const licenseKey = readLicense()?.key;
|
|
28
|
-
const res = await fetch('https://api.ticketlens.dev/v1/digest/deliver', {
|
|
26
|
+
async function defaultDigestDeliverer(payload, { cliToken } = {}) {
|
|
27
|
+
const res = await fetch(`${apiBase()}/v1/digest/deliver`, {
|
|
29
28
|
method: 'POST',
|
|
30
29
|
signal: AbortSignal.timeout(10_000),
|
|
31
30
|
headers: {
|
|
32
31
|
'Content-Type': 'application/json',
|
|
33
|
-
'Authorization': `Bearer ${
|
|
32
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
34
33
|
},
|
|
35
34
|
body: JSON.stringify(payload),
|
|
36
35
|
});
|
|
@@ -297,6 +296,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
297
296
|
return;
|
|
298
297
|
}
|
|
299
298
|
const deliverer = opts.digestDeliverer ?? defaultDigestDeliverer;
|
|
299
|
+
const digestCliToken = opts.cliToken ?? readCliToken(configDir) ?? null;
|
|
300
300
|
|
|
301
301
|
// Triage history delta (non-fatal — wrapped in try/catch)
|
|
302
302
|
let delta = null;
|
|
@@ -321,7 +321,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
321
321
|
},
|
|
322
322
|
tickets: sorted,
|
|
323
323
|
delta,
|
|
324
|
-
});
|
|
324
|
+
}, { cliToken: digestCliToken });
|
|
325
325
|
return;
|
|
326
326
|
}
|
|
327
327
|
|
|
@@ -84,24 +84,46 @@ export async function promptSecret(label, { stream = process.stderr, existingVal
|
|
|
84
84
|
*/
|
|
85
85
|
export async function promptScheduleAnswers(args = [], { stream = process.stderr } = {}) {
|
|
86
86
|
// Allow pre-filling via flags: --time=07:00 --email=dev@example.com --timezone=UTC
|
|
87
|
-
const timeArg
|
|
87
|
+
const timeArg = args.find(a => a.startsWith('--time='))?.split('=')[1];
|
|
88
88
|
const emailArg = args.find(a => a.startsWith('--email='))?.split('=')[1];
|
|
89
|
-
const tzArg
|
|
89
|
+
const tzArg = args.find(a => a.startsWith('--timezone='))?.split('=')[1];
|
|
90
90
|
|
|
91
|
-
const
|
|
92
|
-
stream,
|
|
93
|
-
validate: (v) => /^\d{1,2}:\d{2}$/.test(v) ? null : 'Enter time as HH:MM (e.g. 07:00)',
|
|
94
|
-
});
|
|
91
|
+
const s = createStyler({ isTTY: stream.isTTY });
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
// ── Header box ─────────────────────────────────────────────────────────────
|
|
94
|
+
const headerLines = [
|
|
95
|
+
`${s.bold(s.cyan('◆ TicketLens'))} — Digest Schedule`,
|
|
96
|
+
s.dim('Configure your daily triage digest delivery.'),
|
|
97
|
+
];
|
|
98
|
+
const innerWidth = headerLines.reduce((max, l) => Math.max(max, visLen(l)), 0) + 4;
|
|
99
|
+
const bc = s.cyan;
|
|
100
|
+
stream.write('\n');
|
|
101
|
+
stream.write(bc('╭' + '─'.repeat(innerWidth) + '╮') + '\n');
|
|
102
|
+
for (const line of headerLines) {
|
|
103
|
+
const pad = innerWidth - 2 - visLen(line);
|
|
104
|
+
stream.write(bc('│') + ' ' + line + ' '.repeat(Math.max(0, pad)) + bc('│') + '\n');
|
|
105
|
+
}
|
|
106
|
+
stream.write(bc('╰' + '─'.repeat(innerWidth) + '╯') + '\n');
|
|
107
|
+
stream.write('\n');
|
|
100
108
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
const sysTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
110
|
+
|
|
111
|
+
const time = timeArg ?? await promptText(
|
|
112
|
+
s.dim('Delivery time') + s.dim(' (HH:MM, 24h):'),
|
|
113
|
+
{ stream, validate: (v) => /^\d{1,2}:\d{2}$/.test(v) ? null : 'Enter time as HH:MM (e.g. 07:00)' },
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const email = emailArg ?? await promptText(
|
|
117
|
+
s.dim('Delivery email:'),
|
|
118
|
+
{ stream, validate: (v) => v.includes('@') ? null : 'Enter a valid email address' },
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const timezone = tzArg ?? await promptText(
|
|
122
|
+
s.dim('Timezone') + s.dim(` [${sysTimezone}]:`),
|
|
123
|
+
{ stream, defaultValue: sysTimezone },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
stream.write('\n');
|
|
105
127
|
|
|
106
128
|
return { time, email, timezone };
|
|
107
129
|
}
|
|
@@ -2,9 +2,10 @@ import { writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir, platform as osPlatform } from 'node:os';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { red, green, cyan } from './ansi.mjs';
|
|
5
|
+
import { red, green, yellow, cyan, bold, dim } from './ansi.mjs';
|
|
6
|
+
import { apiBase, warnIfInsecure } from './api-utils.mjs';
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const SCHEDULE_PATH = '/v1/schedule';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Build a macOS LaunchAgent plist string.
|
|
@@ -59,7 +60,9 @@ export async function runScheduleWizard({
|
|
|
59
60
|
writeLocalJob = defaultWriteLocalJob,
|
|
60
61
|
timeoutMs = 10_000,
|
|
61
62
|
print = s => process.stdout.write(s),
|
|
63
|
+
warn = s => process.stderr.write(s),
|
|
62
64
|
}) {
|
|
65
|
+
warnIfInsecure(apiBase(), warn);
|
|
63
66
|
if (!cliToken) {
|
|
64
67
|
print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
|
|
65
68
|
return { ok: false };
|
|
@@ -69,32 +72,44 @@ export async function runScheduleWizard({
|
|
|
69
72
|
const hour = parseInt(hourStr, 10);
|
|
70
73
|
const minute = parseInt(minuteStr, 10);
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetcher(`${apiBase()}${SCHEDULE_PATH}`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({ email, timezone, deliverAt: time }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
if (res.status === 401) {
|
|
88
|
+
print(` ${red('✗')} Session expired. Run ${cyan('ticketlens login')} to reconnect.\n`);
|
|
89
|
+
return { ok: false, status: 401 };
|
|
90
|
+
}
|
|
91
|
+
if (res.status === 403) {
|
|
92
|
+
print(` ${red('✗')} schedule requires a Pro license\n`);
|
|
93
|
+
return { ok: false, status: 403 };
|
|
94
|
+
}
|
|
95
|
+
print(` ${yellow('⚠')} Schedule API error (${res.status}) — try again later\n`);
|
|
96
|
+
return { ok: false, status: res.status };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = await res.json();
|
|
100
|
+
|
|
101
|
+
const ticketlensBin = resolveTicketlensBin();
|
|
102
|
+
const content = platform === 'darwin'
|
|
103
|
+
? buildPlist({ hour, minute, ticketlensBin })
|
|
104
|
+
: buildCronLine({ hour, minute, ticketlensBin });
|
|
105
|
+
|
|
106
|
+
writeLocalJob(content, platform);
|
|
107
|
+
|
|
108
|
+
return data;
|
|
109
|
+
} catch {
|
|
110
|
+
print(` ${yellow('⚠')} Schedule API error (network error) — try again later\n`);
|
|
111
|
+
return { ok: false };
|
|
86
112
|
}
|
|
87
|
-
|
|
88
|
-
const data = await res.json();
|
|
89
|
-
|
|
90
|
-
const ticketlensBin = resolveTicketlensBin();
|
|
91
|
-
const content = platform === 'darwin'
|
|
92
|
-
? buildPlist({ hour, minute, ticketlensBin })
|
|
93
|
-
: buildCronLine({ hour, minute, ticketlensBin });
|
|
94
|
-
|
|
95
|
-
writeLocalJob(content, platform);
|
|
96
|
-
|
|
97
|
-
return data;
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
export async function runScheduleStop({
|
|
@@ -102,17 +117,28 @@ export async function runScheduleStop({
|
|
|
102
117
|
cliToken,
|
|
103
118
|
platform = osPlatform(),
|
|
104
119
|
print = s => process.stdout.write(s),
|
|
120
|
+
warn = s => process.stderr.write(s),
|
|
105
121
|
}) {
|
|
122
|
+
warnIfInsecure(apiBase(), warn);
|
|
106
123
|
if (!cliToken) {
|
|
107
124
|
print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
|
|
108
125
|
return;
|
|
109
126
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
let res;
|
|
128
|
+
try {
|
|
129
|
+
res = await fetcher(`${apiBase()}${SCHEDULE_PATH}`, {
|
|
130
|
+
method: 'DELETE',
|
|
131
|
+
signal: AbortSignal.timeout(10_000),
|
|
132
|
+
headers: { 'Authorization': `Bearer ${cliToken}` },
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
print(` ${yellow('⚠')} Schedule API error (network error) — try again later\n`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
print(` ${yellow('⚠')} Schedule API error (${res.status}) — try again later\n`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
116
142
|
|
|
117
143
|
if (platform === 'darwin') {
|
|
118
144
|
const plistPath = join(homedir(), 'Library', 'LaunchAgents', 'io.ticketlens.digest.plist');
|
|
@@ -126,31 +152,41 @@ export async function runScheduleStop({
|
|
|
126
152
|
spawnSync('crontab', [tmp], { encoding: 'utf8' });
|
|
127
153
|
}
|
|
128
154
|
|
|
129
|
-
print(
|
|
155
|
+
print(`\n ${green('✔')} ${bold('Digest schedule removed.')}\n\n`);
|
|
130
156
|
}
|
|
131
157
|
|
|
132
158
|
export async function runScheduleStatus({
|
|
133
159
|
fetcher = globalThis.fetch,
|
|
134
160
|
cliToken,
|
|
135
161
|
print = s => process.stdout.write(s),
|
|
162
|
+
warn = s => process.stderr.write(s),
|
|
136
163
|
}) {
|
|
164
|
+
warnIfInsecure(apiBase(), warn);
|
|
137
165
|
if (!cliToken) {
|
|
138
166
|
print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
|
|
139
167
|
return;
|
|
140
168
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
169
|
+
let res;
|
|
170
|
+
try {
|
|
171
|
+
res = await fetcher(`${apiBase()}${SCHEDULE_PATH}`, {
|
|
172
|
+
method: 'GET',
|
|
173
|
+
signal: AbortSignal.timeout(10_000),
|
|
174
|
+
headers: { 'Authorization': `Bearer ${cliToken}` },
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
print(` ${yellow('⚠')} Schedule API error (network error) — try again later\n`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
146
180
|
if (!res.ok) {
|
|
147
|
-
|
|
181
|
+
print(' No active digest schedule found.\n');
|
|
148
182
|
return;
|
|
149
183
|
}
|
|
150
184
|
const data = await res.json();
|
|
151
|
-
print(
|
|
152
|
-
print(`
|
|
153
|
-
print(`
|
|
185
|
+
print(`\n ${green('✔')} ${bold('Active Digest Schedule')}\n\n`);
|
|
186
|
+
print(` ${dim('Time: ')} ${cyan(data.deliverAt)} ${dim(data.timezone)}\n`);
|
|
187
|
+
print(` ${dim('Last delivered:')} ${data.lastDeliveredAt ? cyan(data.lastDeliveredAt) : dim('never')}\n`);
|
|
188
|
+
print(` ${dim('Next delivery:')} ${cyan(data.nextDelivery)}\n`);
|
|
189
|
+
print('\n');
|
|
154
190
|
}
|
|
155
191
|
|
|
156
192
|
function resolveTicketlensBin() {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { apiBase } from './api-utils.mjs';
|
|
2
|
+
|
|
1
3
|
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
|
2
4
|
const OPENAI_URL = 'https://api.openai.com/v1/chat/completions';
|
|
3
5
|
const GROQ_URL = 'https://api.groq.com/openai/v1/chat/completions';
|
|
4
|
-
const CLOUD_URL = 'https://api.ticketlens.dev/v1/summarize';
|
|
5
6
|
const DEFAULT_PROMPT = 'Summarize this Jira ticket in 3 sentences. Focus on what matters most for implementation. Be concrete.\n\n';
|
|
6
7
|
const DEFAULT_MAX_TOKENS = 256;
|
|
7
8
|
|
|
@@ -134,7 +135,7 @@ async function callGroq({ brief, apiKey, fetcher, timeoutMs, prompt, maxTokens }
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
async function cloud({ brief, licenseKey, fetcher, timeoutMs }) {
|
|
137
|
-
const res = await fetcher(
|
|
138
|
+
const res = await fetcher(`${apiBase()}/v1/summarize`, {
|
|
138
139
|
method: 'POST',
|
|
139
140
|
signal: AbortSignal.timeout(timeoutMs),
|
|
140
141
|
headers: {
|