@memnexus-ai/cli 1.7.213 → 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.
@@ -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
- console.log(chalk_1.default.yellow(`Warning: Skipping ID prefix "${id}" (too short, min ${MIN_PREFIX_LENGTH} chars)`));
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
- console.log(chalk_1.default.yellow(`Warning: No memory found with prefix "${id}"`));
192
+ warn(`Warning: No memory found with prefix "${id}"`);
179
193
  }
180
194
  else if (matches.length > 1) {
181
- console.log(chalk_1.default.yellow(`Warning: Ambiguous prefix "${id}" matches ${matches.length} memories, skipping`));
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 <id>')
1087
- .description('Delete a memory')
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.'));