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.
@@ -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('✔')} Digest scheduled for ${s.cyan(answers.time)} ${answers.timezone}\n`);
289
- process.stdout.write(` Next delivery: ${s.cyan(result.nextDelivery)}\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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": {
@@ -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, readLicense } from './lib/license.mjs';
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 { readLicense } = await import('./lib/license.mjs');
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 ${licenseKey}`,
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 = args.find(a => a.startsWith('--time='))?.split('=')[1];
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 = args.find(a => a.startsWith('--timezone='))?.split('=')[1];
89
+ const tzArg = args.find(a => a.startsWith('--timezone='))?.split('=')[1];
90
90
 
91
- const time = timeArg ?? await promptText('Delivery time (HH:MM, 24h):', {
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
- const email = emailArg ?? await promptText('Delivery email:', {
97
- stream,
98
- validate: (v) => v.includes('@') ? null : 'Enter a valid email address',
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 timezone = tzArg ?? await promptText('Timezone (e.g. America/New_York):', {
102
- stream,
103
- defaultValue: Intl.DateTimeFormat().resolvedOptions().timeZone,
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 SCHEDULE_URL = 'https://api.ticketlens.dev/v1/schedule';
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
- const res = await fetcher(SCHEDULE_URL, {
73
- method: 'POST',
74
- signal: AbortSignal.timeout(timeoutMs),
75
- headers: {
76
- 'Content-Type': 'application/json',
77
- 'Authorization': `Bearer ${cliToken}`,
78
- },
79
- body: JSON.stringify({ email, timezone, deliverAt: time }),
80
- });
81
-
82
- if (!res.ok) {
83
- const err = new Error(`Schedule API error ${res.status}`);
84
- err.status = res.status;
85
- throw err;
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
- const res = await fetcher(SCHEDULE_URL, {
111
- method: 'DELETE',
112
- signal: AbortSignal.timeout(10_000),
113
- headers: { 'Authorization': `Bearer ${cliToken}` },
114
- });
115
- if (!res.ok) throw new Error(`Schedule API error ${res.status}`);
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(` ${green('✔')} Digest schedule removed.\n`);
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
- const res = await fetcher(SCHEDULE_URL, {
142
- method: 'GET',
143
- signal: AbortSignal.timeout(10_000),
144
- headers: { 'Authorization': `Bearer ${cliToken}` },
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
- process.stdout.write('No active digest schedule found.\n');
181
+ print(' No active digest schedule found.\n');
148
182
  return;
149
183
  }
150
184
  const data = await res.json();
151
- print(` ${green('✔')} Digest schedule: ${cyan(data.deliverAt)} ${data.timezone}\n`);
152
- print(` Last delivered: ${data.lastDeliveredAt ?? 'never'}\n`);
153
- print(` Next delivery: ${cyan(data.nextDelivery)}\n`);
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(CLOUD_URL, {
138
+ const res = await fetcher(`${apiBase()}/v1/summarize`, {
138
139
  method: 'POST',
139
140
  signal: AbortSignal.timeout(timeoutMs),
140
141
  headers: {