@empirical-gauzza/cli 1.0.29 → 1.0.31

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.
@@ -50,18 +50,22 @@ const RECALL_PATHS = ['semantic', 'list', 'graph', 'hebbian'];
50
50
  const RECALL_HOPS = [1, 2];
51
51
  // Hops only mean something on a pathway that walks relationships.
52
52
  const RECALL_HOP_PATHS = ['graph', 'hebbian'];
53
+ // Reminder states `memory query --remind-state` may keep. Same enum the server's shared
54
+ // normalizer and Memory.remindState declare.
55
+ const REMIND_STATES = ['pending', 'notified', 'acknowledged', 'cancelled'];
53
56
 
54
57
  // ─── Scoped `--help` (C-005 item 5) ───────────────────────────────────────────
55
58
  // `empirical memory --help` / `empirical memory <sub> --help` print scoped usage instead of
56
59
  // the 90-line global help wall bin/empirical.js otherwise short-circuits every --help to.
57
60
 
58
61
  const MEMORY_SUBCOMMAND_USAGE = {
59
- query: 'Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--tags-all <t1,t2>] [--status <s1,s2>] [--path semantic|list|graph|hebbian] [--hops 1|2] [--pretty] [--format table]',
62
+ query: 'Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--tags-all <t1,t2>] [--status <s1,s2>] [--due-before <ISO date>] [--due-after <ISO date>] [--remind-state pending,notified] [--path semantic|list|graph|hebbian] [--hops 1|2] [--pretty] [--format table]',
60
63
  get: 'Usage: empirical memory get --memory-id <id> [--pretty]',
61
64
  list: 'Usage: empirical memory list [--category <cat>] [--tags <t1,t2>] [--start-date <ISO>] [--end-date <ISO>] [--limit 20] [--cursor <cursor>] [--oldest] [--pretty] [--format table]',
62
65
  record: 'Usage: empirical memory record --category <cat> (--summary <text> | --summary - | --summary-file <path>) [--tags t1,t2] [--node-type observation] [--json-file <path>] [--no-dedupe] [--no-auto-link] [--author-type user|agent]',
63
66
  note: 'Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--min-score 0.35]',
64
67
  update: 'Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--min-score 0.35]',
68
+ acknowledge: 'Usage: empirical memory acknowledge --memory-id <id> [--cancel] (marks a due reminder done, or dropped with --cancel; frees its slot and clears the alert)',
65
69
  delete: 'Usage: empirical memory delete (--match <query> | --memory-id <id>) [--confirm] [--min-score 0.35]\nUsage: empirical memory delete (--tag <t> | --category <c> | --all) --confirm',
66
70
  duplicates: 'Usage: empirical memory duplicates --memory-id <id>',
67
71
  merge: 'Usage: empirical memory merge --canonical-id <id> --duplicate-id <id> [--no-merge-tags] [--no-merge-relationships]',
@@ -209,6 +213,29 @@ async function runMemoryQuery(args) {
209
213
  const pretty = parseFlag(args, '--pretty');
210
214
  const format = parseOption(args, '--format', 'json');
211
215
 
216
+ // Reminder filters. Dates are parsed HERE as well as server-side, for the same reason --due
217
+ // is on record: a typo fails at the terminal, not as a 400 after the round trip. Anything
218
+ // Date can parse is accepted and sent as the caller typed it.
219
+ const dueFilters = {};
220
+ for (const [flag, key] of [['--due-before', 'dueBefore'], ['--due-after', 'dueAfter']]) {
221
+ const raw = parseOption(args, flag, '');
222
+ if (!raw) continue;
223
+ if (Number.isNaN(new Date(raw).getTime())) {
224
+ warn(`Invalid ${flag} "${raw}". Use an ISO 8601 date, for example 2026-10-14 or 2026-10-14T09:00:00Z.`);
225
+ process.exit(1);
226
+ }
227
+ dueFilters[key] = String(raw).trim();
228
+ }
229
+ const remindStateRaw = parseOption(args, '--remind-state', '');
230
+ const remindState = remindStateRaw
231
+ ? remindStateRaw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
232
+ : [];
233
+ const unknownStates = remindState.filter((s) => !REMIND_STATES.includes(s));
234
+ if (unknownStates.length) {
235
+ warn(`--remind-state must be one of: ${REMIND_STATES.join(', ')} (received ${JSON.stringify(unknownStates)})`);
236
+ process.exit(1);
237
+ }
238
+
212
239
  // --path / --hops are validated HERE, where the value is decided, so a typo costs no round
213
240
  // trip and the message names the four pathways. The server validates the same way (the shared
214
241
  // normalizer in packages/server/src/services/recallParams.service.ts); this is a fast local
@@ -252,6 +279,9 @@ async function runMemoryQuery(args) {
252
279
  if (tags.length) body.tags = tags;
253
280
  if (tagsAll.length) body.tagsAll = tagsAll;
254
281
  if (status.length) body.status = status;
282
+ if (dueFilters.dueBefore) body.dueBefore = dueFilters.dueBefore;
283
+ if (dueFilters.dueAfter) body.dueAfter = dueFilters.dueAfter;
284
+ if (remindState.length) body.remindState = remindState;
255
285
  if (path) body.path = path;
256
286
  if (hops !== null) body.hops = hops;
257
287
 
@@ -490,6 +520,23 @@ async function runMemoryRecord(args) {
490
520
  payload.authorType = authorType;
491
521
  }
492
522
 
523
+ // --due turns the memory into a reminder. Parsed and rejected HERE as well as server-side, so
524
+ // a typo fails immediately at the terminal instead of coming back as a 400 after the write was
525
+ // composed. Accepts anything Date can parse, so '2026-10-14' and a full ISO timestamp both work.
526
+ const dueRaw = parseOption(args, '--due', '');
527
+ if (dueRaw) {
528
+ const parsedDue = new Date(dueRaw);
529
+ if (Number.isNaN(parsedDue.getTime())) {
530
+ warn(JSON.stringify({
531
+ error: true,
532
+ code: 'invalid_due',
533
+ message: `Invalid --due "${dueRaw}". Use an ISO 8601 date, for example 2026-10-14 or 2026-10-14T09:00:00Z.`
534
+ }));
535
+ process.exit(1);
536
+ }
537
+ payload.dueAt = parsedDue.toISOString();
538
+ }
539
+
493
540
  const accessToken = requireAccessToken();
494
541
  const baseUrl = resolveApiBaseUrl();
495
542
  const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
@@ -549,6 +596,29 @@ async function runMemoryNote(args) {
549
596
  log(JSON.stringify(created));
550
597
  }
551
598
 
599
+ // Closing a reminder from the terminal or from an agent reading a CLI banner. One PUT, the same
600
+ // transition the portal's Mark done / Cancel make; the server retires the alert on every surface.
601
+ async function runMemoryAcknowledge(args) {
602
+ const memoryId = parseOption(args, '--memory-id', '');
603
+ const cancel = parseFlag(args, '--cancel');
604
+ if (!memoryId) {
605
+ warn(MEMORY_SUBCOMMAND_USAGE.acknowledge);
606
+ process.exit(1);
607
+ }
608
+ const accessToken = requireAccessToken();
609
+ const baseUrl = resolveApiBaseUrl();
610
+ const url = new URL(`/api/v1/memories/${encodeURIComponent(memoryId)}`, baseUrl).toString();
611
+ let response;
612
+ try {
613
+ response = await httpJson(url, 'PUT', { remindState: cancel ? 'cancelled' : 'acknowledged' }, buildApiAuthHeaders(accessToken));
614
+ } catch (error) {
615
+ printApiError(error);
616
+ process.exit(1);
617
+ }
618
+ const data = response && response.data ? response.data : response;
619
+ log(JSON.stringify({ id: data.id, remindState: data.remindState, dueAt: data.dueAt || null, message: cancel ? 'Reminder cancelled. Its slot is free.' : 'Reminder marked done. Its slot is free.' }, null, 2));
620
+ }
621
+
552
622
  async function runMemoryUpdate(args) {
553
623
  const match = parseOption(args, '--match', '');
554
624
  const patchJson = parseOption(args, '--patch-json', '');
@@ -941,9 +1011,21 @@ async function runMemoryBulkImport(args) {
941
1011
  const bulkUrl = new URL('/api/v1/memories/bulk', baseUrl).toString();
942
1012
 
943
1013
  try {
944
- const response = await httpJson(bulkUrl, 'POST', { memories, relationships }, buildApiAuthHeaders(accessToken));
945
- const data = (response && response.data) ? response.data : response;
946
- log(JSON.stringify(data));
1014
+ const enqueueResponse = await httpJson(bulkUrl, 'POST', { memories, relationships }, buildApiAuthHeaders(accessToken));
1015
+ const { jobId } = (enqueueResponse && enqueueResponse.data) ? enqueueResponse.data : enqueueResponse;
1016
+ const statusUrl = new URL(`/api/v1/memories/bulk/${jobId}`, baseUrl).toString();
1017
+
1018
+ const pollIntervalMs = 3000;
1019
+ const maxPolls = 200;
1020
+ for (let i = 0; i < maxPolls; i += 1) {
1021
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1022
+ const statusResponse = await httpJson(statusUrl, 'GET', undefined, buildApiAuthHeaders(accessToken));
1023
+ const job = (statusResponse && statusResponse.data) ? statusResponse.data : statusResponse;
1024
+ if (job.status === 'succeeded') { log(JSON.stringify(job)); return; }
1025
+ if (job.status === 'failed') { warn(JSON.stringify(job)); process.exit(1); }
1026
+ }
1027
+ warn(JSON.stringify({ error: true, code: 'timeout', message: `Job ${jobId} did not complete within ${(maxPolls * pollIntervalMs) / 1000}s` }));
1028
+ process.exit(1);
947
1029
  } catch (error) {
948
1030
  printApiError(error);
949
1031
  process.exit(1);
@@ -1025,6 +1107,7 @@ async function runMemoryCommand(subArgs) {
1025
1107
  if (sub === 'record') return runMemoryRecord(rest);
1026
1108
  if (sub === 'note') return runMemoryNote(rest);
1027
1109
  if (sub === 'update') return runMemoryUpdate(rest);
1110
+ if (sub === 'acknowledge' || sub === 'ack' || sub === 'done') return runMemoryAcknowledge(rest);
1028
1111
  if (sub === 'delete') return runMemoryDelete(rest);
1029
1112
  if (sub === 'duplicates') return runMemoryDuplicates(rest);
1030
1113
  if (sub === 'merge') return runMemoryMerge(rest);
@@ -1053,6 +1136,7 @@ module.exports = {
1053
1136
  runMemoryRecord,
1054
1137
  runMemoryNote,
1055
1138
  runMemoryUpdate,
1139
+ runMemoryAcknowledge,
1056
1140
  runMemoryDelete,
1057
1141
  runMemoryDuplicates,
1058
1142
  runMemoryMerge,
@@ -7,6 +7,8 @@ const {
7
7
  OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo']
8
8
  } = require('./installers/nativePlugin');
9
9
  const { checkNativePluginUpdates } = require('./pluginUpdateCheck');
10
+ const { addInstalledHosts } = require('./deviceConfig');
11
+ const { writeCoreMemoryGuidance } = require('./installers/coreMemoryBlock');
10
12
 
11
13
  const SUPPORTED_TARGETS = ['codex', 'copilot', 'claude'];
12
14
 
@@ -68,6 +70,15 @@ async function runPluginCommand(args = []) {
68
70
  if (pluginNames.length === 1) installNativePlugin(target);
69
71
  else installNativePlugin(target, pluginNames);
70
72
  log(uiSuccess(`${target} plugins installed through its marketplace: ${pluginNames.join(', ')}.`));
73
+ // The skill alone does not make an agent use Empirical; the identity-framed block in the
74
+ // host's global instructions file does. Write it on this path too, not only in `install`.
75
+ writeCoreMemoryGuidance(target, pluginNames);
76
+ // Record the host so `empirical doctor` checks it, and only it, from now on.
77
+ try {
78
+ addInstalledHosts([target]);
79
+ } catch (_error) {
80
+ // Device-config bookkeeping must never fail a plugin install that already succeeded.
81
+ }
71
82
  }
72
83
  log('');
73
84
  log('Native plugin installation does not configure MCP or credentials.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empirical-gauzza/cli",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "Persistent cross-tool memory for AI agents. Give Claude Code, Codex, Copilot, Cursor, and ChatGPT one shared memory: save project context once, recall it everywhere via CLI or MCP.",
5
5
  "keywords": [
6
6
  "memory",