ticketlens 0.21.5 → 0.21.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.
package/README.md
CHANGED
|
@@ -403,7 +403,9 @@ Every note is scanned before saving — anything shaped like a real secret (API
|
|
|
403
403
|
|
|
404
404
|
**Team sync:** on a Team plan with Recall enabled for your account (owner-managed, per-tier or per-client), notes also sync to your team's shared pool — `note add` pushes in the background, `recall` pulls the team's notes (cached 4h) before searching. A team manager reviews and verifies incoming notes at `console/admin/recall` before they're marked trusted. Without Team Recall entitlement, everything stays on your machine — no network call.
|
|
405
405
|
|
|
406
|
-
**Offline resilience:** if a team push fails for a transient reason (network error, timeout, or a 5xx from the backend), the note stays safely in your local vault and is queued for retry — nothing is lost. The queue flushes automatically in the background (at most once every 15 minutes,
|
|
406
|
+
**Offline resilience:** if a team push fails for a transient reason (network error, timeout, or a 5xx from the backend), the note stays safely in your local vault and is queued for retry — nothing is lost. The queue flushes automatically in the background (at most once every 15 minutes) before every command, not just `recall`/`note add` — a short 4s timeout on that check means it never stalls an unrelated command — or on demand with `ticketlens recall sync`. A session-expired (401) or not-entitled (403) push is never queued — those need you to act (`ticketlens login`, or an owner grant), not a retry. Queued entries expire after 30 days and are capped at 200; switching accounts never flushes a note under the wrong login.
|
|
407
|
+
|
|
408
|
+
**Removing a note:** `ticketlens note delete --id="..." [--ticket=KEY]` removes a note from your local vault. Local only — if it was already pushed to a team, teammates who pulled it keep their copy; deleting it there too is a manager action from the Console (Admin > Recall).
|
|
407
409
|
|
|
408
410
|
`note add`'s save confirmation and `recall`'s search results are styled by default in a terminal; add `--plain` to either for bare, pipe-safe output. `recall` always shows each note's file ID (e.g. `[1784135399545-fe01c4.md]`) so you can open it directly (`cat ~/.ticketlens/recall/<PREFIX>/<id>`), or pass `--full` to print the full body content inline instead.
|
|
409
411
|
|
|
@@ -678,6 +680,7 @@ ticketlens history <TICKET-KEY> # Show urgency timeline for a tick
|
|
|
678
680
|
|
|
679
681
|
# ── Recall ────────────────────────────────────────────────────────────────────
|
|
680
682
|
echo "note body" | ticketlens note add --title="..." --ticket=CNV1-2 --tags=a,b # Save a note [Pro]
|
|
683
|
+
ticketlens note delete --id="..." --ticket=CNV1-2 # Remove a note from your local vault [Pro]
|
|
681
684
|
ticketlens recall CNV1-2 # Search saved notes by ticket key [Pro]
|
|
682
685
|
ticketlens recall "retry backoff" # Free-text search across all notes [Pro]
|
|
683
686
|
ticketlens recall sync # Retry any notes stuck in the local queue [Pro]
|
|
@@ -758,6 +761,7 @@ ticketlens triage --stale=3 # Custom stale threshold (default is 5)
|
|
|
758
761
|
ticketlens triage --digest # POST scored triage results to digest endpoint
|
|
759
762
|
ticketlens schedule # Set up a scheduled daily digest
|
|
760
763
|
ticketlens note add --title="..." # Save a Recall note (body from stdin)
|
|
764
|
+
ticketlens note delete --id="..." # Remove a note from your local vault
|
|
761
765
|
ticketlens recall <query|TICKET-KEY> # Search your saved Recall notes
|
|
762
766
|
ticketlens recall sync # Retry any notes stuck in the local queue
|
|
763
767
|
ticketlens activate YOUR-LICENSE-KEY # Activate Pro license
|
package/bin/ticketlens.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import { checkForUpdate, getUpdateHint } from '../skills/jtb/scripts/lib/update-
|
|
|
37
37
|
import { incrementInvocation, incrementCommand } from '../skills/jtb/scripts/lib/activity-counter.mjs';
|
|
38
38
|
import { DEFAULT_CONFIG_DIR } from '../skills/jtb/scripts/lib/config.mjs';
|
|
39
39
|
import { checkTeamJiraConfigUpdate } from '../skills/jtb/scripts/lib/team-jira-sync.mjs';
|
|
40
|
+
import { maybeAutoFlush } from '../skills/jtb/scripts/lib/recall-queue.mjs';
|
|
40
41
|
|
|
41
42
|
const TRACKED_COMMANDS = new Set([
|
|
42
43
|
'triage', 'fetch', 'get', 'compliance', 'review', 'standup',
|
|
@@ -60,6 +61,26 @@ revalidateIfStale();
|
|
|
60
61
|
// Fire-and-forget: refresh the cached latest npm version once per 24h
|
|
61
62
|
checkForUpdate();
|
|
62
63
|
|
|
64
|
+
// Best-effort: retry any Recall notes still queued from a prior failed push —
|
|
65
|
+
// on every command now, not just note add/recall, so a backlog from a burst
|
|
66
|
+
// of transient failures doesn't just sit until the user happens to run one
|
|
67
|
+
// of those two again. Cheap no-op when the queue is empty (one local file
|
|
68
|
+
// read); maybeAutoFlush's own cooldown still protects a down backend from
|
|
69
|
+
// being hit by every single command.
|
|
70
|
+
try {
|
|
71
|
+
const cliToken = readCliToken(DEFAULT_CONFIG_DIR);
|
|
72
|
+
if (cliToken) {
|
|
73
|
+
// Short timeout — this runs before every command, unrelated to Recall or
|
|
74
|
+
// not, so it must never be the reason an unrelated command feels slow.
|
|
75
|
+
const result = await maybeAutoFlush({ cliToken, configDir: DEFAULT_CONFIG_DIR, timeoutMs: 4000 });
|
|
76
|
+
if (result?.flushed) {
|
|
77
|
+
process.stderr.write(` ✔ Synced ${result.flushed} pending Recall note${result.flushed === 1 ? '' : 's'}.\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Never let a background sync check break the actual command.
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
// Show a one-line update hint on stderr after the command exits (never blocks stdout)
|
|
64
85
|
process.on('exit', () => {
|
|
65
86
|
try {
|
package/package.json
CHANGED
|
@@ -46,6 +46,7 @@ export function printHelp({ stream = process.stdout } = {}) {
|
|
|
46
46
|
` ${s.brand('ticketlens')} history ${s.dim('<TICKET-KEY>')} Urgency timeline for a ticket ${s.dim('[Pro]')}`,
|
|
47
47
|
` ${s.brand('ticketlens')} stats ${s.dim('[options]')} Personal response-time metrics from local history`,
|
|
48
48
|
` ${s.brand('ticketlens')} note add ${s.dim('--title=... [--ticket=KEY]')} Save a Recall note ${s.dim('[Pro]')}`,
|
|
49
|
+
` ${s.brand('ticketlens')} note delete ${s.dim('--id=... [--ticket=KEY]')} Remove a note from your local vault ${s.dim('[Pro]')}`,
|
|
49
50
|
` ${s.brand('ticketlens')} recall ${s.dim('<query|TICKET-KEY>')} Search your saved Recall notes ${s.dim('[Pro]')}`,
|
|
50
51
|
'',
|
|
51
52
|
` ${s.brand('ticketlens')} delete ${s.dim('<PROFILE-NAME>')} Remove a profile`,
|
|
@@ -141,6 +141,7 @@ export async function flushQueue({
|
|
|
141
141
|
isRetryableFailureFn = isRetryableFailure,
|
|
142
142
|
warn = () => {},
|
|
143
143
|
now = () => Date.now(),
|
|
144
|
+
timeoutMs,
|
|
144
145
|
} = {}) {
|
|
145
146
|
const nowMs = now();
|
|
146
147
|
const currentHash = hashToken(cliToken);
|
|
@@ -154,7 +155,9 @@ export async function flushQueue({
|
|
|
154
155
|
continue;
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
const result = await pushNoteFn(entry.notePayload, {
|
|
158
|
+
const result = await pushNoteFn(entry.notePayload, {
|
|
159
|
+
cliToken, configDir, warn, ...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
160
|
+
});
|
|
158
161
|
if (result.ok) {
|
|
159
162
|
flushed++;
|
|
160
163
|
continue;
|
|
@@ -191,36 +194,52 @@ function writeLastFlushAttemptAt(configDir, isoTimestamp) {
|
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
/**
|
|
194
|
-
* Time-gated background flush
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
197
|
+
* Time-gated background flush. Called from every command's entry point
|
|
198
|
+
* (bin/ticketlens.mjs), not just Recall-specific ones — a note added during
|
|
199
|
+
* a burst of failures (e.g. a debugging session) would otherwise sit queued
|
|
200
|
+
* until the user happens to run `note add`/`recall` again, or runs
|
|
201
|
+
* `recall sync` by hand. A no-op unless the queue is non-empty AND at least
|
|
202
|
+
* AUTO_FLUSH_INTERVAL_MS has passed since the last attempt — the interval is
|
|
203
|
+
* a single global cooldown, not per-entry, so a burst of failures in one
|
|
204
|
+
* short window still only gets one automatic retry pass, by design: this
|
|
205
|
+
* runs on every command now, so the next opportunity is never far away, and
|
|
206
|
+
* the cooldown exists specifically to stop a down backend from being hit by
|
|
207
|
+
* every single command in the meantime. The attempt timestamp is recorded
|
|
208
|
+
* even on failure, so a down backend can't be hammered once per command
|
|
209
|
+
* within the window.
|
|
200
210
|
*
|
|
201
211
|
* @param {object} opts
|
|
202
212
|
* @param {string} opts.cliToken
|
|
203
213
|
* @param {string} [opts.configDir]
|
|
204
214
|
* @param {() => number} [opts.now]
|
|
205
215
|
* @param {Function} [opts.flushQueueFn]
|
|
216
|
+
* @param {number} [opts.timeoutMs] - per-request timeout, passed through to
|
|
217
|
+
* pushNote. Callers running this unconditionally on every command (as
|
|
218
|
+
* opposed to an explicit `recall sync`) should pass something short — this
|
|
219
|
+
* must feel instant, not stall an unrelated command behind a slow network.
|
|
220
|
+
* @returns {Promise<{flushed: number, remaining: number}|null>} null if skipped
|
|
221
|
+
* (empty queue or still cooling down) — distinguishes "nothing to report"
|
|
222
|
+
* from "attempted, flushed 0" for a caller that wants to print a summary.
|
|
206
223
|
*/
|
|
207
224
|
export async function maybeAutoFlush({
|
|
208
225
|
cliToken,
|
|
209
226
|
configDir = DEFAULT_CONFIG_DIR,
|
|
210
227
|
now = () => Date.now(),
|
|
211
228
|
flushQueueFn = flushQueue,
|
|
229
|
+
timeoutMs,
|
|
212
230
|
} = {}) {
|
|
213
|
-
if (readQueue(configDir).length === 0) return;
|
|
231
|
+
if (readQueue(configDir).length === 0) return null;
|
|
214
232
|
|
|
215
233
|
const lastAttemptAt = readLastFlushAttemptAt(configDir);
|
|
216
234
|
const nowMs = now();
|
|
217
|
-
if (lastAttemptAt && nowMs - new Date(lastAttemptAt).getTime() < AUTO_FLUSH_INTERVAL_MS) return;
|
|
235
|
+
if (lastAttemptAt && nowMs - new Date(lastAttemptAt).getTime() < AUTO_FLUSH_INTERVAL_MS) return null;
|
|
218
236
|
|
|
219
237
|
try {
|
|
220
|
-
await flushQueueFn({ cliToken, configDir, now });
|
|
238
|
+
return await flushQueueFn({ cliToken, configDir, now, ...(timeoutMs !== undefined ? { timeoutMs } : {}) });
|
|
221
239
|
} catch {
|
|
222
240
|
// A down backend or a thrown network error must never crash the command
|
|
223
241
|
// that opportunistically triggered this background attempt.
|
|
242
|
+
return null;
|
|
224
243
|
} finally {
|
|
225
244
|
writeLastFlushAttemptAt(configDir, new Date(nowMs).toISOString());
|
|
226
245
|
}
|