ticketlens 0.4.5 → 0.4.7

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.
@@ -284,8 +284,9 @@ switch (command) {
284
284
  const result = await runScheduleWizard({ answers, cliToken: readCliToken() });
285
285
  if (!result.ok) { process.exitCode = 1; break; }
286
286
 
287
- process.stdout.write(`✔ Digest scheduled for ${answers.time} ${answers.timezone}\n`);
288
- process.stdout.write(` Next delivery: ${result.nextDelivery}\n`);
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`);
289
290
  break;
290
291
  }
291
292
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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
 
@@ -1,8 +1,8 @@
1
1
  import { readCliToken } from './cli-auth.mjs';
2
2
  import { formatCollisions } from './collision-reporter.mjs';
3
3
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
4
-
5
4
  import { apiBase, warnIfInsecure } from './api-utils.mjs';
5
+ import { red, yellow, bold, cyan } from './ansi.mjs';
6
6
 
7
7
  const COLLISIONS_PATH = '/v1/triage/collisions';
8
8
 
@@ -29,7 +29,7 @@ export async function runCollisions(args = [], opts = {}) {
29
29
 
30
30
  const cliToken = readCliTokenFn(configDir) ?? null;
31
31
  if (!cliToken) {
32
- print('✗ collisions requires Console access. Run ticketlens login first.\n');
32
+ print(` ${red('✗')} ${bold('collisions')} requires Console access. Run ${cyan('ticketlens login')} first.\n`);
33
33
  return { ok: false };
34
34
  }
35
35
 
@@ -41,14 +41,14 @@ export async function runCollisions(args = [], opts = {}) {
41
41
 
42
42
  if (!res.ok) {
43
43
  if (res.status === 401) {
44
- print('✗ Session expired. Run ticketlens login to reconnect.\n');
44
+ print(` ${red('✗')} Session expired. Run ${cyan('ticketlens login')} to reconnect.\n`);
45
45
  return { ok: false, status: 401 };
46
46
  }
47
47
  if (res.status === 403) {
48
- print('✗ collisions requires a Team license\n');
48
+ print(` ${red('✗')} ${bold('collisions')} requires a Team license\n`);
49
49
  return { ok: false, status: 403 };
50
50
  }
51
- print(`⚠ Failed to fetch collisions (${res.status})\n`);
51
+ print(` ${yellow('⚠')} Failed to fetch collisions (${res.status})\n`);
52
52
  return { ok: false, status: res.status };
53
53
  }
54
54
 
@@ -67,7 +67,7 @@ export async function runCollisions(args = [], opts = {}) {
67
67
 
68
68
  return { ok: true };
69
69
  } catch {
70
- print('⚠ Failed to fetch collisions (network error)\n');
70
+ print(` ${yellow('⚠')} Failed to fetch collisions (network error)\n`);
71
71
  return { ok: false };
72
72
  }
73
73
  }
@@ -2,8 +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, yellow, cyan } from './ansi.mjs';
6
+ import { apiBase, warnIfInsecure } from './api-utils.mjs';
5
7
 
6
- const SCHEDULE_URL = 'https://api.ticketlens.dev/v1/schedule';
8
+ const SCHEDULE_PATH = '/v1/schedule';
7
9
 
8
10
  /**
9
11
  * Build a macOS LaunchAgent plist string.
@@ -58,9 +60,11 @@ export async function runScheduleWizard({
58
60
  writeLocalJob = defaultWriteLocalJob,
59
61
  timeoutMs = 10_000,
60
62
  print = s => process.stdout.write(s),
63
+ warn = s => process.stderr.write(s),
61
64
  }) {
65
+ warnIfInsecure(apiBase(), warn);
62
66
  if (!cliToken) {
63
- print('✗ schedule requires Console access. Run ticketlens login first.\n');
67
+ print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
64
68
  return { ok: false };
65
69
  }
66
70
  const { time, email, timezone } = answers;
@@ -68,32 +72,44 @@ export async function runScheduleWizard({
68
72
  const hour = parseInt(hourStr, 10);
69
73
  const minute = parseInt(minuteStr, 10);
70
74
 
71
- const res = await fetcher(SCHEDULE_URL, {
72
- method: 'POST',
73
- signal: AbortSignal.timeout(timeoutMs),
74
- headers: {
75
- 'Content-Type': 'application/json',
76
- 'Authorization': `Bearer ${cliToken}`,
77
- },
78
- body: JSON.stringify({ email, timezone, deliverAt: time }),
79
- });
80
-
81
- if (!res.ok) {
82
- const err = new Error(`Schedule API error ${res.status}`);
83
- err.status = res.status;
84
- 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 };
85
112
  }
86
-
87
- const data = await res.json();
88
-
89
- const ticketlensBin = resolveTicketlensBin();
90
- const content = platform === 'darwin'
91
- ? buildPlist({ hour, minute, ticketlensBin })
92
- : buildCronLine({ hour, minute, ticketlensBin });
93
-
94
- writeLocalJob(content, platform);
95
-
96
- return data;
97
113
  }
98
114
 
99
115
  export async function runScheduleStop({
@@ -101,17 +117,28 @@ export async function runScheduleStop({
101
117
  cliToken,
102
118
  platform = osPlatform(),
103
119
  print = s => process.stdout.write(s),
120
+ warn = s => process.stderr.write(s),
104
121
  }) {
122
+ warnIfInsecure(apiBase(), warn);
105
123
  if (!cliToken) {
106
- print('✗ schedule requires Console access. Run ticketlens login first.\n');
124
+ print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
125
+ return;
126
+ }
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`);
107
140
  return;
