ticketlens 0.38.38 → 0.38.40
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
|
@@ -460,7 +460,7 @@ Every note is scanned before saving — anything shaped like a real secret (API
|
|
|
460
460
|
|
|
461
461
|
**Tags matter for search relevance.** `--tags=a,b` accepts anything, but a generic tag (the project name, "gotcha", "bug") gives future search almost nothing to match on. Tag with what the note is actually *about* — the specific technology, error type, or root cause (`retry-backoff`, `null-pointer`, `auth-middleware`) — so it surfaces when someone else hits the same problem.
|
|
462
462
|
|
|
463
|
-
**Local file attachments.** `note add --attach=path1,path2` saves a screenshot or file alongside the note, in your local vault only (`~/.ticketlens/recall/<PREFIX>/<note-id>/`) — same 10 MB/file, 50 MB/call, 20-file caps as ticket attachments. Unlike ticket attachments, this is never pushed to Team Recall sync — a teammate who pulls the note gets the text only, not the file.
|
|
463
|
+
**Local file attachments.** `note add --attach=path1,path2` (or the `recall_add` MCP tool's `attachments` array) saves a screenshot or file alongside the note, in your local vault only (`~/.ticketlens/recall/<PREFIX>/<note-id>/`) — same 10 MB/file, 50 MB/call, 20-file caps as ticket attachments. Unlike ticket attachments, this is never pushed to Team Recall sync — a teammate who pulls the note gets the text only, not the file.
|
|
464
464
|
|
|
465
465
|
**Gaps** — every `ticketlens PROJ-123` brief also diffs the ticket's own description against its linked tickets (from the depth traversal you already requested) and its own downloaded attachments, looking for requirements mentioned there but missing here. Anything uncovered shows up under a `## Gaps` section, citing exactly where it came from — a linked ticket key or an attachment filename — as evidence, never an instruction to act on. Nothing is saved anywhere; it's recomputed fresh on every fetch. Requires a Pro license, same as Recall. No network call beyond what the brief already made.
|
|
466
466
|
|
package/package.json
CHANGED
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.
|
|
1
|
+
<!-- jtb-skill-version: 0.42.0 -->
|
|
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.
|
|
@@ -371,7 +371,7 @@ echo "The body text of the note, one or more paragraphs." | \
|
|
|
371
371
|
|
|
372
372
|
**Choosing tags:** derive them 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`. A tag like `jtb` or `ticketlens` tells a future search nothing that the ticket/project context doesn't already say; a tag like `retry-backoff` is what actually surfaces this note when someone else hits the same problem. A tag that just restates the title in different words, or one you can't trace to a specific sentence in the body, gives that same zero signal — if you can't point to the exact phrase that justifies it, drop it. Same rule whether you're constructing the bash command above or calling `recall_add` directly — see its tool description for the same guidance.
|
|
373
373
|
|
|
374
|
-
**Attaching local files.** `note add --attach=path1,path2` (Pro) saves a screenshot or file alongside the note, in the local vault only — same 10 MB/file, 50 MB/call, 20-file caps as `ticket_create`/`ticket_comment`'s `--attach`. Unlike ticket attachments, this never uploads anywhere: it is not pushed to the team Console backend even with Team Recall sync active, so a teammate who pulls this note gets the text only, not the file. The `recall_add` MCP tool
|
|
374
|
+
**Attaching local files.** `note add --attach=path1,path2` (Pro) saves a screenshot or file alongside the note, in the local vault only — same 10 MB/file, 50 MB/call, 20-file caps as `ticket_create`/`ticket_comment`'s `--attach`. Unlike ticket attachments, this never uploads anywhere: it is not pushed to the team Console backend even with Team Recall sync active, so a teammate who pulls this note gets the text only, not the file. The `recall_add` MCP tool has a matching `attachments` array parameter — prefer it over the bash form when available, same rule as the rest of this section.
|
|
375
375
|
|
|
376
376
|
To search saved notes directly (outside of automatic brief injection): `ticketlens recall "<query>"`.
|
|
377
377
|
|
|
@@ -362,10 +362,11 @@ async function callCollisions(args, { configDir, runCollisionsFn }) {
|
|
|
362
362
|
* `--ticket=EVIL-999` stays inert precisely because it's never anything
|
|
363
363
|
* but one opaque array element.
|
|
364
364
|
*/
|
|
365
|
-
function buildNoteAddArgs({ title, ticket, tags }) {
|
|
365
|
+
function buildNoteAddArgs({ title, ticket, tags, attachments }) {
|
|
366
366
|
const args = [`--title=${title}`];
|
|
367
367
|
if (ticket) args.push(`--ticket=${ticket}`);
|
|
368
368
|
if (Array.isArray(tags) && tags.length > 0) args.push(`--tags=${tags.join(',')}`);
|
|
369
|
+
if (Array.isArray(attachments) && attachments.length > 0) args.push(`--attach=${attachments.join(',')}`);
|
|
369
370
|
return args;
|
|
370
371
|
}
|
|
371
372
|
|
|
@@ -165,6 +165,7 @@ export const TOOLS = [
|
|
|
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
167
|
body: { type: 'string', description: 'The note body — one or more paragraphs.' },
|
|
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. Unlike ticket attachments, these are never pushed to Team Recall sync — a teammate who pulls this note gets the text only, not the file.' },
|
|
168
169
|
},
|
|
169
170
|
required: ['title', 'body'],
|
|
170
171
|
},
|
|
@@ -150,6 +150,68 @@ const CODE_FILENAME_RE = new RegExp(`^([A-Za-z][A-Za-z-]*)\\.(${CODE_EXTENSION_A
|
|
|
150
150
|
// text regardless of any token's label-word status.
|
|
151
151
|
const FILENAME_REFERENCE_RE = new RegExp(`^[A-Za-z][A-Za-z-]*\\.(${CODE_EXTENSION_ALTERNATION})('s)?$`, 'i');
|
|
152
152
|
|
|
153
|
+
// Underscore-delimited segments (Zend-1-style PHP class names — a common
|
|
154
|
+
// legacy naming convention, e.g. "Acme_Http_ClientFactory",
|
|
155
|
+
// "Zend_Http_Client") are letters-only per segment, no digit support (same
|
|
156
|
+
// convention as CODE_FILENAME_RE's stem). Deliberately does NOT require
|
|
157
|
+
// isHyphenatedWordCompound's "no internal case switch" guard: that guard
|
|
158
|
+
// exists there to keep base64-shaped content from posing as a lowercase-
|
|
159
|
+
// English hyphenated compound, but PascalCase segments ARE the real,
|
|
160
|
+
// expected positive signal for a class name ("ClientFactory",
|
|
161
|
+
// "SaleSystem") — requiring their absence would reject the exact
|
|
162
|
+
// legitimate shape this exists to recognize. See
|
|
163
|
+
// looksLikeCodeIdentifierOrPath's own comment for how this stays
|
|
164
|
+
// downgrade-only despite that asymmetry.
|
|
165
|
+
const UNDERSCORE_COMPOUND_RE = /^[A-Za-z]+(_[A-Za-z]+)+$/;
|
|
166
|
+
|
|
167
|
+
// Slash-delimited path segments (a namespace-shaped filesystem/class path —
|
|
168
|
+
// "acme/library/Billing/Client/Http/Adapter/Fetch/", optionally trailing-
|
|
169
|
+
// slash-terminated). Allows digits per segment (real
|
|
170
|
+
// directory/namespace segments routinely do — "v2", "api2"), unlike the
|
|
171
|
+
// underscore shape above — a deliberate, narrower convention than mixing
|
|
172
|
+
// delimiters within one candidate would need.
|
|
173
|
+
const SLASH_PATH_RE = /^[A-Za-z0-9]+(\/[A-Za-z0-9]+)+\/?$/;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Downgrade-only, same never-exempt treatment as looksLikeCodeFilename and
|
|
177
|
+
* looksLikeCodeSyntax below: true for a candidate shaped like a multi-segment
|
|
178
|
+
* code identifier (underscore-delimited PHP/Zend-1-style class name) or a
|
|
179
|
+
* namespace/filesystem path (slash-delimited), each segment capped at
|
|
180
|
+
* MAX_COMPOUND_SEGMENT_LENGTH so a long random run can't pose as one
|
|
181
|
+
* "segment" — same cap isHyphenatedWordCompound already uses. Backlog #17:
|
|
182
|
+
* isLabelWord's ordinary-word branch only matches letters-only tokens with
|
|
183
|
+
* no separator at all, and neither existing downgrade path below requires a
|
|
184
|
+
* file extension or bracket/paren — so a standalone identifier or path like
|
|
185
|
+
* the two shapes above had no exemption path whatsoever, unlike a hyphenated
|
|
186
|
+
* compound or a dotted filename.
|
|
187
|
+
*
|
|
188
|
+
* A disguised real secret matching either shape still surfaces as a
|
|
189
|
+
* warning, never a silent pass — see the security regression tests
|
|
190
|
+
* alongside this function's own tests for both shapes.
|
|
191
|
+
*
|
|
192
|
+
* Known accepted gap (security review, backlog #17): unlike
|
|
193
|
+
* isHyphenatedWordCompound, this has no whole-token case-switch guard (see
|
|
194
|
+
* UNDERSCORE_COMPOUND_RE's own comment for why one can't be added without
|
|
195
|
+
* rejecting the real Zend_Http_Client-shaped identifiers this exists to
|
|
196
|
+
* recognize) — so a uniform-case (all-upper or all-lower) letters-only
|
|
197
|
+
* secret needs only ONE underscore or slash inserted to downgrade from a
|
|
198
|
+
* full reject to a warning, no camouflage (fake extension, case pattern)
|
|
199
|
+
* required. Still never a silent pass — always a warning — so this is the
|
|
200
|
+
* same class of trade-off as CODE_SYNTAX_RE's even less-constrained
|
|
201
|
+
* downgrade above (any bracket/paren, no shape requirement at all), not a
|
|
202
|
+
* new exposure.
|
|
203
|
+
*/
|
|
204
|
+
function looksLikeCodeIdentifierOrPath(rawToken) {
|
|
205
|
+
const stripped = stripEdgePunctuation(rawToken);
|
|
206
|
+
if (UNDERSCORE_COMPOUND_RE.test(stripped)) {
|
|
207
|
+
return stripped.split('_').every(segment => segment.length <= MAX_COMPOUND_SEGMENT_LENGTH);
|
|
208
|
+
}
|
|
209
|
+
if (SLASH_PATH_RE.test(stripped)) {
|
|
210
|
+
return stripped.split('/').filter(Boolean).every(segment => segment.length <= MAX_COMPOUND_SEGMENT_LENGTH);
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
153
215
|
function looksLikeCodeFilename(rawToken) {
|
|
154
216
|
const stripped = stripEdgePunctuation(rawToken);
|
|
155
217
|
const match = stripped.match(CODE_FILENAME_RE);
|
|
@@ -443,7 +505,11 @@ export function scanForSecrets({ title = '', tags = [], body = '' } = {}) {
|
|
|
443
505
|
// Downgrading to a warning — never silently dropping the signal — matches
|
|
444
506
|
// how an email address is already handled below.
|
|
445
507
|
const randomCandidates = candidates.filter(token => !EMAIL_RE.test(token) && looksRandom(token, combined));
|
|
446
|
-
if (
|
|
508
|
+
if (
|
|
509
|
+
randomCandidates.some(
|
|
510
|
+
token => !looksLikeCodeFilename(token) && !looksLikeCodeSyntax(token) && !looksLikeCodeIdentifierOrPath(token),
|
|
511
|
+
)
|
|
512
|
+
) {
|
|
447
513
|
reasons.push('Contains a long, random-looking string that could be a secret.');
|
|
448
514
|
}
|
|
449
515
|
if (randomCandidates.some(token => looksLikeCodeFilename(token))) {
|
|
@@ -452,6 +518,9 @@ export function scanForSecrets({ title = '', tags = [], body = '' } = {}) {
|
|
|
452
518
|
if (randomCandidates.some(token => looksLikeCodeSyntax(token))) {
|
|
453
519
|
warnings.push('Contains a code-syntax-shaped token (brackets or parentheses) that also reads as high-entropy — double-check it is not a credential.');
|
|
454
520
|
}
|
|
521
|
+
if (randomCandidates.some(token => looksLikeCodeIdentifierOrPath(token))) {
|
|
522
|
+
warnings.push('Contains a code-identifier-or-path-shaped token that also reads as high-entropy — double-check it is not a credential.');
|
|
523
|
+
}
|
|
455
524
|
|
|
456
525
|
if (EMAIL_RE.test(combined)) {
|
|
457
526
|
warnings.push('Contains an email address.');
|