@empirical-gauzza/cli 1.0.27 → 1.0.28

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.
@@ -48,6 +48,8 @@ If you are not sure whether a task qualifies, treat it as if it does.
48
48
  Add `--category` or `--tags` when known. Omit `--pretty` for routine recall because graph-rich
49
49
  results can be large. A slow response alone is API latency, not an outage; report an outage
50
50
  only when the CLI returns an actual error, and use `empirical doctor` to verify connectivity.
51
+ When the user asks for every record of a topic, add `--path list`; when they ask what a
52
+ memory connects to, add `--path graph`.
51
53
 
52
54
  3. Scope recall to the project you are in. Derive a project tag from the repository name
53
55
  (`git rev-parse --show-toplevel`, then the folder name) or, outside a repo, the working
@@ -61,6 +63,21 @@ If you are not sure whether a task qualifies, treat it as if it does.
61
63
  doc memory`) over a match query. For exhaustive topic retrieval, use the topic as a tag and
62
64
  follow pagination.
63
65
 
66
+ 5. A filtered query (category, tags, nodeType) that returns nothing is not proof the memory
67
+ doesn't exist — it only proves nothing matched that exact filter combination. A memory's
68
+ real category or tag convention often does not match how you or the user would naturally
69
+ describe it in conversation (e.g. described as "an idea" but stored under a different
70
+ category; tagged with a project's short name where you tried its full one). Before
71
+ concluding a memory doesn't exist, retry at least once with the filter relaxed or removed —
72
+ category dropped, tags loosened to an OR match, or a plain unfiltered query — rather than
73
+ only varying the search phrase.
74
+
75
+ 6. If a specific recall miss turns out to have a narrow, demonstrable cause (a filter mismatch,
76
+ a stale tag, a wrong category), stop there. Report that cause and move on — do not generalize
77
+ one miss into a theory about the memory system being broken and then go build measurement or
78
+ debugging work around the theory. Confirm the narrow cause is insufficient before entertaining
79
+ a systemic explanation, not after.
80
+
64
81
  ## Follow the current user policy
65
82
 
66
83
  The installer questionnaire configures a dynamic Empirical policy. It is user-scoped and may
@@ -113,6 +130,13 @@ Two rules that are not optional:
113
130
  Both dedupe against near-duplicate existing memories and auto-link related ones by default; pass
114
131
  `--no-dedupe` / `--no-auto-link` on the CLI only when you deliberately want to skip one of those.
115
132
 
133
+ **One idea per memory, not a bundle.** When a turn produces several distinct facts or ideas
134
+ worth keeping, record each as its own call rather than folding them into one large memory.
135
+ `record_graph_memory` (and `empirical memory record`) can and should be called multiple times
136
+ in the same turn — that is expected usage, not overhead to be minimized. A compound memory is
137
+ harder to find later (a query matching one idea in it won't surface the others as well) and
138
+ harder to link precisely into the graph than several atomic ones.
139
+
116
140
  **Record facts about the USER, not about your own work.** You are a scribe here. "Matt prefers
117
141
  short status updates" is a memory; "refactored the cluster selector, tests pass" is your diary
118
142
  and belongs in the work-history skill, tagged and with `mass: 1`. The distinction is the
@@ -44,6 +44,8 @@ If you are not sure whether a task qualifies, treat it as if it does.
44
44
  Add `--category` or `--tags` when known. Omit `--pretty` for routine recall because graph-rich
45
45
  results can be large. Use `empirical memory list --recent` (see `empirical doc memory` for the
46
46
  exact flag) for "recent/latest/today" requests instead of a match query.
47
+ When the user asks for every record of a topic, add `--path list`; when they ask what a
48
+ memory connects to, add `--path graph`.
47
49
  3. Scope recall to the project you are in. Derive a project tag from the repository name
48
50
  (`git rev-parse --show-toplevel`, then the folder name) or, outside a repo, the working
49
51
  directory name, lowercased with spaces as hyphens. Add `--tags <project>` to every
@@ -52,6 +54,18 @@ If you are not sure whether a task qualifies, treat it as if it does.
52
54
  facts about this one.
53
55
  4. A slow response alone is API latency, not an outage; report an outage only when the CLI returns
54
56
  an actual error, and use `empirical doctor` to verify connectivity.
57
+ 5. A filtered query (`--category`, `--tags`, `--tags-all`, `--status`) that returns nothing is not
58
+ proof the memory doesn't exist — it only proves nothing matched that exact filter combination.
59
+ A memory's real category or tag convention often does not match how you or the user would
60
+ naturally describe it in conversation (e.g. described as "an idea" but stored under a different
61
+ category; tagged with a project's short name where you tried its full one). Before concluding a
62
+ memory doesn't exist, retry at least once with the filter relaxed or removed — category dropped,
63
+ tags loosened, or a plain unfiltered `--match` — rather than only varying the search phrase.
64
+ 6. If a specific recall miss turns out to have a narrow, demonstrable cause (a filter mismatch, a
65
+ stale tag, a wrong category), stop there. Report that cause and move on — do not generalize one
66
+ miss into a theory about the memory system being broken and then go build measurement or
67
+ debugging work around the theory. Confirm the narrow cause is insufficient before entertaining a
68
+ systemic explanation, not after.
55
69
 
56
70
  ## Follow the current user policy
57
71
 
@@ -105,6 +119,13 @@ Two rules that are not optional:
105
119
  first. Pass `--no-dedupe` or `--no-auto-link` only when you specifically want to bypass one of
106
120
  those, which should be rare.
107
121
 
122
+ **One idea per memory, not a bundle.** When a turn produces several distinct facts or ideas
123
+ worth keeping, record each with its own `empirical memory record` call rather than folding them
124
+ into one large memory. Calling it multiple times in the same turn is expected usage, not
125
+ overhead to be minimized. A compound memory is harder to find later (a query matching one idea
126
+ in it won't surface the others as well) and harder to link precisely into the graph than several
127
+ atomic ones.
128
+
108
129
  **Record facts about the USER, not about your own work.** You are a scribe here. "Matt prefers
109
130
  short status updates" is a memory; "refactored the cluster selector, tests pass" is your diary
110
131
  and belongs in the work-history skill, tagged and with `mass: 1`. The distinction is the
@@ -39,9 +39,24 @@ If you are not sure whether a task qualifies, treat it as if it does.
39
39
  `nodeType`, and `monthKey` when they improve recall. For "recent/latest/today" requests, prefer
40
40
  `list_memories` instead — it is the tool built for that shape of request. For exhaustive topic
41
41
  retrieval, use the topic as a tag and follow pagination.
42
+ When the user asks for every record of a topic use `list_memories` with that topic as a tag,
43
+ and when they ask what a memory connects to use `get_memory_neighbors`.
42
44
  5. A slow response alone is API latency, not an outage; report an outage only when a tool call
43
45
  returns an actual error, and use `get_empirical_policy` or a retry to verify connectivity before
44
46
  escalating.
47
+ 6. A filtered `query_memories` call (`category`, `tags`, `nodeType`) that returns nothing is not
48
+ proof the memory doesn't exist — it only proves nothing matched that exact filter combination.
49
+ A memory's real category or tag convention often does not match how you or the user would
50
+ naturally describe it in conversation (e.g. described as "an idea" but stored under a
51
+ different category; tagged with a project's short name where you tried its full one). Before
52
+ concluding a memory doesn't exist, retry at least once with the filter relaxed or removed —
53
+ category dropped, tags loosened to an OR match, or a plain unfiltered query — rather than only
54
+ varying the search phrase.
55
+ 7. If a specific recall miss turns out to have a narrow, demonstrable cause (a filter mismatch, a
56
+ stale tag, a wrong category), stop there. Report that cause and move on — do not generalize one
57
+ miss into a theory about the memory system being broken and then go build measurement or
58
+ debugging work around the theory. Confirm the narrow cause is insufficient before entertaining a
59
+ systemic explanation, not after.
45
60
 
46
61
  ## Follow the current user policy
47
62
 
@@ -82,6 +97,13 @@ Two rules that are not optional:
82
97
  with the `record_graph_memory` MCP tool. It runs write-intent classification, dedupe, and
83
98
  auto-linking automatically — you do not need to check for duplicates yourself first.
84
99
 
100
+ **One idea per memory, not a bundle.** When a turn produces several distinct facts or ideas
101
+ worth keeping, record each with its own `record_graph_memory` call rather than folding them
102
+ into one large memory. Calling it multiple times in the same turn is expected usage, not
103
+ overhead to be minimized. A compound memory is harder to find later (a query matching one idea
104
+ in it won't surface the others as well) and harder to link precisely into the graph than several
105
+ atomic ones.
106
+
85
107
  **Record facts about the USER, not about your own work.** You are a scribe here. "Matt prefers
86
108
  short status updates" is a memory; "refactored the cluster selector, tests pass" is your diary
87
109
  and belongs in the work-history skill, tagged and with `mass: 1`. The distinction is the
@@ -6,7 +6,7 @@ const { log, warn } = require('./ui');
6
6
  const { httpJson, printApiError } = require('./http');
7
7
  const { parseOption, parseFlag } = require('./env');
8
8
  const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
9
- const { MEMORY_CATEGORIES, DOC_TOPICS } = require('./constants');
9
+ const { MEMORY_CATEGORIES, NODE_TYPES, DOC_TOPICS } = require('./constants');
10
10
  const { withSelectedWorkspaceContext } = require('./workspaceContext');
11
11
 
12
12
  // ─── Table rendering (--format table) ─────────────────────────────────────────
@@ -34,17 +34,29 @@ function renderMemoryResultsTable(items) {
34
34
  id: m.id || m._id || '',
35
35
  category: m.category || '',
36
36
  summary: truncateForTable(m.summary, 60),
37
- score: m.rankScore != null ? m.rankScore : (m.similarityScore != null ? m.similarityScore : '')
37
+ // The recall front door emits `score` (toQueryRow plus score in mcp/tools/memory.ts); the
38
+ // older /query route emitted rankScore/similarityScore. Read all three so the column is not
39
+ // blank against a real /recall row. On the list pathways score is deliberately absent, and
40
+ // an empty cell is the honest rendering of "this pathway does not rank".
41
+ score: m.score != null ? m.score : (m.rankScore != null ? m.rankScore : (m.similarityScore != null ? m.similarityScore : ''))
38
42
  }));
39
43
  return renderTable(rows, ['id', 'category', 'summary', 'score']);
40
44
  }
41
45
 
46
+ // The retrieval pathways `memory query --path` may pin. Same four names the server's shared
47
+ // normalizer accepts (packages/server/src/services/recallParams.service.ts). Omitting --path
48
+ // leaves the classifier deciding, exactly as before.
49
+ const RECALL_PATHS = ['semantic', 'list', 'graph', 'hebbian'];
50
+ const RECALL_HOPS = [1, 2];
51
+ // Hops only mean something on a pathway that walks relationships.
52
+ const RECALL_HOP_PATHS = ['graph', 'hebbian'];
53
+
42
54
  // ─── Scoped `--help` (C-005 item 5) ───────────────────────────────────────────
43
55
  // `empirical memory --help` / `empirical memory <sub> --help` print scoped usage instead of
44
56
  // the 90-line global help wall bin/empirical.js otherwise short-circuits every --help to.
45
57
 
46
58
  const MEMORY_SUBCOMMAND_USAGE = {
47
- query: 'Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--tags-all <t1,t2>] [--status <s1,s2>] [--pretty] [--format table]',
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]',
48
60
  get: 'Usage: empirical memory get --memory-id <id> [--pretty]',
49
61
  list: 'Usage: empirical memory list [--category <cat>] [--tags <t1,t2>] [--start-date <ISO>] [--end-date <ISO>] [--limit 20] [--cursor <cursor>] [--oldest] [--pretty] [--format table]',
50
62
  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]',
@@ -179,7 +191,7 @@ function printMemoryResolveError(result) {
179
191
  async function runMemoryQuery(args) {
180
192
  const match = parseOption(args, '--match', '');
181
193
  if (!match) {
182
- warn('Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--tags-all <t1,t2>] [--status <s1,s2>] [--pretty]');
194
+ warn(MEMORY_SUBCOMMAND_USAGE.query);
183
195
  process.exit(1);
184
196
  }
185
197
 
@@ -197,29 +209,79 @@ async function runMemoryQuery(args) {
197
209
  const pretty = parseFlag(args, '--pretty');
198
210
  const format = parseOption(args, '--format', 'json');
199
211
 
212
+ // --path / --hops are validated HERE, where the value is decided, so a typo costs no round
213
+ // trip and the message names the four pathways. The server validates the same way (the shared
214
+ // normalizer in packages/server/src/services/recallParams.service.ts); this is a fast local
215
+ // echo of that contract, never a second, looser one.
216
+ const pathRaw = parseOption(args, '--path', '');
217
+ let path = '';
218
+ if (pathRaw) {
219
+ path = String(pathRaw).trim().toLowerCase();
220
+ if (!RECALL_PATHS.includes(path)) {
221
+ warn(`--path must be one of: ${RECALL_PATHS.join(', ')} (received ${JSON.stringify(pathRaw)})`);
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ const hopsRaw = parseOption(args, '--hops', '');
227
+ let hops = null;
228
+ if (hopsRaw) {
229
+ const parsedHops = Number(hopsRaw);
230
+ if (!RECALL_HOPS.includes(parsedHops)) {
231
+ warn(`--hops must be 1 or 2 (received ${JSON.stringify(hopsRaw)})`);
232
+ process.exit(1);
233
+ }
234
+ // Silently ignoring --hops on a ranked or list pathway would look like it took effect.
235
+ if (!RECALL_HOP_PATHS.includes(path)) {
236
+ warn(`--hops only applies with --path graph or --path hebbian${path ? ` (received --path ${path})` : ' (no --path given)'}`);
237
+ process.exit(1);
238
+ }
239
+ hops = parsedHops;
240
+ }
241
+
200
242
  const accessToken = requireAccessToken();
201
243
  const baseUrl = resolveApiBaseUrl();
202
- const queryUrl = new URL('/api/v1/memories/query', baseUrl).toString();
244
+ // The recall front door: the same answer the MCP query_memories tool gives, which /query never
245
+ // gave (classifier routing, the list block, the exact-tag retry, agent-note exclusion,
246
+ // retrievalPath). It takes its parameters FLAT, matching the shared normalizer, not nested
247
+ // under `filters` the way the older /query route did. Send one shape only.
248
+ const queryUrl = new URL('/api/v1/memories/recall', baseUrl).toString();
203
249
 
204
250
  const body = { query: match, topK };
205
- const filters = {};
206
- if (category) filters.category = category;
207
- if (tags.length) filters.tags = tags;
208
- if (tagsAll.length) filters.tagsAll = tagsAll;
209
- if (status.length) filters.status = status;
210
- if (Object.keys(filters).length) body.filters = filters;
251
+ if (category) body.category = category;
252
+ if (tags.length) body.tags = tags;
253
+ if (tagsAll.length) body.tagsAll = tagsAll;
254
+ if (status.length) body.status = status;
255
+ if (path) body.path = path;
256
+ if (hops !== null) body.hops = hops;
211
257
 
212
258
  let response;
213
259
  try {
214
260
  response = await httpJson(queryUrl, 'POST', body, buildApiAuthHeaders(accessToken));
215
261
  } catch (error) {
262
+ // A 404 here means the server predates the recall front door. Say that plainly instead of
263
+ // rerouting to /query: that fallback would quietly answer through a different routing path
264
+ // with different defaults (topK 10 against 3) and keep the old route alive forever.
265
+ if (error && error.statusCode === 404) {
266
+ warn(JSON.stringify({
267
+ error: true,
268
+ code: 'recall_route_missing',
269
+ message: 'This server does not serve /api/v1/memories/recall yet. Upgrade the Empirical server, or use an older CLI against it.'
270
+ }));
271
+ process.exit(1);
272
+ }
216
273
  printApiError(error);
217
274
  process.exit(1);
218
275
  }
219
276
 
220
- const results = (response && response.data && Array.isArray(response.data.results))
221
- ? response.data.results
222
- : (Array.isArray(response) ? response : []);
277
+ // The front door answers with data.memories; the older /query route answered with data.results.
278
+ // Both are accepted so a CLI pointed at a server that predates /recall still prints rows.
279
+ const data = response && response.data ? response.data : null;
280
+ const results = (data && Array.isArray(data.memories))
281
+ ? data.memories
282
+ : ((data && Array.isArray(data.results))
283
+ ? data.results
284
+ : (Array.isArray(response) ? response : []));
223
285
 
224
286
  if (format === 'table') {
225
287
  log(renderMemoryResultsTable(results));
@@ -353,6 +415,7 @@ async function runMemoryRecord(args) {
353
415
  if (!category || !summary) {
354
416
  warn('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]');
355
417
  warn('Valid categories: ' + MEMORY_CATEGORIES.join(', '));
418
+ warn('Valid node types: ' + NODE_TYPES.join(', '));
356
419
  process.exit(1);
357
420
  }
358
421
 
@@ -369,6 +432,15 @@ async function runMemoryRecord(args) {
369
432
  const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
370
433
  const nodeType = parseOption(args, '--node-type', 'observation');
371
434
 
435
+ if (!NODE_TYPES.includes(nodeType)) {
436
+ warn(JSON.stringify({
437
+ error: true,
438
+ code: 'invalid_node_type',
439
+ message: `Invalid nodeType "${nodeType}". Valid: ${NODE_TYPES.join(', ')}`
440
+ }));
441
+ process.exit(1);
442
+ }
443
+
372
444
  payload = { category, summary, nodeType, data: { text: String(summary) } };
373
445
  if (tags.length) payload.tags = tags;
374
446
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empirical-gauzza/cli",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
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",