ticketlens 0.38.45 → 0.38.46

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
@@ -452,6 +452,8 @@ Every note is scanned before saving — anything shaped like a real secret (API
452
452
 
453
453
  **Queue settings:** the retry cooldown (default 15 min), per-request timeout (4s), max queued notes (200), and queued-note expiry (30 days) are set by your team manager at `console/admin/recall` and apply to every member's CLI — solo users get the same platform defaults. `ticketlens recall settings` shows the values currently in effect, fetched live: a manager's change is visible the moment your CLI's next retry decision runs, not on a delay.
454
454
 
455
+ **Recall capture strictness:** your team manager can also set a team default at `console/admin/recall` (Settings tab). It only applies if you've never run `ticketlens config set recallStrictness` yourself — your own explicit choice always wins over the team default. `ticketlens recall settings` also reports the strictness currently in effect and where it came from.
456
+
455
457
  **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).
456
458
 
457
459
  **Any MCP-capable AI harness:** `ticketlens mcp` starts a stdio [MCP](https://modelcontextprotocol.io) server exposing `fetch`, `triage`, `compliance`, `review`, `standup`, `pr`, `stats`, `issue_types`, `history`, `collisions`, `ledger`, `doctor`, `recall_add`, `recall_update`, `recall_delete`, `recall_search`, `ticket_comment`, `ticket_transition`, `ticket_assign`, `ticket_duplicates`, `ticket_link`, `ticket_update`, and `ticket_create` as native tools — every CLI action now has an MCP tool, so any MCP-compatible AI assistant, not just Claude Code, can call them directly instead of constructing a shell command. It's a thin adapter over the exact same code as the CLI commands above — same license gate per tool, same secret scan/local vault/tracker writes, same team sync — nothing is reimplemented. `fetch`, `doctor`, and `standup` are Free; `triage`'s base scan is Free with some options gated Pro/Team, same as the CLI (`ticketlens triage --help`); `compliance` and `pr` are Free, sharing a 3-checks/month cap on their requirements-coverage section, Pro unlimited; `review` is Free for branch/files/ticket context, with its coverage/focus section requiring Pro as a plain license check — it does not draw from that same monthly counter; `stats` is Free with a 7-day lookback cap, Pro extends it to 30 days, same split as the CLI (`ticketlens stats --help`); `issue_types` is Free and Jira-only — pre-fetches and caches a profile's real creatable projects and issue types ahead of a `ticket_create` attempt, sharing its cache with that tool's own reactive enrichment; Linear/GitHub profiles get a clear "not available" instead of an empty result; `history` reads local triage history only (zero network) and requires Pro; `collisions` requires `ticketlens login` (Console access) plus a Team license; `ledger` exports the local, signed compliance audit trail (zero network) and requires Pro; every other tool needs Pro. `recall_update` overwrites an existing Recall note's body — internal plumbing for the note quality loop, not typically called directly. `recall_delete` is destructive and local-vault-only — requires `confirm: true` alongside `id` to actually execute; there is no interactive y/N prompt under MCP (no real terminal to prompt against), so omitting it always fails rather than silently blocking. Point your harness's MCP config at it: `{ "command": "ticketlens", "args": ["mcp"] }` — or run `ticketlens mcp install` in a project to write that entry into its `.mcp.json` for you (creates the file if it doesn't exist, merges in if it does — never touches any other entry already there; `--dry-run` to preview first).
@@ -808,7 +810,7 @@ ticketlens note delete --id="..." --ticket=CNV1-2 # Remove a note from your loc
808
810
  ticketlens recall CNV1-2 # Search saved notes by ticket key [Pro]
809
811
  ticketlens recall "retry backoff" # Free-text search across all notes [Pro]
810
812
  ticketlens recall sync # Retry any notes stuck in the local queue [Team+]
811
- ticketlens recall settings # Show effective retry-queue settings, fetched live [Team+]
813
+ ticketlens recall settings # Show effective retry-queue settings + capture strictness, fetched live [Team+]
812
814
  ticketlens mcp # Start the MCP stdio server (recall/ticket write tools) [Pro]
813
815
  ticketlens mcp install # Register it into the current project's .mcp.json
814
816
  ticketlens mcp install --dry-run # Preview the registration without writing
@@ -920,7 +922,7 @@ ticketlens note add --title="..." # Save a Recall note (body from stdin)
920
922
  ticketlens note delete --id="..." # Remove a note from your local vault
921
923
  ticketlens recall <query|TICKET-KEY> # Search your saved Recall notes
922
924
  ticketlens recall sync # Retry any notes stuck in the local queue
923
- ticketlens recall settings # Show effective retry-queue settings, fetched live
925
+ ticketlens recall settings # Show effective retry-queue settings + capture strictness, fetched live
924
926
  ticketlens mcp # Start the MCP stdio server (recall/ticket write tools)
925
927
  ticketlens comment CNV1-2 --body="..." # Post a comment to the tracker
926
928
  ticketlens comment CNV1-2 --body="..." --attach=./bug.png # Attach local files (comment/create only)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.38.45",
3
+ "version": "0.38.46",
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": {
@@ -1,4 +1,4 @@
1
- <!-- jtb-skill-version: 0.42.4 -->
1
+ <!-- jtb-skill-version: 0.42.5 -->
2
2
  ---
3
3
  name: jtb
4
4
  description: Fetch a Jira ticket's full context (description, comments, linked issues, code references) and assemble a structured TicketBrief for implementation planning. Use when user types /jtb, mentions a Jira ticket key, or wants to plan work from a Jira ticket.
@@ -62,7 +62,7 @@ Fetches a Jira ticket and produces a structured brief with code references, then
62
62
  /jtb note add --title="..." --attach=./shot.png,./log.txt # attach local files to a Recall note (Pro, local vault only — see note below)
63
63
  /jtb recall PROD-1234 # search saved Recall notes (Pro)
64
64
  /jtb recall sync # retry any notes stuck in the local queue (Team+)
65
- /jtb recall settings # show effective retry-queue settings, fetched live (Team+)
65
+ /jtb recall settings # show effective retry-queue settings + Recall capture strictness, fetched live (Team+)
66
66
  /jtb comment PROD-1234 --body="..." # post a comment to the tracker (Pro)
67
67
  /jtb transition PROD-1234 # list the tracker's current valid transitions (Pro)
68
68
  /jtb transition PROD-1234 --target="Done" --confirm # execute the transition (Pro)
@@ -26,9 +26,12 @@
26
26
  * again after the next compaction/resume rollover, since that dismissal
27
27
  * was never recorded anywhere — only a real capture was.
28
28
  *
29
- * Which of the two cases above actually blocks is governed by the active
30
- * profile's recallStrictness — see recall-nudge-lib.mjs's shouldNag() doc
31
- * comment for the calibration and why strict doesn't widen this further.
29
+ * Which of the two cases above actually blocks is governed by the effective
30
+ * recallStrictness — the active profile's own explicit config-set value, or
31
+ * else the team's Console-set default (backlog #20), resolved entirely from
32
+ * local state via resolveEffectiveRecallStrictness() so this hook never
33
+ * touches the network. See recall-nudge-lib.mjs's shouldNag() doc comment
34
+ * for the calibration and why strict doesn't widen this further.
32
35
  *
33
36
  * resolveProfile() below is given scanTranscript()'s matched ticket key (not
34
37
  * null), so it resolves by ticket-key prefix the same way the brief-injection