108
141
  }
109
- const res = await fetcher(SCHEDULE_URL, {
110
- method: 'DELETE',
111
- signal: AbortSignal.timeout(10_000),
112
- headers: { 'Authorization': `Bearer ${cliToken}` },
113
- });
114
- if (!res.ok) throw new Error(`Schedule API error ${res.status}`);
115
142
 
116
143
  if (platform === 'darwin') {
117
144
  const plistPath = join(homedir(), 'Library', 'LaunchAgents', 'io.ticketlens.digest.plist');
@@ -125,31 +152,39 @@ export async function runScheduleStop({
125
152
  spawnSync('crontab', [tmp], { encoding: 'utf8' });
126
153
  }
127
154
 
128
- process.stdout.write('✔ Digest schedule removed.\n');
155
+ print(` ${green('✔')} Digest schedule removed.\n`);
129
156
  }
130
157
 
131
158
  export async function runScheduleStatus({
132
159
  fetcher = globalThis.fetch,
133
160
  cliToken,
134
161
  print = s => process.stdout.write(s),
162
+ warn = s => process.stderr.write(s),
135
163
  }) {
164
+ warnIfInsecure(apiBase(), warn);
136
165
  if (!cliToken) {
137
- print('✗ schedule requires Console access. Run ticketlens login first.\n');
166
+ print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
167
+ return;
168
+ }
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`);
138
178
  return;
139
179
  }
140
- const res = await fetcher(SCHEDULE_URL, {
141
- method: 'GET',
142
- signal: AbortSignal.timeout(10_000),
143
- headers: { 'Authorization': `Bearer ${cliToken}` },
144
- });
145
180
  if (!res.ok) {
146
- process.stdout.write('No active digest schedule found.\n');
181
+ print(' No active digest schedule found.\n');
147
182
  return;
148
183
  }
149
184
  const data = await res.json();
150
- process.stdout.write(`Digest schedule: ${data.deliverAt} ${data.timezone}\n`);
151
- process.stdout.write(`Last delivered: ${data.lastDeliveredAt ?? 'never'}\n`);
152
- process.stdout.write(`Next delivery: ${data.nextDelivery}\n`);
185
+ print(` ${green('✔')} Digest schedule: ${cyan(data.deliverAt)} ${data.timezone}\n`);
186
+ print(` Last delivered: ${data.lastDeliveredAt ?? 'never'}\n`);
187
+ print(` Next delivery: ${cyan(data.nextDelivery)}\n`);
153
188
  }
154
189
 
155
190
  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: {
@@ -8,6 +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
12
 
12
13
  const PUSH_PATH = '/v1/triage/push';
13
14
 
@@ -54,7 +55,7 @@ export async function pushTriageSnapshot({
54
55
  } = {}) {
55
56
  warnIfInsecure(apiBase(), warn);
56
57
  if (!cliToken) {
57
- print('✗ --push requires Console access. Run ticketlens login first.\n');
58
+ print(` ${red('✗')} ${bold('--push')} requires Console access. Run ${cyan('ticketlens login')} first.\n`);
58
59
  return { ok: false };
59
60
  }
60
61
 
@@ -100,24 +101,24 @@ export async function pushTriageSnapshot({
100
101
  });
101
102
 
102
103
  if (res.ok) {
103
- print(`✓ Queue updated — view at ${queueUrl(apiBase())}\n`);
104
+ print(` ${green('✓')} Queue updated — view at ${queueUrl(apiBase())}\n`);
104
105
  return { ok: true, status: res.status };
105
106
  }
106
107
 
107
108
  if (res.status === 401) {
108
- print('✗ Session expired. Run ticketlens login to reconnect.\n');
109
+ print(` ${red('✗')} Session expired. Run ${cyan('ticketlens login')} to reconnect.\n`);
109
110
  return { ok: false, status: res.status };
110
111
  }
111
112
 
112
113
  if (res.status === 403) {
113
- print('✗ --push requires a Team license\n');
114
+ print(` ${red('✗')} ${bold('--push')} requires a Team license\n`);
114
115
  return { ok: false, status: res.status };
115
116
  }
116
117
 
117
- print(`⚠ Push failed (${res.status}) — triage output unaffected\n`);
118
+ print(` ${yellow('⚠')} Push failed (${res.status}) — triage output unaffected\n`);
118
119
  return { ok: false, status: res.status };
119
120
  } catch {
120
- print('⚠ Push failed (network error) — triage output unaffected\n');
121
+ print(` ${yellow('⚠')} Push failed (network error) — triage output unaffected\n`);
121
122
  return { ok: false };
122
123
  }
123
124
  }
@@ -4,6 +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
8
 
8
9
  const SHARE_PATH = '/v1/triage/share';
9
10
 
@@ -48,7 +49,7 @@ export async function shareTriageSnapshot({
48
49
  } = {}) {
49
50
  warnIfInsecure(apiBase(), warn);
50
51
  if (!cliToken) {
51
- print('✗ --share requires Console access. Run ticketlens login first.\n');
52
+ print(` ${red('✗')} ${bold('--share')} requires Console access. Run ${cyan('ticketlens login')} first.\n`);
52
53
  return { ok: false };
53
54
  }
54
55
 
@@ -71,24 +72,24 @@ export async function shareTriageSnapshot({
71
72
 
72
73
  if (res.ok) {
73
74
  const data = await res.json();
74
- print(`✓ Share link (expires in 24h):\n ${data.url}\n`);
75
+ print(` ${green('✓')} Share link (expires in 24h):\n ${data.url}\n`);
75
76
  return { ok: true, status: res.status };
76
77
  }
77
78
 
78
79
  if (res.status === 401) {
79
- print('✗ Session expired. Run ticketlens login to reconnect.\n');
80
+ print(` ${red('✗')} Session expired. Run ${cyan('ticketlens login')} to reconnect.\n`);
80
81
  return { ok: false, status: res.status };
81
82
  }
82
83
 
83
84
  if (res.status === 403) {
84
- print('✗ --share requires a Team license\n');
85
+ print(` ${red('✗')} ${bold('--share')} requires a Team license\n`);
85
86
  return { ok: false, status: res.status };
86
87
  }
87
88
 
88
- print(`⚠ Share failed (${res.status}) — triage output unaffected\n`);
89
+ print(` ${yellow('⚠')} Share failed (${res.status}) — triage output unaffected\n`);
89
90
  return { ok: false, status: res.status };
90
91
  } catch {
91
- print('⚠ Share failed (network error) — triage output unaffected\n');
92
+ print(` ${yellow('⚠')} Share failed (network error) — triage output unaffected\n`);
92
93
  return { ok: false };
93
94
  }
94
95
  }