eklavya 1.57.0 → 1.58.0

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
@@ -106,6 +106,7 @@ npx eklavya config get
106
106
  npx eklavya dashboard
107
107
  npx eklavya feedback generate
108
108
  npx eklavya db-path
109
+ npx eklavya --version
109
110
  ```
110
111
 
111
112
  The [CLI reference](https://eklavya-run.web.app/docs/cli/) lists all commands and
@@ -16,7 +16,7 @@ import { GLOBAL_PROJECT, projectKey } from './store.js';
16
16
  import { missingProjects, moveProject } from './relocate.js';
17
17
  import { claudeHome } from './install.js';
18
18
  import { guessProjectMap } from './claude-mem.js';
19
- import { spin } from './theme.js';
19
+ import { bold, dim, failText, paint, spin, styleUsage } from './theme.js';
20
20
  import { importOffThread } from './memory/import-worker.js';
21
21
  import { isInternalObserver, releaseWorker, renewWorker, reserveWorker, stopWorker, workerStatus, } from './memory/reservation.js';
22
22
  import { identityFor } from './memory/identity.js';
@@ -28,9 +28,11 @@ import { replayProject, transcriptDirFor, transcriptsFor } from './memory/replay
28
28
  import { droppedCount } from './memory/spool.js';
29
29
  import { savingsFrom, savingsLine } from './memory/tokens.js';
30
30
  import { inventory, exportPayload, restoreExport, EXPORT_SCHEMA_VERSION, ImportError, IMPORTED_TABLES, verifyImport, } from './memory/import.js';
31
+ /** A `label: value` row with its label dim; a line without that shape is returned as it is. */
32
+ const kv = (line) => line.replace(/^(\s*[a-z][a-z ]*:)(\s+)(?=\S)/, (_m, k, gap) => `${dim(k)}${gap}`);
31
33
  /** The same as `cli.ts`'s: importing that one would run its `main()`. */
32
34
  function fail(message) {
33
- process.stderr.write(`${message}\n`);
35
+ process.stderr.write(`${failText(message)}\n`);
34
36
  process.exit(1);
35
37
  }
36
38
  /**
@@ -156,7 +158,7 @@ function memoryStatus() {
156
158
  `pending: ${pendingEventCount(db, project)} evidence events here, ${pendingEventCount(db)} in total`,
157
159
  `queue: ${queue.pending} pending · ${queue.paused} paused · ${queue.failed} failed${queue.quarantined ? ` · ${queue.quarantined} quarantined` : ''}`,
158
160
  `oldest job: ${queue.oldest ?? '—'}`,
159
- ...(pauseLine(db) ? [`paused: ${pauseLine(db)} — fix it, then: eklavya memory process`] : []),
161
+ ...(pauseLine(db) ? [`paused: ${paint.warn(`${pauseLine(db)} — fix it, then: eklavya memory process`)}`] : []),
160
162
  `worker: ${workerLine(db)}`,
161
163
  // Named separately from the summarizer because they answer different
162
164
  // questions: one is "will anything leave this machine", the other is
@@ -170,7 +172,7 @@ function memoryStatus() {
170
172
  `reads: ${readLine(readTotals(db))}`,
171
173
  savingsLine(savings),
172
174
  ];
173
- process.stdout.write(`${lines.join('\n')}\n`);
175
+ process.stdout.write(`${lines.map(kv).join('\n')}\n`);
174
176
  }
175
177
  finally {
176
178
  db.close();
@@ -194,12 +196,13 @@ function memorySearch(argv) {
194
196
  limit: numberFlag(argv, '--limit', 10),
195
197
  });
196
198
  if (!hits.length) {
197
- process.stdout.write('No matches.\n');
199
+ process.stdout.write(`${dim('No matches.')}\n`);
198
200
  return;
199
201
  }
200
202
  for (const hit of hits) {
201
- process.stdout.write(`#${hit.entry.id} ${hit.entry.occurred_at.slice(0, 16).replace('T', ' ')} ${hit.entry.title}\n` +
202
- ` ${hit.entry.type ?? hit.entry.kind} · score ${hit.score.toFixed(3)} · ${hit.via}${hit.entry.import_source ? ` · imported from ${hit.entry.import_source}` : ''}\n`);
203
+ process.stdout.write(`${paint.aged(`#${hit.entry.id}`)} ${hit.entry.occurred_at.slice(0, 16).replace('T', ' ')} ${bold(hit.entry.title)}\n` +
204
+ dim(` ${hit.entry.type ?? hit.entry.kind} · score ${hit.score.toFixed(3)} · ${hit.via}${hit.entry.import_source ? ` · imported from ${hit.entry.import_source}` : ''}`) +
205
+ '\n');
203
206
  }