@@ -40,7 +43,8 @@
40
43
  */
41
44
 
42
45
  import { readStdinJson, readState, writeState, scanTranscript, hasRecentCapture, writeLastCaptureAt, hasRecentNag, writeLastNagAt, shouldNag } from './recall-nudge-lib.mjs';
43
- import { resolveProfile, normalizeRecallStrictness } from '../scripts/lib/profile-resolver.mjs';
46
+ import { resolveProfile, resolveEffectiveRecallStrictness } from '../scripts/lib/profile-resolver.mjs';
47
+ import { readCliToken } from '../scripts/lib/cli-auth.mjs';
44
48
 
45
49
  const input = readStdinJson();
46
50
  const sessionId = input?.session_id;
@@ -60,7 +64,8 @@ const state = readState(sessionId);
60
64
  if (state.stopChecked) process.exit(0); // already asked once this session — respect the answer
61
65
 
62
66
  const profile = resolveProfile(ticketKey, { cwd });
63
- const recallStrictness = normalizeRecallStrictness(profile?.recallStrictness);
67
+ const cliToken = readCliToken();
68
+ const recallStrictness = resolveEffectiveRecallStrictness({ profile, cliToken });
64
69
 
65
70
  if (!shouldNag({ sawFetch, sawRecallFlag, sawNoteAdd, recallStrictness })) {
66
71
  process.exit(0); // nothing this strictness level requires a capture for
@@ -53,7 +53,7 @@ export function printHelp({ stream = process.stdout } = {}) {
53
53
  ` ${s.brand('ticketlens')} note delete ${s.dim('--id=... [--ticket=KEY]')} Remove a note from your local vault ${s.dim('[Pro]')}`,
54
54
  ` ${s.brand('ticketlens')} recall ${s.dim('<query|TICKET-KEY>')} Search your saved Recall notes ${s.dim('[Pro]')}`,
55
55
  ` ${s.brand('ticketlens')} recall sync Retry any notes stuck in the local queue ${s.dim('[Team+]')}`,
56
- ` ${s.brand('ticketlens')} recall settings Show effective retry-queue settings, fetched live ${s.dim('[Team+]')}`,
56
+ ` ${s.brand('ticketlens')} recall settings Show effective retry-queue settings + capture strictness, fetched live ${s.dim('[Team+]')}`,
57
57
  ` ${s.brand('ticketlens')} mcp Start the MCP stdio server (Recall + ticket writes) ${s.dim('[Pro]')}`,
58
58
  ` ${s.brand('ticketlens')} comment ${s.dim('<TICKET-KEY> --body=... [--attach=...]')} Post a comment to the tracker ${s.dim('[Pro]')}`,
59
59
  ` ${s.brand('ticketlens')} transition ${s.dim('<TICKET-KEY> [--target=... --confirm]')} Move ticket status ${s.dim('[Pro]')}`,
@@ -13,7 +13,7 @@ import { scanForSecrets } from './secret-scanner.mjs';
13
13
  import { checkNoteStructure, checkWordCount, WORD_LIMITS } from './note-structural-check.mjs';
14
14
  import { writeNote, patchNoteBody, deleteNote, deleteNoteAnyPrefix, rebuildIndex } from './recall-vault.mjs';
15
15
  import { readCliToken } from './cli-auth.mjs';
16
- import { resolveProfile, loadProfileRecallTeamId, normalizeRecallStrictness } from './profile-resolver.mjs';
16
+ import { resolveProfile, loadProfileRecallTeamId, resolveEffectiveRecallStrictness } from './profile-resolver.mjs';
17
17
  import { pushNote } from './recall-sync.mjs';
18
18
  import { enqueueNote, isRetryableFailure, maybeAutoFlush } from './recall-queue.mjs';
19
19
  import { incrementDraftKept, incrementDraftDeleted } from './activity-counter.mjs';
@@ -108,6 +108,7 @@ export async function runNoteAdd(cmdArgs, {
108
108
  writeNoteFn = writeNote,
109
109
  readCliTokenFn = readCliToken,
110
110
  resolveProfileFn = resolveProfile,
111
+ resolveEffectiveRecallStrictnessFn = resolveEffectiveRecallStrictness,
111
112
  loadProfileRecallTeamIdFn = loadProfileRecallTeamId,
112
113
  pushNoteFn = pushNote,
113
114
  enqueueNoteFn = enqueueNote,
@@ -155,10 +156,13 @@ export async function runNoteAdd(cmdArgs, {
155
156
  }
156
157
 
157
158
  // Resolved once here (not just inside the push block below) so the
158
- // word-count warning below can read this profile's recallStrictness
159
- // regardless of whether a CLI token is configured for pushing.
159
+ // word-count warning below can read this profile's effective recallStrictness
160
+ // regardless of whether a CLI token is configured for pushing. cliToken is
161
+ // read here too (not just in the push block) since resolveEffectiveRecallStrictness
162
+ // needs it to validate a team-default cache entry belongs to this account.
160
163
  const profile = resolveProfileFn(ticketKey || null, { configDir, cwd: process.cwd() });
161
- const strictness = normalizeRecallStrictness(profile?.recallStrictness);
164
+ const cliToken = readCliTokenFn(configDir);
165
+ const strictness = resolveEffectiveRecallStrictnessFn({ profile, configDir, cliToken });
162
166
  const wordCount = checkWordCountFn({ title, body }, { maxWords: WORD_LIMITS[strictness] });
163
167
  for (const warning of wordCount.warnings) {
164
168
  stream.write(` Warning: ${warning}\n`);
@@ -193,7 +197,6 @@ export async function runNoteAdd(cmdArgs, {
193
197
  const savedLine = `Saved note "${title}" (${id})${attachSuffix}`;
194
198
  stream.write(styled ? `\n ${s.green('✔')} ${savedLine}\n\n` : ` ${savedLine}\n`);
195
199
 
196
- const cliToken = readCliTokenFn(configDir);
197
200
  if (cliToken) {
198
201
  const warn = (s) => stream.write(s);
199
202
  // Field names match PushRequest's validation rules (external_id, tickets) —
@@ -238,6 +241,8 @@ export async function runNotePatch(cmdArgs, {
238
241
  checkNoteStructureFn = checkNoteStructure,
239
242
  checkWordCountFn = checkWordCount,
240
243
  resolveProfileFn = resolveProfile,
244
+ readCliTokenFn = readCliToken,
245
+ resolveEffectiveRecallStrictnessFn = resolveEffectiveRecallStrictness,
241
246
  scanForSecretsFn = scanForSecrets,
242
247
  patchNoteBodyFn = patchNoteBody,
243
248
  } = {}) {
@@ -267,7 +272,8 @@ export async function runNotePatch(cmdArgs, {
267
272
  }
268
273
 
269
274
  const profile = resolveProfileFn(ticketKey || null, { configDir, cwd: process.cwd() });
270
- const strictness = normalizeRecallStrictness(profile?.recallStrictness);
275
+ const cliToken = readCliTokenFn(configDir);
276
+ const strictness = resolveEffectiveRecallStrictnessFn({ profile, configDir, cliToken });
271
277
  const wordCount = checkWordCountFn({ body }, { maxWords: WORD_LIMITS[strictness] });
272
278
  for (const warning of wordCount.warnings) {
273
279
  stream.write(` Warning: ${warning}\n`);
@@ -8,6 +8,10 @@ import { join } from 'node:path';
8
8
  import { homedir } from 'node:os';
9
9
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
10
10
  import { sanitizeUntrustedText } from './ansi.mjs';
11
+ import { readEffectiveRecallSettings } from './recall-settings-sync.mjs';
12
+ import { RECALL_STRICTNESS_LEVELS, DEFAULT_RECALL_STRICTNESS } from './recall-strictness.mjs';
13
+
14
+ export { RECALL_STRICTNESS_LEVELS, DEFAULT_RECALL_STRICTNESS };
11
15
 
12
16
  /** Simple Levenshtein distance for "did you mean" suggestions. */
13
17
  function levenshtein(a, b) {
@@ -100,13 +104,32 @@ export function saveProfile(name, profileData, credData, configDir = DEFAULT_CON
100
104
  invalidateProfilesCache(configDir);
101
105
  }
102
106
 
103
- export const RECALL_STRICTNESS_LEVELS = ['loose', 'balanced', 'strict'];
104
- export const DEFAULT_RECALL_STRICTNESS = 'balanced';
105
-
106
107
  export function normalizeRecallStrictness(value) {
107
108
  return RECALL_STRICTNESS_LEVELS.includes(value) ? value : DEFAULT_RECALL_STRICTNESS;
108
109
  }
109
110
 
111
+ /**
112
+ * Effective recallStrictness for a profile (backlog #20) — an explicit local
113
+ * `config set recallStrictness` always wins; only falls back to the team's
114
+ * Console-set default (read from the same local, network-free settings cache
115
+ * queue-settings already uses) when the profile has none. Synchronous and
116
+ * local-only by design — this is what the Stop hook calls, and it must never
117
+ * touch the network on every session end.
118
+ *
119
+ * @param {object} [opts]
120
+ * @param {{recallStrictness?: string}} [opts.profile]
121
+ * @param {string} [opts.configDir]
122
+ * @param {string} [opts.cliToken]
123
+ * @returns {string}
124
+ */
125
+ export function resolveEffectiveRecallStrictness({ profile, configDir = DEFAULT_CONFIG_DIR, cliToken } = {}) {
126
+ if (profile?.recallStrictness && RECALL_STRICTNESS_LEVELS.includes(profile.recallStrictness)) {
127
+ return profile.recallStrictness;
128
+ }
129
+ const cached = readEffectiveRecallSettings(configDir, { cliToken });
130
+ return normalizeRecallStrictness(cached.recall_strictness);
131
+ }
132
+
110
133
  export function saveProfileRecallStrictness(name, level, configDir = DEFAULT_CONFIG_DIR) {
111
134
  const config = loadProfiles(configDir) || { profiles: {} };
112
135
  if (!config.profiles[name]) throw new Error(`Unknown profile "${name}"`);
@@ -15,7 +15,7 @@ import { TICKET_KEY_PATTERN } from './cli.mjs';
15
15
  import { isLicensed, showUpgradePrompt } from './license.mjs';
16
16
  import { listNotes } from './recall-vault.mjs';
17
17
  import { readCliToken } from './cli-auth.mjs';
18
- import { resolveProfile, loadProfileRecallTeamId } from './profile-resolver.mjs';
18
+ import { resolveProfile, loadProfileRecallTeamId, resolveEffectiveRecallStrictness, RECALL_STRICTNESS_LEVELS } from './profile-resolver.mjs';
19
19
  import { pullNotes } from './recall-sync.mjs';
20
20
  import { maybeAutoFlush, flushQueue, readQueue } from './recall-queue.mjs';
21
21
  import { getEffectiveRecallSettingsWithSource } from './recall-settings-sync.mjs';
@@ -128,7 +128,9 @@ export async function runRecallSettings(cmdArgs, {
128
128
  stream = process.stdout,
129
129
  isLicensedFn = isLicensed,
130
130
  readCliTokenFn = readCliToken,
131
+ resolveProfileFn = resolveProfile,
131
132
  getEffectiveRecallSettingsWithSourceFn = getEffectiveRecallSettingsWithSource,
133
+ resolveEffectiveRecallStrictnessFn = resolveEffectiveRecallStrictness,
132
134
  } = {}) {
133
135
  if (!isLicensedFn('pro', configDir)) {
134
136
  showUpgradePrompt('pro', 'ticketlens recall', { stream });
@@ -143,5 +145,16 @@ export async function runRecallSettings(cmdArgs, {
143
145
  stream.write(` Max queued notes: ${settings.max_queue_size}\n`);
144
146
  stream.write(` Queued note expiry: ${settings.max_entry_age_ms / 86_400_000} days\n`);
145
147
  stream.write(` Source: ${SOURCE_LABELS[source]}\n`);
148
+
149
+ // recall_strictness has its own precedence, separate from the queue
150
+ // settings above: an explicit local `config set recallStrictness` always
151
+ // wins over the team's Console default (backlog #20) — so its effective
152
+ // value and source are reported independently rather than reusing `source`.
153
+ const profile = resolveProfileFn(null, { configDir, cwd: process.cwd() });
154
+ const hasLocalOverride = RECALL_STRICTNESS_LEVELS.includes(profile?.recallStrictness);
155
+ const recallStrictness = resolveEffectiveRecallStrictnessFn({ profile, configDir, cliToken });
156
+ const strictnessSource = hasLocalOverride ? 'your local `config set recallStrictness`' : SOURCE_LABELS[source];
157
+ stream.write(` Recall capture strictness: ${recallStrictness}\n`);
158
+ stream.write(` Source: ${strictnessSource}\n`);
146
159
  return { ok: true };
147
160
  }
@@ -26,6 +26,7 @@ import { DEFAULT_CONFIG_DIR } from './config.mjs';
26
26
  import { apiBase } from './api-utils.mjs';
27
27
  import { writeFileAtomically } from './recall-vault.mjs';
28
28
  import { hashToken } from './recall-sync.mjs';
29
+ import { RECALL_STRICTNESS_LEVELS, DEFAULT_RECALL_STRICTNESS } from './recall-strictness.mjs';
29
30
 
30
31
  const SETTINGS_PATH = '/v1/recall/settings';
31
32
  const CACHE_FILE = 'recall-settings-cache.json';
@@ -37,14 +38,19 @@ const CACHE_FILE = 'recall-settings-cache.json';
37
38
  const FETCH_TIMEOUT_MS = 3_000;
38
39
 
39
40
  // Platform defaults — must match RecallSettings::DEFAULTS in ticketlens-api.
41
+ // recall_strictness (backlog #20) is a team default only — a profile's own
42
+ // explicit `config set recallStrictness` always wins over it; see
43
+ // profile-resolver.mjs's resolveEffectiveRecallStrictness().
40
44
  export const DEFAULT_RECALL_SETTINGS = {
41
45
  flush_cooldown_ms: 900_000, // 15 minutes
42
46
  timeout_ms: 4_000, // 4 seconds
43
47
  max_queue_size: 200,
44
48
  max_entry_age_ms: 2_592_000_000, // 30 days
49
+ recall_strictness: DEFAULT_RECALL_STRICTNESS,
45
50
  };
46
51
 
47
52
  // Inclusive [min, max] bounds — must match RecallSettings::BOUNDS in ticketlens-api.
53
+ // recall_strictness is an enum, not a numeric range — see recall-strictness.mjs.
48
54
  export const RECALL_SETTINGS_BOUNDS = {
49
55
  flush_cooldown_ms: [60_000, 86_400_000], // 1m .. 24h
50
56
  timeout_ms: [1_000, 30_000], // 1s .. 30s
@@ -57,6 +63,9 @@ function cachePath(configDir) {
57
63
  }
58
64
 
59
65
  function clamp(field, value) {
66
+ if (field === 'recall_strictness') {
67
+ return RECALL_STRICTNESS_LEVELS.includes(value) ? value : DEFAULT_RECALL_SETTINGS.recall_strictness;
68
+ }
60
69
  if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_RECALL_SETTINGS[field];
61
70
  const [min, max] = RECALL_SETTINGS_BOUNDS[field];
62
71
  return Math.min(Math.max(value, min), max);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The three Recall-capture strictness levels — single source of truth.
3
+ * Zero imports of its own (a leaf module) so both profile-resolver.mjs and
4
+ * recall-settings-sync.mjs can import it without risking a circular import
5
+ * between those two.
6
+ */
7
+
8
+ export const RECALL_STRICTNESS_LEVELS = ['loose', 'balanced', 'strict'];
9
+ export const DEFAULT_RECALL_STRICTNESS = 'balanced';