ticketlens 0.38.44 → 0.38.45
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/package.json
CHANGED
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.42.
|
|
1
|
+
<!-- jtb-skill-version: 0.42.4 -->
|
|
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.
|
|
@@ -356,6 +356,8 @@ Example that qualifies: an undocumented schema quirk found only by reading raw D
|
|
|
356
356
|
|
|
357
357
|
Do not capture: routine/obvious information, anything already fully covered by the ticket description or comments, or content shaped like a credential or API key — the tool rejects those outright at save time.
|
|
358
358
|
|
|
359
|
+
**Length:** keep both the title and body brief — a sharp one- or two-sentence takeaway, not a paragraph. The recommended max scales with the same strictness level as the section above: 20 words (strict), 30 words (balanced, default), 50 words (loose). Going over doesn't block the save — the CLI/MCP tool prints a non-blocking warning so you can tighten it, the same way it prints warnings for a detected email address or similar low-severity secret-scan hit.
|
|
360
|
+
|
|
359
361
|
Capture **at the moment you confirm the insight**, not deferred to the end of the session — don't ask the user first, just do it. It requires a Pro license; on a Free account it will no-op with an upgrade hint on stderr, which is fine to ignore. It never blocks or fails the session.
|
|
360
362
|
|
|
361
363
|
Immediately before dispatching the call, say one line in your response: `🔖 Recall-flag: <one-line reason>`. This is a fixed marker some harness setups scan for to confirm captures are actually happening — always emit it right when you decide to capture, even if you go on to call `note add` in the same breath.
|
|
@@ -365,7 +367,7 @@ Immediately before dispatching the call, say one line in your response: `🔖 Re
|
|
|
365
367
|
If your environment lets you dispatch a tool call without waiting for its result — a background task, an async subagent, a fire-and-forget job — try it **once**. If it errors for any reason, don't retry the background path — fall back to calling it synchronously immediately. It's fast and never blocks or fails your session either way, so the synchronous fallback costs nothing; repeatedly retrying a broken background mechanism only adds visible noise for no benefit.
|
|
366
368
|
|
|
367
369
|
```bash
|
|
368
|
-
echo "
|
|
370
|
+
echo "Retry client swallowed 429s silently; added backoff and a warning log on the third failure." | \
|
|
369
371
|
ticketlens note add --title="Short title" --ticket=TICKET-KEY --tags=a,b
|
|
370
372
|
```
|
|
371
373
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Centralised here to avoid triplicating the regex and warning logic.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export const DEFAULT_API_BASE = '
|
|
6
|
+
export const DEFAULT_API_BASE = 'https://api.ticketlens.app';
|
|
7
7
|
export const DEFAULT_SITE_BASE = 'https://ticketlens.app';
|
|
8
8
|
|
|
9
9
|
// Matches localhost, 127.0.0.1, and any hostname ending in .test or .local,
|
|
@@ -161,10 +161,10 @@ export const TOOLS = [
|
|
|
161
161
|
inputSchema: {
|
|
162
162
|
type: 'object',
|
|
163
163
|
properties: {
|
|
164
|
-
title: { type: 'string', description: 'Short one-line title.' },
|
|
164
|
+
title: { type: 'string', description: 'Short one-line title, ~20 (strict), 30 (balanced), or 50 (loose) words max — scales with recallStrictness — going over doesn\'t block the save, but prints a non-blocking warning.' },
|
|
165
165
|
ticket: { type: 'string', description: 'Optional ticket key, e.g. PROJ-123.' },
|
|
166
166
|
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags derived from this note\'s actual content — the specific technology, error type, root cause, or affected component (e.g. "retry-backoff", "null-pointer", "auth-middleware"). Never the project name or a generic category word like "gotcha" or "bug" — those provide no search signal to someone else looking for this note later. A tag that just restates the title in different words, or one you cannot trace to a specific sentence in the body, gives that same zero signal — if you cannot point to the exact phrase that justifies it, drop it.' },
|
|
167
|
-
body: { type: 'string', description: 'The note body —
|
|
167
|
+
body: { type: 'string', description: 'The note body — brief, not a paragraph. ~20 (strict), 30 (balanced), or 50 (loose) words max — scales with recallStrictness — going over doesn\'t block the save, but prints a non-blocking warning.' },
|
|
168
168
|
attachments: { type: 'array', items: { type: 'string' }, description: 'Local file paths to attach (screenshots, logs, etc.), saved next to the note in the local vault. Same 10MB/file, 50MB/call, 20-file caps as ticket_comment/ticket_create for the local save. If this account is entitled and the note syncs to a team, attachments sync too — visible and downloadable from Console > Admin > Recall — but the sync path has a lower 12MB/call cap than the local save (backend request-size limit). Going over it fails the WHOLE push (note text included), not just the attachment; the note stays saved locally either way.' },
|
|
169
169
|
},
|
|
170
170
|
required: ['title', 'body'],
|
|
@@ -177,7 +177,7 @@ export const TOOLS = [
|
|
|
177
177
|
type: 'object',
|
|
178
178
|
properties: {
|
|
179
179
|
id: { type: 'string', description: 'Note id to patch, as printed by recall_add or recall_search.' },
|
|
180
|
-
body: { type: 'string', description: 'The replacement note body.' },
|
|
180
|
+
body: { type: 'string', description: 'The replacement note body — brief, not a paragraph. ~20 (strict), 30 (balanced), or 50 (loose) words max — scales with recallStrictness — going over doesn\'t block the update, but prints a non-blocking warning.' },
|
|
181
181
|
ticket: { type: 'string', description: 'Ticket key the note is about, e.g. PROJ-123. Optional — narrows the search when omitted, the vault is searched across all ticket prefixes.' },
|
|
182
182
|
expectMtime: { type: 'number', description: 'The note file\'s mtime (ms) last observed by the caller. If the note changed since then, the patch is a no-op rather than an overwrite — optimistic concurrency, not a hard requirement.' },
|
|
183
183
|
},
|
|
@@ -10,10 +10,10 @@ import path from 'node:path';
|
|
|
10
10
|
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
11
11
|
import { isLicensed, showUpgradePrompt } from './license.mjs';
|
|
12
12
|
import { scanForSecrets } from './secret-scanner.mjs';
|
|
13
|
-
import { checkNoteStructure } from './note-structural-check.mjs';
|
|
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 } from './profile-resolver.mjs';
|
|
16
|
+
import { resolveProfile, loadProfileRecallTeamId, normalizeRecallStrictness } 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';
|
|
@@ -103,6 +103,7 @@ export async function runNoteAdd(cmdArgs, {
|
|
|
103
103
|
readStdin = defaultReadStdin,
|
|
104
104
|
isLicensedFn = isLicensed,
|
|
105
105
|
checkNoteStructureFn = checkNoteStructure,
|
|
106
|
+
checkWordCountFn = checkWordCount,
|
|
106
107
|
scanForSecretsFn = scanForSecrets,
|
|
107
108
|
writeNoteFn = writeNote,
|
|
108
109
|
readCliTokenFn = readCliToken,
|
|
@@ -153,6 +154,16 @@ export async function runNoteAdd(cmdArgs, {
|
|
|
153
154
|
return { written: false };
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
// 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.
|
|
160
|
+
const profile = resolveProfileFn(ticketKey || null, { configDir, cwd: process.cwd() });
|
|
161
|
+
const strictness = normalizeRecallStrictness(profile?.recallStrictness);
|
|
162
|
+
const wordCount = checkWordCountFn({ title, body }, { maxWords: WORD_LIMITS[strictness] });
|
|
163
|
+
for (const warning of wordCount.warnings) {
|
|
164
|
+
stream.write(` Warning: ${warning}\n`);
|
|
165
|
+
}
|
|
166
|
+
|
|
156
167
|
const scan = scanForSecretsFn({ title, tags, body });
|
|
157
168
|
if (scan.rejected) {
|
|
158
169
|
stream.write(` Note not saved — ${scan.reasons.join(' ')}\n`);
|
|
@@ -187,7 +198,7 @@ export async function runNoteAdd(cmdArgs, {
|
|
|
187
198
|
const warn = (s) => stream.write(s);
|
|
188
199
|
// Field names match PushRequest's validation rules (external_id, tickets) —
|
|
189
200
|
// the backend wire contract, not the local vault's internal camelCase shape.
|
|
190
|
-
|
|
201
|
+
// `profile` was already resolved above (needed there for the word-count check).
|
|
191
202
|
const recallTeamId = profile ? loadProfileRecallTeamIdFn(profile.name, configDir) : null;
|
|
192
203
|
const payload = { external_id: id, title, tickets: ticketKeys, tags, author, sources: [], body, captured_at: capturedAt.toISOString() };
|
|
193
204
|
if (recallTeamId !== null) payload.group_id = recallTeamId;
|
|
@@ -225,6 +236,8 @@ export async function runNotePatch(cmdArgs, {
|
|
|
225
236
|
readStdin = defaultReadStdin,
|
|
226
237
|
isLicensedFn = isLicensed,
|
|
227
238
|
checkNoteStructureFn = checkNoteStructure,
|
|
239
|
+
checkWordCountFn = checkWordCount,
|
|
240
|
+
resolveProfileFn = resolveProfile,
|
|
228
241
|
scanForSecretsFn = scanForSecrets,
|
|
229
242
|
patchNoteBodyFn = patchNoteBody,
|
|
230
243
|
} = {}) {
|
|
@@ -253,6 +266,13 @@ export async function runNotePatch(cmdArgs, {
|
|
|
253
266
|
return { patched: false };
|
|
254
267
|
}
|
|
255
268
|
|
|
269
|
+
const profile = resolveProfileFn(ticketKey || null, { configDir, cwd: process.cwd() });
|
|
270
|
+
const strictness = normalizeRecallStrictness(profile?.recallStrictness);
|
|
271
|
+
const wordCount = checkWordCountFn({ body }, { maxWords: WORD_LIMITS[strictness] });
|
|
272
|
+
for (const warning of wordCount.warnings) {
|
|
273
|
+
stream.write(` Warning: ${warning}\n`);
|
|
274
|
+
}
|
|
275
|
+
|
|
256
276
|
const scan = scanForSecretsFn({ title: '', tags: [], body });
|
|
257
277
|
if (scan.rejected) {
|
|
258
278
|
stream.write(` Note not updated — ${scan.reasons.join(' ')}\n`);
|
|
@@ -31,3 +31,39 @@ export function checkNoteStructure({ body = '' } = {}) {
|
|
|
31
31
|
|
|
32
32
|
return { rejected: false, reason: null };
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// Backlog #18 — mandatory brevity cap, one entry per recallStrictness level.
|
|
36
|
+
// Warning-only: a rambling note still saves, it just gets flagged so the
|
|
37
|
+
// caller can tighten it, same non-blocking pattern scanForSecrets uses for
|
|
38
|
+
// its `warnings` array.
|
|
39
|
+
export const WORD_LIMITS = { strict: 20, balanced: 30, loose: 50 };
|
|
40
|
+
|
|
41
|
+
function countWords(text) {
|
|
42
|
+
const trimmed = text.trim();
|
|
43
|
+
return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Warning-only companion to checkNoteStructure: flags a title/body that runs
|
|
48
|
+
* long instead of rejecting it. `maxWords` is a plain number so this stays
|
|
49
|
+
* dependency-free — the caller maps recallStrictness to a number.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ title?: string, body?: string }} note
|
|
52
|
+
* @param {{ maxWords?: number }} opts
|
|
53
|
+
* @returns {{ warnings: string[] }}
|
|
54
|
+
*/
|
|
55
|
+
export function checkWordCount({ title = '', body = '' } = {}, { maxWords = WORD_LIMITS.balanced } = {}) {
|
|
56
|
+
const warnings = [];
|
|
57
|
+
|
|
58
|
+
const titleWords = countWords(title);
|
|
59
|
+
if (titleWords > maxWords) {
|
|
60
|
+
warnings.push(`Note title is ${titleWords} words (recommended max: ${maxWords}).`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const bodyWords = countWords(body);
|
|
64
|
+
if (bodyWords > maxWords) {
|
|
65
|
+
warnings.push(`Note body is ${bodyWords} words (recommended max: ${maxWords}).`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { warnings };
|
|
69
|
+
}
|