204
207
  }
205
208
  finally {
@@ -215,11 +218,11 @@ function memoryTimeline(argv) {
215
218
  since: flag(argv, '--since') ?? null,
216
219
  });
217
220
  if (!rows.length) {
218
- process.stdout.write('Nothing recorded for this project yet.\n');
221
+ process.stdout.write(`${dim('Nothing recorded for this project yet.')}\n`);
219
222
  return;
220
223
  }
221
224
  for (const row of rows) {
222
- process.stdout.write(`#${row.id} ${row.occurred_at.slice(0, 16).replace('T', ' ')} ${row.kind} ${row.title}\n`);
225
+ process.stdout.write(`${paint.aged(`#${row.id}`)} ${row.occurred_at.slice(0, 16).replace('T', ' ')} ${dim(row.kind)} ${row.title}\n`);
223
226
  }
224
227
  }
225
228
  finally {
@@ -237,28 +240,30 @@ function memoryShow(argv) {
237
240
  fail(`No memory entry #${id}.`);
238
241
  const tags = entryTags(db, id);
239
242
  const lines = [
240
- `#${entry.id} ${entry.title}`,
241
- `kind: ${entry.kind}${entry.type ? ` / ${entry.type}` : ''}`,
242
- `project: ${entry.project}`,
243
- `occurred: ${entry.occurred_at}`,
244
- `generator: ${entry.generator}`,
245
- ...(entry.import_source ? [`imported: from ${entry.import_source} (unassessed — no mastery, no attempts)`] : []),
246
- ...(entry.superseded_by ? [`superseded by #${entry.superseded_by}`] : []),
247
- ...(tags.length ? [`tags: ${tags.join(', ')}`] : []),
248
- ...(entry.files ? [`files: ${JSON.parse(entry.files).join(', ')}`] : []),
243
+ `${paint.aged(`#${entry.id}`)} ${bold(entry.title)}`,
244
+ ...[
245
+ `kind: ${entry.kind}${entry.type ? ` / ${entry.type}` : ''}`,
246
+ `project: ${entry.project}`,
247
+ `occurred: ${entry.occurred_at}`,
248
+ `generator: ${entry.generator}`,
249
+ ...(entry.import_source ? [`imported: from ${entry.import_source} (unassessed — no mastery, no attempts)`] : []),
250
+ ...(entry.superseded_by ? [`superseded by #${entry.superseded_by}`] : []),
251
+ ...(tags.length ? [`tags: ${tags.join(', ')}`] : []),
252
+ ...(entry.files ? [`files: ${JSON.parse(entry.files).join(', ')}`] : []),
253
+ ].map(kv),
249
254
  '',
250
255
  entry.narrative || '(no narrative)',
251
256
  ];
252
257
  const facts = entry.facts ? JSON.parse(entry.facts) : [];
253
258
  if (facts.length)
254
- lines.push('', 'Facts:', ...facts.map((f) => ` - ${f}`));
259
+ lines.push('', bold('Facts:'), ...facts.map((f) => ` - ${f}`));
255
260
  const events = entryEvents(db, id);
256
- lines.push('', `Evidence (${events.length}):`);
261
+ lines.push('', bold(`Evidence (${events.length}):`));
257
262
  for (const event of events) {
258
263
  lines.push(` ${event.occurred_at.slice(0, 16).replace('T', ' ')} ${event.kind}${event.tool ? `/${event.tool}` : ''} ${event.body.slice(0, 120).replace(/\s+/g, ' ')}`);
259
264
  }
260
265
  if (!events.length)
261
- lines.push(' (none linked — imported or hand-written entries carry no local evidence)');
266
+ lines.push(dim(' (none linked — imported or hand-written entries carry no local evidence)'));
262
267
  process.stdout.write(`${lines.join('\n')}\n`);
263
268
  }
264
269
  finally {
@@ -304,7 +309,7 @@ function memoryProcess(argv) {
304
309
  if (!token) {
305
310
  const holder = workerStatus(db);
306
311
  if (!background) {
307
- process.stdout.write(`another memory worker is running${holder?.pid ? ` (pid ${holder.pid})` : ''} — its queue is this queue, so nothing to do.\n`);
312
+ process.stdout.write(dim(`another memory worker is running${holder?.pid ? ` (pid ${holder.pid})` : ''} — its queue is this queue, so nothing to do.`) + '\n');
308
313
  }
309
314
  db.close();
310
315
  return;
@@ -337,7 +342,7 @@ function runWorker(db, token, config, maxJobs, background, resumed) {
337
342
  loadConfig: () => loadConfig().config,
338
343
  }).then((result) => {
339
344
  if (!background) {
340
- process.stdout.write(`${resumed ? `resumed ${resumed} paused · ` : ''}processed ${result.processed} · entries ${result.entries} · failed ${result.failed} · skipped ${result.skipped}${result.handedOff ? ' · more queued, continuing in the background' : ''}\n`);
345
+ process.stdout.write(paint.ok(`${resumed ? `resumed ${resumed} paused · ` : ''}processed ${result.processed} · entries ${result.entries} · failed ${result.failed} · skipped ${result.skipped}`) + `${result.handedOff ? dim(' · more queued, continuing in the background') : ''}\n`);
341
346
  }
342
347
  db.close();
343
348
  },
@@ -370,16 +375,18 @@ function memoryBacklog(argv) {
370
375
  if (action === 'list') {
371
376
  const groups = backlogSummary(db, sel);
372
377
  if (!groups.length) {
373
- process.stdout.write('No unfinished jobs or helper sessions.\n');
378
+ process.stdout.write(`${dim('No unfinished jobs or helper sessions.')}\n`);
374
379
  return;
375
380
  }
376
381
  for (const g of groups) {
377
- process.stdout.write(`${String(g.batches).padStart(6)} ${g.status.padEnd(11)} ${g.project}${g.helper ? ' [observer helper sessions]' : ''}\n` +
378
- ` ${g.events} events · ${g.oldest.slice(0, 16).replace('T', ' ')} → ${g.newest.slice(0, 16).replace('T', ' ')}\n`);
382
+ process.stdout.write(`${String(g.batches).padStart(6)} ${g.status.padEnd(11)} ${g.project}${g.helper ? paint.warn(' [observer helper sessions]') : ''}\n` +
383
+ dim(` ${g.events} events · ${g.oldest.slice(0, 16).replace('T', ' ')} → ${g.newest.slice(0, 16).replace('T', ' ')}`) +
384
+ '\n');
379
385
  }
380
386
  if (groups.some((g) => g.helper)) {
381
- process.stdout.write('\nHelper sessions are the observer summarising its own runs — noise. Delete them, and the memories\n' +
382
- 'they produced, or set the unfinished ones aside:\n' +
387
+ process.stdout.write(dim('\nHelper sessions are the observer summarising its own runs — noise. Delete them, and the memories\n' +
388
+ 'they produced, or set the unfinished ones aside:') +
389
+ '\n' +
383
390
  ' eklavya memory backlog discard --helpers\n eklavya memory backlog quarantine --helpers\n');
384
391
  }
385
392
  return;
@@ -387,14 +394,14 @@ function memoryBacklog(argv) {
387
394
  if (!selected)
388
395
  fail(`eklavya memory backlog ${action} needs --helpers, --project <key>, --session <id> or --batch <id>.`);
389
396
  if (action === 'quarantine') {
390
- process.stdout.write(`quarantined ${quarantineBacklog(db, sel)} job(s) — kept, and never processed until restored.\n`);
397
+ process.stdout.write(`${paint.ok(`quarantined ${quarantineBacklog(db, sel)} job(s)`)} — kept, and never processed until restored.\n`);
391
398
  }
392
399
  else if (action === 'restore') {
393
- process.stdout.write(`restored ${restoreBacklog(db, sel)} job(s) to the queue.\n`);
400
+ process.stdout.write(`${paint.ok(`restored ${restoreBacklog(db, sel)} job(s)`)} to the queue.\n`);
394
401
  }
395
402
  else if (action === 'discard') {
396
403
  const gone = discardBacklog(db, sel);
397
- process.stdout.write(`discarded ${gone.batches} batch(es), ${gone.events} evidence event(s) and ${gone.entries} memory entr${gone.entries === 1 ? 'y' : 'ies'}.\n`);
404
+ process.stdout.write(`${paint.ok(`discarded ${gone.batches} batch(es), ${gone.events} evidence event(s) and ${gone.entries} memory entr${gone.entries === 1 ? 'y' : 'ies'}`)}.\n`);
398
405
  }
399
406
  else {
400
407
  fail('Usage: eklavya memory backlog [list|quarantine|discard|restore] [--helpers] [--project <key>] [--session <id>] [--batch <id>]');
@@ -416,7 +423,7 @@ function memoryStop() {
416
423
  const { config } = loadConfig();
417
424
  process.stdout.write(stopMessage(outcome));
418
425
  if (config.providers.observer) {
419
- process.stdout.write('the next session seam starts a new one. To keep it stopped: eklavya config set providers.observer null\n');
426
+ process.stdout.write(`${dim('the next session seam starts a new one. To keep it stopped: eklavya config set providers.observer null')}\n`);
420
427
  }
421
428
  db.close();
422
429
  });
@@ -424,9 +431,9 @@ function memoryStop() {
424
431
  /** What `memory stop` says it did. Exported for the outcomes a test cannot stage in time. */
425
432
  export function stopMessage(outcome) {
426
433
  if (!outcome.stopped)
427
- return 'no memory worker is running.\n';
428
- return (`stopped the memory worker${outcome.pid ? ` (pid ${outcome.pid})` : ''}${outcome.child ? ` and its claude call (pid ${outcome.child})` : ''}${outcome.forced ? ' — it had to be killed' : ''}. Unfinished jobs stay queued.\n` +
429
- (outcome.released ? '' : 'something it started would not exit; the slot stays held until it does.\n'));
434
+ return `${dim('no memory worker is running.')}\n`;
435
+ return (`${paint.ok(`stopped the memory worker${outcome.pid ? ` (pid ${outcome.pid})` : ''}${outcome.child ? ` and its claude call (pid ${outcome.child})` : ''}`)}${outcome.forced ? paint.warn(' — it had to be killed') : ''}. Unfinished jobs stay queued.\n` +
436
+ (outcome.released ? '' : `${paint.warn('something it started would not exit; the slot stays held until it does.')}\n`));
430
437
  }
431
438
  /**
432
439
  * Backfills from Claude Code's own transcripts.
@@ -441,13 +448,13 @@ function memoryReplay(argv) {
441
448
  try {
442
449
  const { config } = loadConfig();
443
450
  if (!config.memory.enabled) {
444
- process.stdout.write('memory.enabled is false, so there is nowhere to replay into.\n');
451
+ process.stdout.write(`${paint.warn('memory.enabled is false, so there is nowhere to replay into.')}\n`);
445
452
  return;
446
453
  }
447
454
  const cwd = process.cwd();
448
455
  const files = transcriptsFor(cwd);
449
456
  if (!files.length) {
450
- process.stdout.write(`No Claude Code transcripts found for this checkout.\nLooked in: ${transcriptDirFor(cwd)}\n`);
457
+ process.stdout.write(`${dim('No Claude Code transcripts found for this checkout.')}\n${dim('Looked in:')} ${paint.aged(transcriptDirFor(cwd))}\n`);
451
458
  return;
452
459
  }
453
460
  const limit = Number(flag(argv, '--limit', '20'));
@@ -459,13 +466,15 @@ function memoryReplay(argv) {
459
466
  excluded: sum.excluded + r.excluded,
460
467
  }), { read: 0, captured: 0, duplicates: 0, excluded: 0 });
461
468
  process.stdout.write([
462
- `transcripts: ${results.length} of ${files.length}`,
463
- `lines read: ${total.read}`,
464
- `captured: ${total.captured}`,
465
- `already had: ${total.duplicates}`,
466
- `excluded: ${total.excluded} (privacy filter, or capture set to minimal)`,
469
+ ...[
470
+ `transcripts: ${results.length} of ${files.length}`,
471
+ `lines read: ${total.read}`,
472
+ `captured: ${total.captured}`,
473
+ `already had: ${total.duplicates}`,
474
+ `excluded: ${total.excluded} (privacy filter, or capture set to minimal)`,
475
+ ].map(kv),
467
476
  '',
468
- 'Run `eklavya memory process` to summarise what was captured.',
477
+ dim('Run `eklavya memory process` to summarise what was captured.'),
469
478
  '',
470
479
  ].join('\n'));
471
480
  }
@@ -478,14 +487,14 @@ function memoryPrune() {
478
487
  try {
479
488
  const { config } = loadConfig();
480
489
  if (!config.memory.retention_days) {
481
- process.stdout.write('memory.retention_days is not set for this project, so its raw evidence is kept until deleted by hand.\n');
490
+ process.stdout.write(`${dim('memory.retention_days is not set for this project, so its raw evidence is kept until deleted by hand.')}\n`);
482
491
  return;
483
492
  }
484
493
  // This project only, under this project's resolved retention: another
485
494
  // project may keep its evidence (retention unset) or a different window.
486
495
  const project = currentProject();
487
496
  const removed = pruneEvidence(db, config, { project });
488
- process.stdout.write(`Deleted ${removed} raw evidence events older than ${config.memory.retention_days} days in ${project}.\n`);
497
+ process.stdout.write(`${paint.ok(`Deleted ${removed} raw evidence events`)} older than ${config.memory.retention_days} days in ${project}.\n`);
489
498
  }
490
499
  finally {
491
500
  db.close();
@@ -504,10 +513,10 @@ function memoryMove(argv) {
504
513
  if (!oldArg) {
505
514
  const missing = missingProjects(db);
506
515
  process.stdout.write(missing.length
507
- ? `History filed under folders that no longer exist:\n${missing
516
+ ? `${bold('History filed under folders that no longer exist:')}\n${missing
508
517
  .map((m) => ` ${m.project} (${m.entries} entries)`)
509
518
  .join('\n')}\nRe-file one here: eklavya memory move <old>\n`
510
- : 'No history is filed under a folder that no longer exists.\n');
519
+ : `${dim('No history is filed under a folder that no longer exists.')}\n`);
511
520
  return;
512
521
  }
513
522
  const from = path.resolve(oldArg);
@@ -529,8 +538,8 @@ function memoryMove(argv) {
529
538
  if (!held.n)
530
539
  fail(`No history is filed under ${from}. Run \`eklavya memory move\` to list the folders that have some.`);
531
540
  const report = moveProject(db, from, to);
532
- process.stdout.write(`Moved ${from} → ${to}: ${report.rows} rows re-filed, ${report.entries} memory entries here now.\n` +
533
- report.left.map((f) => `Left in place, because ${to} already has one: ${f}\n`).join(''));
541
+ process.stdout.write(`${paint.ok(`Moved ${from} → ${to}`)}: ${report.rows} rows re-filed, ${report.entries} memory entries here now.\n` +
542
+ report.left.map((f) => `${paint.warn(`Left in place, because ${to} already has one: ${f}`)}\n`).join(''));
534
543
  }
535
544
  finally {
536
545
  db.close();
@@ -543,7 +552,7 @@ function dispositionReport(fields) {
543
552
  const group = fields.filter((f) => f.kind === kind);
544
553
  if (!group.length)
545
554
  continue;
546
- lines.push('', `${kind} (${group.length}):`);
555
+ lines.push('', bold(`${kind} (${group.length}):`));
547
556
  for (const f of group) {
548
557
  lines.push(` ${f.table}.${f.field}${f.to ? ` -> ${f.to}` : ''}${f.reason ? ` — ${f.reason}` : ''}`);
549
558
  }
@@ -591,17 +600,17 @@ function verifyLines(r) {
591
600
  const missing = r.tables.reduce((n, t) => n + t.missing.length, 0);
592
601
  const width = Math.max(0, ...r.projects.map((p) => p.project.length));
593
602
  return [
594
- `verify: ${r.sourcePath}`,
603
+ kv(`verify: ${r.sourcePath}`),
595
604
  ...r.tables.map((t) => ` ${t.table.padEnd(18)} ${String(t.source).padStart(6)} in source · ${String(t.present).padStart(6)} in Eklavya · ${t.missing.length} missing${t.missing.length ? ` (ids ${t.missing.slice(0, 10).join(', ')}${t.missing.length > 10 ? ', …' : ''})` : ''}`),
596
605
  ` changed since import: ${r.changed} entr${r.changed === 1 ? 'y' : 'ies'}`,
597
- 'placement:',
606
+ bold('placement:'),
598
607
  ...r.projects.flatMap((p) => Object.entries(p.filedUnder).map(([to, n]) => {
599
608
  const placed = path.isAbsolute(to);
600
609
  return ` ${String(n).padStart(6)} ${p.project.padEnd(width)} ${placed ? `→ ${to}` : 'not placed — searchable only with --all-projects; --map it to a checkout'}`;
601
610
  })),
602
611
  missing
603
- ? `INCOMPLETE: ${missing} source row(s) are not in Eklavya — re-run without --verify to import them.`
604
- : 'complete: every source row is in Eklavya.',
612
+ ? paint.fail(`INCOMPLETE: ${missing} source row(s) are not in Eklavya — re-run without --verify to import them.`)
613
+ : paint.ok('complete: every source row is in Eklavya.'),
605
614
  ];
606
615
  }
607
616
  async function memoryImport(argv) {
@@ -633,14 +642,16 @@ async function memoryImport(argv) {
633
642
  const projectMap = { ...guessProjectMap(source, claudeHome(), unsure), ...explicit };
634
643
  for (const name of Object.keys(explicit))
635
644
  delete unsure[name];
636
- const unsureLines = Object.entries(unsure).map(([name, paths]) => ` ${name}: ${paths.length} checkouts carry this name — pick one: --map ${name}=<path> (${paths.join(', ')})`);
645
+ const unsureLines = Object.entries(unsure).map(([name, paths]) => paint.warn(` ${name}: ${paths.length} checkouts carry this name — pick one: --map ${name}=<path> (${paths.join(', ')})`));
637
646
  const lines = [
638
- `source: ${found.sourcePath}`,
639
- `schema: ${found.schemaVersion ?? 'unversioned'} (this importer understands up to ${found.supportedMax})`,
640
- `range: ${found.dateRange.from?.slice(0, 10) ?? '—'} … ${found.dateRange.to?.slice(0, 10) ?? '—'}`,
641
- 'tables:',
647
+ ...[
648
+ `source: ${found.sourcePath}`,
649
+ `schema: ${found.schemaVersion ?? 'unversioned'} (this importer understands up to ${found.supportedMax})`,
650
+ `range: ${found.dateRange.from?.slice(0, 10) ?? '—'} … ${found.dateRange.to?.slice(0, 10) ?? '—'}`,
651
+ ].map(kv),
652
+ bold('tables:'),
642
653
  ...found.tables.map((t) => ` ${t.rows.toString().padStart(7)} ${t.name}${t.known ? '' : ' (unrecognised)'}`),
643
- 'projects:',
654
+ bold('projects:'),
644
655
  ...found.projects.map((p) => ` ${p.entries.toString().padStart(7)} ${p.project}`),
645
656
  dispositionReport(found.fields),
646
657
  ];
@@ -656,7 +667,7 @@ async function memoryImport(argv) {
656
667
  ...planned.map(([from, to]) => `would map: ${from} -> ${to}`),
657
668
  ...(unmapped.length ? [`would keep as-is: ${unmapped.join(', ')}`] : []),
658
669
  ...unsureLines,
659
- 'Dry run: nothing was written, and the source was opened read-only.',
670
+ dim('Dry run: nothing was written, and the source was opened read-only.'),
660
671
  '',
661
672
  ].join('\n'));
662
673
  return;
@@ -667,20 +678,20 @@ async function memoryImport(argv) {
667
678
  const rows = IMPORTED_TABLES.map((t) => ` ${t.padEnd(18)} read ${report.read[t]} · imported ${report.imported[t]} · already present ${report.skipped[t]}`);
668
679
  process.stdout.write([
669
680
  '',
670
- `snapshot: ${report.snapshot}`,
681
+ kv(`snapshot: ${report.snapshot}`),
671
682
  ...rows,
672
- ` concept candidates: ${report.candidates} (all unassessed — no mastery, no attempts, no gate touched)`,
673
- ` evidence links: ${report.links} (drill-down from an entry to the prompts and tool uses behind it)`,
683
+ ` concept candidates: ${report.candidates} ${dim('(all unassessed — no mastery, no attempts, no gate touched)')}`,
684
+ ` evidence links: ${report.links} ${dim('(drill-down from an entry to the prompts and tool uses behind it)')}`,
674
685
  ` re-indexed: ${report.reindexed} entries`,
675
686
  ...(report.rehomed ? [` re-homed: ${report.rehomed} entries an earlier run left under a bare project name`] : []),
676
- ` validation: ${report.validation.ok ? 'ok' : `FAILED — ${report.validation.notes.join('; ')}`}`,
687
+ ` validation: ${report.validation.ok ? paint.ok('ok') : paint.fail(`FAILED — ${report.validation.notes.join('; ')}`)}`,
677
688
  ...report.projectsMapped.map((p) => ` mapped: ${p.from} -> ${p.to}`),
678
689
  // The unmapped list is the useful half: those rows only ever surface
679
690
  // under --all-projects until somebody maps them.
680
691
  ...(report.projectsKept.length
681
692
  ? [
682
693
  ` kept as-is: ${report.projectsKept.join(', ')}`,
683
- ' (unmapped projects are searchable only with --all-projects; re-run with --map to file them under a checkout)',
694
+ dim(' (unmapped projects are searchable only with --all-projects; re-run with --map to file them under a checkout)'),
684
695
  ]
685
696
  : []),
686
697
  '',
@@ -724,7 +735,7 @@ function memoryExport(argv) {
724
735
  // `--force` replaced, whose old mode `mode` would otherwise keep.
725
736
  fs.writeFileSync(out, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: force ? 'w' : 'wx' });
726
737
  fs.chmodSync(out, 0o600);
727
- process.stdout.write(`Wrote ${out} — ${payload.entries.length} entries, schema version ${EXPORT_SCHEMA_VERSION}\n`);
738
+ process.stdout.write(`${paint.ok('Wrote')} ${paint.aged(out)} — ${payload.entries.length} entries, schema version ${EXPORT_SCHEMA_VERSION}\n`);
728
739
  }
729
740
  finally {
730
741
  db.close();
@@ -746,13 +757,15 @@ function memoryRestore(argv) {
746
757
  try {
747
758
  const r = restoreExport(db, path.resolve(from));
748
759
  process.stdout.write([
749
- `Restored ${from} (export schema version ${r.schemaVersion}):`,
750
- ` entries: ${r.entries.restored} restored, ${r.entries.skipped} already here`,
751
- ` evidence: ${r.evidence.restored} restored, ${r.evidence.skipped} already here`,
752
- ` links: ${r.tags} tag(s), ${r.links} evidence link(s)`,
753
- ` receipts: ${r.receipts.restored} restored, ${r.receipts.skipped} already here (${r.receiptItems} item(s))`,
754
- ` reindexed: ${r.reindexed} entries — search index and vectors rebuilt`,
755
- 'Learning history was not touched: no attempt, mastery or gate row is written by a restore.',
760
+ `${paint.ok(`Restored ${from}`)} (export schema version ${r.schemaVersion}):`,
761
+ ...[
762
+ ` entries: ${r.entries.restored} restored, ${r.entries.skipped} already here`,
763
+ ` evidence: ${r.evidence.restored} restored, ${r.evidence.skipped} already here`,
764
+ ` links: ${r.tags} tag(s), ${r.links} evidence link(s)`,
765
+ ` receipts: ${r.receipts.restored} restored, ${r.receipts.skipped} already here (${r.receiptItems} item(s))`,
766
+ ` reindexed: ${r.reindexed} entries — search index and vectors rebuilt`,
767
+ ].map(kv),
768
+ dim('Learning history was not touched: no attempt, mastery or gate row is written by a restore.'),
756
769
  '',
757
770
  ].join('\n'));
758
771
  }
@@ -798,7 +811,7 @@ function memorySync(argv) {
798
811
  .join(', ')
799
812
  : 'none seen yet'}`,
800
813
  ];
801
- process.stdout.write(`${lines.join('\n')}\n`);
814
+ process.stdout.write(`${lines.map(kv).join('\n')}\n`);
802
815
  return;
803
816
  }
804
817
  const result = sub === 'push' ? push(db, config, { target }) : pull(db, config, { target });
@@ -809,23 +822,23 @@ function memorySync(argv) {
809
822
  }
810
823
  if (sub === 'push') {
811
824
  const r = result;
812
- process.stdout.write(`Pushed to ${r.target} as ${r.device_id}: ${r.staged} new revision${r.staged === 1 ? '' : 's'}, ${r.written} record${r.written === 1 ? '' : 's'} written, ${r.already} already there.\n`);
825
+ process.stdout.write(`${paint.ok('Pushed')} to ${r.target} as ${r.device_id}: ${r.staged} new revision${r.staged === 1 ? '' : 's'}, ${r.written} record${r.written === 1 ? '' : 's'} written, ${r.already} already there.\n`);
813
826
  return;
814
827
  }
815
828
  const r = result;
816
- process.stdout.write(`Pulled from ${r.target}: ${r.applied} applied (${r.tombstones} deletion${r.tombstones === 1 ? '' : 's'}), ${r.skipped} already known, ${r.conflicts} quarantined.\n`);
829
+ process.stdout.write(`${paint.ok('Pulled')} from ${r.target}: ${r.applied} applied (${r.tombstones} deletion${r.tombstones === 1 ? '' : 's'}), ${r.skipped} already known, ${r.conflicts} quarantined.\n`);
817
830
  if (r.recovered) {
818
831
  process.stdout.write(`Recovered ${r.recovered} earlier record${r.recovered === 1 ? '' : 's'} that arrived late before this version.\n`);
819
832
  }
820
833
  if (r.conflicts) {
821
- process.stdout.write('Quarantined versions are kept whole in sync_conflicts — nothing was overwritten.\n');
834
+ process.stdout.write(`${paint.warn('Quarantined versions are kept whole in sync_conflicts — nothing was overwritten.')}\n`);
822
835
  }
823
836
  if (r.stalled.length) {
824
- process.stdout.write(`Could not read a record from: ${r.stalled.join(', ')} — likely still being written. The next pull reads it.\n`);
837
+ process.stdout.write(`${paint.warn(`Could not read a record from: ${r.stalled.join(', ')} — likely still being written. The next pull reads it.`)}\n`);
825
838
  }
826
839
  if (r.outstanding.length) {
827
840
  const owed = r.outstanding.map((o) => `${o.device_id} (${o.missing} revision${o.missing === 1 ? '' : 's'}, from ${o.first})`);
828
- process.stdout.write(`Still owed: ${owed.join(', ')}. Later records were applied; the next pull applies these once they arrive and can be read.\n`);
841
+ process.stdout.write(`${paint.warn(`Still owed: ${owed.join(', ')}. Later records were applied; the next pull applies these once they arrive and can be read.`)}\n`);
829
842
  }
830
843
  }
831
844
  finally {
@@ -866,7 +879,7 @@ export function memoryCommand(argv) {
866
879
  case 'sync':
867
880
  return memorySync(rest);
868
881
  default:
869
- process.stderr.write(MEMORY_USAGE);
882
+ process.stderr.write(styleUsage(MEMORY_USAGE));
870
883
  process.exit(1);
871
884
  }
872
885
  }