ticketlens 0.21.4 → 0.21.5
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/bin/ticketlens.mjs
CHANGED
|
@@ -630,6 +630,16 @@ switch (command) {
|
|
|
630
630
|
});
|
|
631
631
|
break;
|
|
632
632
|
}
|
|
633
|
+
if (cmdArgs[0] === 'delete') {
|
|
634
|
+
const { runNoteDelete } = await import('../skills/jtb/scripts/lib/note-command.mjs');
|
|
635
|
+
runNoteDelete(cmdArgs.slice(1)).then(({ deleted }) => {
|
|
636
|
+
if (!deleted) process.exitCode = 1;
|
|
637
|
+
}).catch(err => {
|
|
638
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
639
|
+
process.exitCode = 1;
|
|
640
|
+
});
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
633
643
|
process.stderr.write('Usage: ticketlens note add --title="..." [--ticket=KEY] [--tags=a,b]\n');
|
|
634
644
|
process.exitCode = 1;
|
|
635
645
|
break;
|
package/package.json
CHANGED
|
@@ -520,6 +520,12 @@ export function printNoteHelp({ stream = process.stdout } = {}) {
|
|
|
520
520
|
` Code session — not typically invoked by hand. Every note it writes gets the`,
|
|
521
521
|
` same structural and secret-scan checks ${s.brand('note add')} applies to user input.`,
|
|
522
522
|
'',
|
|
523
|
+
` ${s.bold('ticketlens note delete')} ${s.dim('--id="..." [--ticket=KEY]')} ${s.dim('[Pro]')}`,
|
|
524
|
+
'',
|
|
525
|
+
` Removes a note from your local vault. Local only — if this note was already`,
|
|
526
|
+
` pushed to a team, teammates who pulled it keep their copy; deleting it there`,
|
|
527
|
+
` too is a manager action from the Console (Admin > Recall).`,
|
|
528
|
+
'',
|
|
523
529
|
];
|
|
524
530
|
stream.write(lines.join('\n') + '\n');
|
|
525
531
|
}
|
|
@@ -11,7 +11,7 @@ import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
|
11
11
|
import { isLicensed, showUpgradePrompt } from './license.mjs';
|
|
12
12
|
import { scanForSecrets } from './secret-scanner.mjs';
|
|
13
13
|
import { checkNoteStructure } from './note-structural-check.mjs';
|
|
14
|
-
import { writeNote, patchNoteBody } from './recall-vault.mjs';
|
|
14
|
+
import { writeNote, patchNoteBody, deleteNote } from './recall-vault.mjs';
|
|
15
15
|
import { readCliToken } from './cli-auth.mjs';
|
|
16
16
|
import { pushNote } from './recall-sync.mjs';
|
|
17
17
|
import { enqueueNote, isRetryableFailure, maybeAutoFlush } from './recall-queue.mjs';
|
|
@@ -210,3 +210,45 @@ export async function runNotePatch(cmdArgs, {
|
|
|
210
210
|
stream.write(patched ? ` Updated note (${id})\n` : ` Note not updated — (${id}) not found or already changed.\n`);
|
|
211
211
|
return { patched };
|
|
212
212
|
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Implements `tl note delete` — removes a note from the local vault only.
|
|
216
|
+
*
|
|
217
|
+
* Local-only, same limitation `note patch` already documents: if this note
|
|
218
|
+
* was already pushed to a team (Team Recall enabled), teammates who already
|
|
219
|
+
* pulled it keep their copy — this does not push a tombstone. Deleting a
|
|
220
|
+
* note everywhere it's synced is a manager action from the Console today
|
|
221
|
+
* (Admin > Recall), which does propagate a tombstone on next pull.
|
|
222
|
+
*
|
|
223
|
+
* @param {string[]} cmdArgs
|
|
224
|
+
* @returns {Promise<{ deleted: boolean }>}
|
|
225
|
+
*/
|
|
226
|
+
export async function runNoteDelete(cmdArgs, {
|
|
227
|
+
configDir = DEFAULT_CONFIG_DIR,
|
|
228
|
+
stream = process.stderr,
|
|
229
|
+
isLicensedFn = isLicensed,
|
|
230
|
+
deleteNoteFn = deleteNote,
|
|
231
|
+
} = {}) {
|
|
232
|
+
if (!isLicensedFn('pro', configDir)) {
|
|
233
|
+
showUpgradePrompt('pro', 'ticketlens note', { stream });
|
|
234
|
+
return { deleted: false };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const id = parseFlag(cmdArgs, 'id');
|
|
238
|
+
if (!id) {
|
|
239
|
+
stream.write('Usage: ticketlens note delete --id="..." [--ticket=KEY]\n');
|
|
240
|
+
return { deleted: false };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ticketKey = parseFlag(cmdArgs, 'ticket');
|
|
244
|
+
if (ticketKey && !TICKET_KEY_PATTERN.test(ticketKey)) {
|
|
245
|
+
stream.write(` Invalid --ticket value "${ticketKey}" — expected a ticket key like PROJ-123.\n`);
|
|
246
|
+
return { deleted: false };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const { deleted } = deleteNoteFn({ external_id: id, tickets: ticketKey ? [ticketKey] : [] }, { configDir });
|
|
250
|
+
stream.write(deleted
|
|
251
|
+
? ` Deleted note (${id}) — local vault only; see help for team-synced notes.\n`
|
|
252
|
+
: ` Note not deleted — (${id}) not found.\n`);
|
|
253
|
+
return { deleted };
|
|
254
|
+
}
|
|
@@ -101,25 +101,41 @@ function hasInternalCaseSwitch(token) {
|
|
|
101
101
|
/**
|
|
102
102
|
* True for a token that stops a joined-chunk run: either a recognized git/
|
|
103
103
|
* checksum label word ("commit", "sha256", "md5sum", ...), or an ordinary
|
|
104
|
-
* English word (letters only,
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
104
|
+
* English word (letters only, optionally with an internal possessive/
|
|
105
|
+
* contraction apostrophe — "relay's", "doesn't" — but no base64-style case
|
|
106
|
+
* switching). Anything else — a fragment containing a digit or other symbol,
|
|
107
|
+
* a hyphenated compound, or an all-letter chunk that still reads as random
|
|
108
|
+
* content — stays eligible to join, so a secret split by whitespace can still
|
|
109
|
+
* be reassembled for the entropy check.
|
|
108
110
|
*
|
|
109
|
-
*
|
|
111
|
+
* The apostrophe allowance matters because without it, an ordinary possessive
|
|
112
|
+
* next to another non-label token (e.g. a hyphenated compound: "relay's
|
|
113
|
+
* decision-lookup") never gets a chance to stop the run — both fail to
|
|
114
|
+
* qualify, so they concatenate into one artificial blob whose mixed
|
|
115
|
+
* punctuation trips the entropy threshold. A real false positive, not a
|
|
116
|
+
* hypothetical one (see the regression test below). Hyphenated tokens are
|
|
117
|
+
* deliberately NOT given the same allowance: a short prefix + hyphen + long
|
|
118
|
+
* letter-run is indistinguishable from a real compound word by shape alone
|
|
119
|
+
* (e.g. "sk-abcdefghijklmnop", the letters-only half of a whitespace-split
|
|
120
|
+
* API key) — see the adjacent regression test for exactly this shape. Once
|
|
121
|
+
* the apostrophe fix lets "relay's" stop the run on its own, an adjacent
|
|
122
|
+
* hyphenated compound is short enough on its own to fall under
|
|
123
|
+
* MIN_RANDOM_TOKEN_LENGTH anyway, so it never needed the same allowance.
|
|
124
|
+
*
|
|
125
|
+
* Known accepted gap: this can't tell a genuine possessive from one an
|
|
110
126
|
* attacker deliberately chose as a separator to defeat the scanner (e.g.
|
|
111
|
-
* splitting a secret around
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
127
|
+
* splitting a secret around "user's"). Closing that would require treating
|
|
128
|
+
* ordinary prose words as joinable too, which in turn re-joins real sentence
|
|
129
|
+
* text into false-positive "random" strings (verified empirically — see the
|
|
130
|
+
* regression test for "PROD-123456 for background"). Entropy-based detection
|
|
131
|
+
* is a safety net, not a cryptographic guarantee; the well-known secret
|
|
132
|
+
* shapes in HARD_REJECT_PATTERNS are the layer that's fully whitespace- and
|
|
133
|
+
* word-insertion-proof, since they match a specific literal prefix.
|
|
118
134
|
*/
|
|
119
135
|
function isLabelWord(token) {
|
|
120
136
|
const stripped = stripEdgePunctuation(token);
|
|
121
137
|
if (GIT_REFERENCE_WORD_RE.test(stripped)) return true;
|
|
122
|
-
return /^[A-Za-z]
|
|
138
|
+
return /^[A-Za-z]+(?:'[A-Za-z]+)*$/.test(stripped) && !hasInternalCaseSwitch(stripped);
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
/**
|