@memnexus-ai/cli 1.7.214 → 1.7.215
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/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +43 -0
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/memories.d.ts +88 -0
- package/dist/commands/memories.d.ts.map +1 -1
- package/dist/commands/memories.js +483 -7
- package/dist/commands/memories.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -36,7 +36,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.BULK_DELETE_LIST_PAGE_SIZE = exports.BULK_DELETE_MAX_RETRIES = exports.BULK_DELETE_PAGE_PAUSE_MS = exports.BULK_DELETE_MAX_IDS = void 0;
|
|
39
40
|
exports.parseCodeContextFlag = parseCodeContextFlag;
|
|
41
|
+
exports.parseBatchIds = parseBatchIds;
|
|
42
|
+
exports.isValidIsoDate = isValidIsoDate;
|
|
43
|
+
exports.chunkIds = chunkIds;
|
|
44
|
+
exports.isRateLimitError = isRateLimitError;
|
|
45
|
+
exports.getRetryAfterMs = getRetryAfterMs;
|
|
46
|
+
exports.sleep = sleep;
|
|
47
|
+
exports.bulkDeleteWithRetry = bulkDeleteWithRetry;
|
|
48
|
+
exports.runBulkDeletePaged = runBulkDeletePaged;
|
|
49
|
+
exports.collectAllMemoryIds = collectAllMemoryIds;
|
|
40
50
|
exports.registerMemoriesCommands = registerMemoriesCommands;
|
|
41
51
|
exports.registerMemoryRelationshipCommands = registerMemoryRelationshipCommands;
|
|
42
52
|
exports.registerMemoryTimelineCommand = registerMemoryTimelineCommand;
|
|
@@ -154,7 +164,11 @@ async function readIdsFromStdin() {
|
|
|
154
164
|
* Resolve short ID prefixes to full UUIDs
|
|
155
165
|
* Returns array of resolved IDs (in same order as input)
|
|
156
166
|
*/
|
|
157
|
-
async function resolveShortIds(ids) {
|
|
167
|
+
async function resolveShortIds(ids, opts = {}) {
|
|
168
|
+
// Default warner preserves the existing `get --stdin` behavior (warnings on
|
|
169
|
+
// stdout). Batch delete injects a stderr warner so short-id resolution
|
|
170
|
+
// warnings never pollute the pure-JSON stdout stream under --format json.
|
|
171
|
+
const warn = opts.warn ?? ((msg) => console.log(chalk_1.default.yellow(msg)));
|
|
158
172
|
const MIN_PREFIX_LENGTH = 6;
|
|
159
173
|
const resolvedIds = [];
|
|
160
174
|
const resolvedMemories = new Map();
|
|
@@ -164,7 +178,7 @@ async function resolveShortIds(ids) {
|
|
|
164
178
|
resolvedIds.push(id);
|
|
165
179
|
}
|
|
166
180
|
else if (id.length < MIN_PREFIX_LENGTH) {
|
|
167
|
-
|
|
181
|
+
warn(`Warning: Skipping ID prefix "${id}" (too short, min ${MIN_PREFIX_LENGTH} chars)`);
|
|
168
182
|
}
|
|
169
183
|
else {
|
|
170
184
|
// Resolve short ID
|
|
@@ -175,10 +189,10 @@ async function resolveShortIds(ids) {
|
|
|
175
189
|
});
|
|
176
190
|
const matches = listResponse.data?.data || [];
|
|
177
191
|
if (matches.length === 0) {
|
|
178
|
-
|
|
192
|
+
warn(`Warning: No memory found with prefix "${id}"`);
|
|
179
193
|
}
|
|
180
194
|
else if (matches.length > 1) {
|
|
181
|
-
|
|
195
|
+
warn(`Warning: Ambiguous prefix "${id}" matches ${matches.length} memories, skipping`);
|
|
182
196
|
}
|
|
183
197
|
else {
|
|
184
198
|
resolvedIds.push(matches[0].id);
|
|
@@ -349,6 +363,344 @@ async function handleBatchMemoryGet(ids, options) {
|
|
|
349
363
|
throw error;
|
|
350
364
|
}
|
|
351
365
|
}
|
|
366
|
+
// ── Batch delete (PO CLI-latency directive, iteration 1) ─────────────────────
|
|
367
|
+
// Scripted single-deletes hit core-api's deleteRateLimit (20/min for CLI
|
|
368
|
+
// clients): a 53-memory loop issues 53 requests and fails partway. The server's
|
|
369
|
+
// DELETE /api/memories/bulk endpoint counts a whole bulk request as ONE against
|
|
370
|
+
// that limiter (max 100 ids/request, requires confirmDeletion: true), so 500
|
|
371
|
+
// ids = 5 requests — well under 20/min. These helpers implement paging, the
|
|
372
|
+
// 429/Retry-After-honoring retry, and the confirmation gate.
|
|
373
|
+
/** Max ids the server accepts per bulk-delete request (bulk-operations.schema.ts). */
|
|
374
|
+
exports.BULK_DELETE_MAX_IDS = 100;
|
|
375
|
+
/** Pause between sequential 100-id pages, keeps request cadence gentle. */
|
|
376
|
+
exports.BULK_DELETE_PAGE_PAUSE_MS = 250;
|
|
377
|
+
/** Max retries per page on 429 before giving up. */
|
|
378
|
+
exports.BULK_DELETE_MAX_RETRIES = 3;
|
|
379
|
+
/** Page size when listing memories for --all. */
|
|
380
|
+
exports.BULK_DELETE_LIST_PAGE_SIZE = 100;
|
|
381
|
+
/**
|
|
382
|
+
* Parse `--batch <ids...>` values into a flat, de-duplicated id list.
|
|
383
|
+
* Commander collects the variadic option into a string[]; each element may
|
|
384
|
+
* itself be comma- or space-separated, so we split again and trim.
|
|
385
|
+
*/
|
|
386
|
+
function parseBatchIds(values) {
|
|
387
|
+
if (!values || values.length === 0)
|
|
388
|
+
return [];
|
|
389
|
+
const out = [];
|
|
390
|
+
for (const value of values) {
|
|
391
|
+
for (const part of value.split(/[,\s]+/)) {
|
|
392
|
+
const trimmed = part.trim();
|
|
393
|
+
if (trimmed.length > 0)
|
|
394
|
+
out.push(trimmed);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* True when `value` is a parseable ISO 8601 date. Used to validate the
|
|
401
|
+
* `--after`/`--before` filters on `--all` before they count as filters — a
|
|
402
|
+
* malformed date must be rejected, not silently treated as a real filter that
|
|
403
|
+
* bypasses the delete-everything gate. Requires a date-shaped prefix
|
|
404
|
+
* (YYYY-MM-DD…) so lenient locale strings like "notadate" or "July" are refused.
|
|
405
|
+
*/
|
|
406
|
+
function isValidIsoDate(value) {
|
|
407
|
+
if (typeof value !== 'string')
|
|
408
|
+
return false;
|
|
409
|
+
if (!/^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(value.trim()))
|
|
410
|
+
return false;
|
|
411
|
+
return !Number.isNaN(Date.parse(value));
|
|
412
|
+
}
|
|
413
|
+
/** Split ids into pages of at most `size` (default 100). */
|
|
414
|
+
function chunkIds(ids, size = exports.BULK_DELETE_MAX_IDS) {
|
|
415
|
+
const chunks = [];
|
|
416
|
+
for (let i = 0; i < ids.length; i += size) {
|
|
417
|
+
chunks.push(ids.slice(i, i + size));
|
|
418
|
+
}
|
|
419
|
+
return chunks;
|
|
420
|
+
}
|
|
421
|
+
/** True when an error is an HTTP 429 (rate limited). */
|
|
422
|
+
function isRateLimitError(error) {
|
|
423
|
+
const status = error?.status ?? error?.statusCode ?? error?.response?.status;
|
|
424
|
+
return status === 429;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Extract the server's Retry-After hint in milliseconds, or null if absent.
|
|
428
|
+
* The API surfaces it as a body field (`retryAfter`, seconds) on 429s
|
|
429
|
+
* (see lib/errors.ts) and may also send a Retry-After header; we honor both.
|
|
430
|
+
*/
|
|
431
|
+
function getRetryAfterMs(error) {
|
|
432
|
+
const data = error?.data ?? error?.response?.data ?? {};
|
|
433
|
+
const secs = data?.retryAfter ?? data?.retry_after;
|
|
434
|
+
if (typeof secs === 'number' && Number.isFinite(secs) && secs >= 0) {
|
|
435
|
+
return Math.round(secs * 1000);
|
|
436
|
+
}
|
|
437
|
+
const headers = error?.headers;
|
|
438
|
+
let headerVal;
|
|
439
|
+
if (headers) {
|
|
440
|
+
if (typeof headers.get === 'function') {
|
|
441
|
+
headerVal = headers.get('retry-after') ?? headers.get('Retry-After');
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
headerVal = headers['retry-after'] ?? headers['Retry-After'];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (headerVal) {
|
|
448
|
+
const n = parseInt(String(headerVal), 10);
|
|
449
|
+
if (Number.isFinite(n) && n >= 0)
|
|
450
|
+
return n * 1000;
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
/** Promise-based sleep. Extracted so tests can inject a no-op/spy. */
|
|
455
|
+
function sleep(ms) {
|
|
456
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Delete a single page, retrying on 429 up to `maxRetries` times. Honors the
|
|
460
|
+
* server's Retry-After hint; falls back to a linear backoff otherwise.
|
|
461
|
+
*/
|
|
462
|
+
async function bulkDeleteWithRetry(ids, deps) {
|
|
463
|
+
const maxRetries = deps.maxRetries ?? exports.BULK_DELETE_MAX_RETRIES;
|
|
464
|
+
const defaultRetryMs = deps.defaultRetryMs ?? 1000;
|
|
465
|
+
let attempt = 0;
|
|
466
|
+
for (;;) {
|
|
467
|
+
try {
|
|
468
|
+
return await deps.deleteFn(ids);
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
if (isRateLimitError(error) && attempt < maxRetries) {
|
|
472
|
+
const waitMs = getRetryAfterMs(error) ?? defaultRetryMs * (attempt + 1);
|
|
473
|
+
deps.onRateLimited?.(waitMs);
|
|
474
|
+
await deps.sleep(waitMs);
|
|
475
|
+
attempt++;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/** Best-effort human-readable message for a thrown request error. */
|
|
483
|
+
function errorMessage(error) {
|
|
484
|
+
return (error?.data?.error ??
|
|
485
|
+
error?.data?.message ??
|
|
486
|
+
error?.response?.data?.error ??
|
|
487
|
+
error?.message ??
|
|
488
|
+
String(error));
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Delete `ids` in sequential ≤100-id pages with a brief pause between pages and
|
|
492
|
+
* per-page 429 retry. Aggregates per-page results.
|
|
493
|
+
*
|
|
494
|
+
* A page that exhausts its 429 retries (or hits a non-429 error) is CAUGHT here
|
|
495
|
+
* rather than allowed to throw past the aggregation (QA MAJOR #2): every id in
|
|
496
|
+
* the failed page is recorded as a failure, `aborted` is set, remaining pages
|
|
497
|
+
* are counted in `notAttempted`, and paging stops. This guarantees the caller
|
|
498
|
+
* always receives a structured partial result — earlier pages' real deletions
|
|
499
|
+
* are never lost, and json-mode callers still get their JSON document.
|
|
500
|
+
*/
|
|
501
|
+
async function runBulkDeletePaged(ids, deps) {
|
|
502
|
+
const chunks = chunkIds(ids);
|
|
503
|
+
const agg = {
|
|
504
|
+
deleted: 0,
|
|
505
|
+
failed: 0,
|
|
506
|
+
errors: [],
|
|
507
|
+
requestCount: 0,
|
|
508
|
+
rateLimitedCount: 0,
|
|
509
|
+
aborted: false,
|
|
510
|
+
notAttempted: 0,
|
|
511
|
+
};
|
|
512
|
+
const pagePause = deps.pagePauseMs ?? exports.BULK_DELETE_PAGE_PAUSE_MS;
|
|
513
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
514
|
+
if (i > 0 && pagePause > 0)
|
|
515
|
+
await deps.sleep(pagePause);
|
|
516
|
+
try {
|
|
517
|
+
const res = await bulkDeleteWithRetry(chunks[i], {
|
|
518
|
+
deleteFn: deps.deleteFn,
|
|
519
|
+
sleep: deps.sleep,
|
|
520
|
+
maxRetries: deps.maxRetries,
|
|
521
|
+
defaultRetryMs: deps.defaultRetryMs,
|
|
522
|
+
onRateLimited: () => {
|
|
523
|
+
agg.rateLimitedCount++;
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
agg.requestCount++;
|
|
527
|
+
agg.deleted += res.deleted;
|
|
528
|
+
agg.failed += res.failed;
|
|
529
|
+
agg.errors.push(...res.errors);
|
|
530
|
+
deps.onPage?.(i, chunks.length, res);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
// The whole page failed to delete (retries exhausted or non-429). Record
|
|
534
|
+
// every id in it and stop — continuing after a rate-limit exhaustion just
|
|
535
|
+
// piles more requests onto an already-throttled limiter.
|
|
536
|
+
const msg = errorMessage(error);
|
|
537
|
+
for (const id of chunks[i]) {
|
|
538
|
+
agg.errors.push({ id, error: msg });
|
|
539
|
+
}
|
|
540
|
+
agg.failed += chunks[i].length;
|
|
541
|
+
agg.aborted = true;
|
|
542
|
+
for (let j = i + 1; j < chunks.length; j++) {
|
|
543
|
+
agg.notAttempted += chunks[j].length;
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return agg;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Page through the caller's memories (optionally filtered) and collect every
|
|
552
|
+
* id. Used by `--all`. Stops when a page returns fewer than the page size.
|
|
553
|
+
*/
|
|
554
|
+
async function collectAllMemoryIds(listFn, filterParams = {}, pageSize = exports.BULK_DELETE_LIST_PAGE_SIZE) {
|
|
555
|
+
const ids = [];
|
|
556
|
+
let offset = 0;
|
|
557
|
+
for (;;) {
|
|
558
|
+
const response = await listFn({ ...filterParams, limit: pageSize, offset });
|
|
559
|
+
const batch = response?.data?.data ?? [];
|
|
560
|
+
if (batch.length === 0)
|
|
561
|
+
break;
|
|
562
|
+
for (const m of batch) {
|
|
563
|
+
if (m?.id)
|
|
564
|
+
ids.push(m.id);
|
|
565
|
+
}
|
|
566
|
+
if (batch.length < pageSize)
|
|
567
|
+
break;
|
|
568
|
+
offset += pageSize;
|
|
569
|
+
}
|
|
570
|
+
return ids;
|
|
571
|
+
}
|
|
572
|
+
/** Issue one bulk-delete request via the SDK and normalize the response. */
|
|
573
|
+
async function bulkDeleteViaSdk(ids) {
|
|
574
|
+
// SDK exposes bulkDeleteMemories (v1.55.1) — no direct-fetch fallback needed
|
|
575
|
+
// (contrast v1.7.27, which hand-rolled the batch-GET call before the SDK had
|
|
576
|
+
// it). The global fetch wrapper in index.ts still attaches X-MX-Client, so
|
|
577
|
+
// rate-limit attribution stays correct.
|
|
578
|
+
const response = await (0, api_client_1.getSdk)().memories.bulkDeleteMemories({
|
|
579
|
+
ids,
|
|
580
|
+
confirmDeletion: true,
|
|
581
|
+
});
|
|
582
|
+
const data = response?.data ?? response;
|
|
583
|
+
return {
|
|
584
|
+
deleted: data?.deleted ?? 0,
|
|
585
|
+
failed: data?.failed ?? 0,
|
|
586
|
+
errors: Array.isArray(data?.errors) ? data.errors : [],
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Shared batch-delete flow for --batch / --stdin / --all. Handles the
|
|
591
|
+
* confirmation gate, short-id resolution, paged deletion, and output.
|
|
592
|
+
*/
|
|
593
|
+
async function handleBatchDelete(rawIds, options, mode) {
|
|
594
|
+
const jsonMode = options.format === 'json';
|
|
595
|
+
const isNonInteractive = !process.stdin.isTTY || process.env.MX_PROFILE === 'ai';
|
|
596
|
+
// Status/progress goes to stderr so stdout stays pure under --format json.
|
|
597
|
+
// (There is no session-preamble printer on this command path, so the
|
|
598
|
+
// #4176-class stdout-pollution bug cannot occur here — the only stdout write
|
|
599
|
+
// in json mode is the final JSON object below.)
|
|
600
|
+
const status = (msg) => console.error(msg);
|
|
601
|
+
// De-duplicate while preserving order, drop blanks.
|
|
602
|
+
const uniqueIds = [...new Set(rawIds)].filter((id) => id.trim().length > 0);
|
|
603
|
+
if (uniqueIds.length === 0) {
|
|
604
|
+
console.error(chalk_1.default.red('Error: No memory IDs provided for batch delete'));
|
|
605
|
+
process.exitCode = 1;
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// Resolve short-id prefixes exactly like single delete. Warnings go to stderr
|
|
609
|
+
// to preserve JSON purity on stdout.
|
|
610
|
+
const { resolvedIds } = await resolveShortIds(uniqueIds, {
|
|
611
|
+
warn: (m) => console.error(chalk_1.default.yellow(m)),
|
|
612
|
+
});
|
|
613
|
+
if (resolvedIds.length === 0) {
|
|
614
|
+
console.error(chalk_1.default.red('Error: No valid memory IDs to delete after resolution'));
|
|
615
|
+
process.exitCode = 1;
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const count = resolvedIds.length;
|
|
619
|
+
// --all always announces the count it is about to delete.
|
|
620
|
+
if (mode === 'all') {
|
|
621
|
+
status(chalk_1.default.yellow(`About to delete ${count} mem` + (count === 1 ? 'ory' : 'ories') + '.'));
|
|
622
|
+
}
|
|
623
|
+
// Confirmation gate. Batch modes REQUIRE explicit --yes non-interactively;
|
|
624
|
+
// interactively, --yes bypasses the prompt, otherwise we confirm with a count.
|
|
625
|
+
if (isNonInteractive) {
|
|
626
|
+
if (!options.yes) {
|
|
627
|
+
console.error(chalk_1.default.red(`Error: Batch delete of ${count} memories requires explicit --yes in non-interactive mode.`));
|
|
628
|
+
console.error(chalk_1.default.yellow('Re-run with --yes to confirm.'));
|
|
629
|
+
process.exitCode = 1;
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else if (!options.yes) {
|
|
634
|
+
const answers = await inquirer_1.default.prompt([
|
|
635
|
+
{
|
|
636
|
+
type: 'confirm',
|
|
637
|
+
name: 'confirm',
|
|
638
|
+
message: `Delete ${count} memor${count === 1 ? 'y' : 'ies'}? This cannot be undone.`,
|
|
639
|
+
default: false,
|
|
640
|
+
},
|
|
641
|
+
]);
|
|
642
|
+
if (!answers.confirm) {
|
|
643
|
+
console.error(chalk_1.default.yellow('Delete cancelled'));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// Gate passed — now (and only now) do we issue requests with confirmDeletion.
|
|
648
|
+
const spinner = (0, spinner_1.createSpinner)(`Deleting ${count} memories...`, options.format).start();
|
|
649
|
+
try {
|
|
650
|
+
const result = await runBulkDeletePaged(resolvedIds, {
|
|
651
|
+
deleteFn: bulkDeleteViaSdk,
|
|
652
|
+
sleep,
|
|
653
|
+
onPage: (i, total) => {
|
|
654
|
+
if (total > 1 && !jsonMode) {
|
|
655
|
+
spinner.text = `Deleting page ${i + 1} of ${total}...`;
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
// Non-zero exit whenever anything failed OR paging aborted early, so
|
|
660
|
+
// scripts can detect partial failure (QA MAJOR #1). Set before any output
|
|
661
|
+
// branch so both JSON and human paths honor it.
|
|
662
|
+
const hadFailure = result.failed > 0 || result.aborted;
|
|
663
|
+
if (hadFailure) {
|
|
664
|
+
process.exitCode = 1;
|
|
665
|
+
}
|
|
666
|
+
if (result.aborted) {
|
|
667
|
+
spinner.fail(`Deleted ${result.deleted}, ${result.failed} failed, ${result.notAttempted} not attempted (stopped early)`);
|
|
668
|
+
}
|
|
669
|
+
else if (result.failed > 0) {
|
|
670
|
+
spinner.warn(`Deleted ${result.deleted}, ${result.failed} failed`);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
spinner.succeed(`Deleted ${result.deleted} memor${result.deleted === 1 ? 'y' : 'ies'}`);
|
|
674
|
+
}
|
|
675
|
+
if (jsonMode) {
|
|
676
|
+
// Pure JSON on stdout — the ONLY stdout write in this mode. Emitted even
|
|
677
|
+
// on partial failure / early abort so json-mode scripts always get a
|
|
678
|
+
// structured result (QA MAJOR #2).
|
|
679
|
+
process.stdout.write(JSON.stringify({
|
|
680
|
+
deleted: result.deleted,
|
|
681
|
+
failed: result.failed,
|
|
682
|
+
errors: result.errors,
|
|
683
|
+
requestCount: result.requestCount,
|
|
684
|
+
aborted: result.aborted,
|
|
685
|
+
notAttempted: result.notAttempted,
|
|
686
|
+
}) + '\n');
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (result.errors.length > 0) {
|
|
690
|
+
console.log(chalk_1.default.yellow(`\n${result.errors.length} failure(s):`));
|
|
691
|
+
for (const e of result.errors) {
|
|
692
|
+
console.log(` ${e.id}: ${e.error}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (result.aborted && result.notAttempted > 0) {
|
|
696
|
+
console.log(chalk_1.default.yellow(`\nStopped early — ${result.notAttempted} id(s) were not attempted. Re-run to retry them.`));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
spinner.fail('Batch delete failed');
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
352
704
|
/**
|
|
353
705
|
* Register memory management commands
|
|
354
706
|
* @param program - Commander program instance
|
|
@@ -1083,12 +1435,136 @@ function registerMemoriesCommands(program) {
|
|
|
1083
1435
|
* Delete a memory
|
|
1084
1436
|
*/
|
|
1085
1437
|
memories
|
|
1086
|
-
.command('delete
|
|
1087
|
-
.description('Delete
|
|
1088
|
-
.option('--force', 'Skip confirmation prompt')
|
|
1438
|
+
.command('delete [id]')
|
|
1439
|
+
.description('Delete one or more memories (single, or --batch/--stdin/--all)')
|
|
1440
|
+
.option('--force', 'Skip confirmation prompt (single delete only)')
|
|
1441
|
+
.option('--batch <ids...>', 'Batch delete multiple memories by ID (space- or comma-separated)')
|
|
1442
|
+
.option('--stdin', 'Batch delete memory IDs read from stdin (one per line or space-separated)')
|
|
1443
|
+
.option('--all', 'Batch delete ALL of your memories (supports --recent/--topics/--before/--after filters)')
|
|
1444
|
+
.option('--yes', 'Confirm batch deletion (required for --batch/--stdin/--all in non-interactive mode)')
|
|
1445
|
+
.option('--i-know-this-deletes-everything', 'Required (with --yes) for unfiltered --all in non-interactive mode')
|
|
1446
|
+
.option('--recent <duration>', 'Filter for --all: memories from last duration (e.g. 7d, 2w, 1h)')
|
|
1447
|
+
.option('--topics <topics>', 'Filter for --all: comma-separated topics (matches ANY)')
|
|
1448
|
+
.option('--before <date>', 'Filter for --all: created before this date (ISO 8601)')
|
|
1449
|
+
.option('--after <date>', 'Filter for --all: created after this date (ISO 8601)')
|
|
1450
|
+
.option('--format <format>', 'Output format (json for pure-JSON scripting output)')
|
|
1089
1451
|
.action(async (id, options) => {
|
|
1090
1452
|
try {
|
|
1091
1453
|
const isNonInteractive = !process.stdin.isTTY || process.env.MX_PROFILE === 'ai';
|
|
1454
|
+
// ── Batch modes: --batch / --stdin / --all (mutually exclusive) ─────
|
|
1455
|
+
const batchProvided = options.batch !== undefined;
|
|
1456
|
+
const stdinProvided = !!options.stdin;
|
|
1457
|
+
const allProvided = !!options.all;
|
|
1458
|
+
const batchModeCount = [batchProvided, stdinProvided, allProvided].filter(Boolean).length;
|
|
1459
|
+
if (batchModeCount > 1) {
|
|
1460
|
+
console.error(chalk_1.default.red('Error: --batch, --stdin, and --all are mutually exclusive; use only one.'));
|
|
1461
|
+
process.exitCode = 1;
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (batchModeCount === 1) {
|
|
1465
|
+
if (id) {
|
|
1466
|
+
console.error(chalk_1.default.red('Error: Do not pass a positional <id> together with --batch/--stdin/--all.'));
|
|
1467
|
+
process.exitCode = 1;
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (allProvided) {
|
|
1471
|
+
// List-then-delete. Compose the existing list filters when supplied;
|
|
1472
|
+
// otherwise --all deletes everything.
|
|
1473
|
+
const listParams = {};
|
|
1474
|
+
if (options.recent) {
|
|
1475
|
+
const recentTs = (0, date_utils_1.getRecentTimestamp)(options.recent);
|
|
1476
|
+
if (!recentTs) {
|
|
1477
|
+
console.error(chalk_1.default.red(`Error: Invalid duration format: ${options.recent}`));
|
|
1478
|
+
console.error(chalk_1.default.yellow('Supported formats: 30m, 1h, 7d, 2w'));
|
|
1479
|
+
process.exitCode = 1;
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
listParams.from = recentTs;
|
|
1483
|
+
}
|
|
1484
|
+
else if (options.after) {
|
|
1485
|
+
// Validate BEFORE treating as a filter (SEC delta): a typo'd date
|
|
1486
|
+
// (e.g. --after notadate) must NOT silently count toward
|
|
1487
|
+
// hasFilters and skip the --i-know-this-deletes-everything gate —
|
|
1488
|
+
// if the server then ignored the unparseable bound, --all would
|
|
1489
|
+
// wipe everything past the extra-token guard on an operator typo.
|
|
1490
|
+
// Mirror --recent: error and exit on parse failure.
|
|
1491
|
+
if (!isValidIsoDate(options.after)) {
|
|
1492
|
+
console.error(chalk_1.default.red(`Error: --after is not a valid date: ${options.after}`));
|
|
1493
|
+
console.error(chalk_1.default.yellow('Provide an ISO 8601 date (e.g. 2026-07-01 or 2026-07-01T00:00:00Z).'));
|
|
1494
|
+
process.exitCode = 1;
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
listParams.from = options.after;
|
|
1498
|
+
}
|
|
1499
|
+
if (options.before) {
|
|
1500
|
+
if (!isValidIsoDate(options.before)) {
|
|
1501
|
+
console.error(chalk_1.default.red(`Error: --before is not a valid date: ${options.before}`));
|
|
1502
|
+
console.error(chalk_1.default.yellow('Provide an ISO 8601 date (e.g. 2026-07-01 or 2026-07-01T00:00:00Z).'));
|
|
1503
|
+
process.exitCode = 1;
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
listParams.to = options.before;
|
|
1507
|
+
}
|
|
1508
|
+
if (options.topics) {
|
|
1509
|
+
listParams.topics = options.topics
|
|
1510
|
+
.split(',')
|
|
1511
|
+
.map((t) => t.trim())
|
|
1512
|
+
.filter((t) => t.length > 0)
|
|
1513
|
+
.join(',');
|
|
1514
|
+
}
|
|
1515
|
+
const hasFilters = Object.keys(listParams).length > 0;
|
|
1516
|
+
if (!hasFilters) {
|
|
1517
|
+
// Emit the "delete EVERYTHING" warning to stderr REGARDLESS of
|
|
1518
|
+
// format — the automation (--format json) path is exactly where it
|
|
1519
|
+
// matters most, and stderr keeps stdout pure (SEC SHOULD-FIX).
|
|
1520
|
+
console.error(chalk_1.default.yellow('--all: no filters supplied — this will delete EVERY memory you own.'));
|
|
1521
|
+
// Unfiltered --all in non-interactive mode requires a DISTINCT
|
|
1522
|
+
// explicit token in addition to --yes (mirrors the --force
|
|
1523
|
+
// non-interactive block). No bypass.
|
|
1524
|
+
const nonInteractive = !process.stdin.isTTY || process.env.MX_PROFILE === 'ai';
|
|
1525
|
+
if (nonInteractive && !options.iKnowThisDeletesEverything) {
|
|
1526
|
+
console.error(chalk_1.default.red('Error: Unfiltered --all in non-interactive mode requires --i-know-this-deletes-everything (in addition to --yes).'));
|
|
1527
|
+
console.error(chalk_1.default.yellow('Add a filter (--recent/--topics/--before/--after) or pass --i-know-this-deletes-everything to proceed.'));
|
|
1528
|
+
process.exitCode = 1;
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const listSpinner = (0, spinner_1.createSpinner)('Collecting memories to delete...', options.format).start();
|
|
1533
|
+
const allIds = await collectAllMemoryIds((params) => (0, api_client_1.getSdk)().memories.listMemories(params), listParams);
|
|
1534
|
+
listSpinner.stop();
|
|
1535
|
+
if (allIds.length === 0) {
|
|
1536
|
+
console.error(chalk_1.default.yellow('No memories matched — nothing to delete.'));
|
|
1537
|
+
if (options.format === 'json') {
|
|
1538
|
+
process.stdout.write(JSON.stringify({
|
|
1539
|
+
deleted: 0,
|
|
1540
|
+
failed: 0,
|
|
1541
|
+
errors: [],
|
|
1542
|
+
requestCount: 0,
|
|
1543
|
+
aborted: false,
|
|
1544
|
+
notAttempted: 0,
|
|
1545
|
+
}) + '\n');
|
|
1546
|
+
}
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
await handleBatchDelete(allIds, options, 'all');
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
let batchIds = [];
|
|
1553
|
+
if (batchProvided) {
|
|
1554
|
+
batchIds = parseBatchIds(options.batch);
|
|
1555
|
+
}
|
|
1556
|
+
else if (stdinProvided) {
|
|
1557
|
+
batchIds = await readIdsFromStdin();
|
|
1558
|
+
}
|
|
1559
|
+
await handleBatchDelete(batchIds, options, batchProvided ? 'batch' : 'stdin');
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
// ── Single delete (unchanged behavior) ──────────────────────────────
|
|
1563
|
+
if (!id) {
|
|
1564
|
+
console.error(chalk_1.default.red('Error: Provide a memory ID, or use --batch/--stdin/--all.'));
|
|
1565
|
+
process.exitCode = 1;
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1092
1568
|
// Block --force in non-interactive/agent mode to prevent automated bulk deletion
|
|
1093
1569
|
if (options.force && isNonInteractive) {
|
|
1094
1570
|
console.error(chalk_1.default.red('Error: The --force flag is blocked in non-interactive/agent mode for safety.'));
|