@empirical-gauzza/cli 1.0.36 → 1.0.38

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.
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ // Resolving "which memory did the user mean" from a --match query, shared by every memory
4
+ // subcommand that accepts --match. It lives in its own module so attachmentCommands.js can use
5
+ // it without a require cycle back through memoryCommands.js; memoryCommands.js re-exports the
6
+ // three names so existing importers and tests are unaffected.
7
+
8
+ const { warn } = require('./ui');
9
+ const { httpJson } = require('./http');
10
+ const { parseOption } = require('./env');
11
+ const { buildApiAuthHeaders } = require('./auth');
12
+
13
+ async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters) {
14
+ const queryUrl = new URL('/api/v1/memories/query', baseUrl).toString();
15
+ const body = { query: match, topK: 5 };
16
+ if (filters && Object.keys(filters).length) body.filters = filters;
17
+ let response;
18
+ try {
19
+ response = await httpJson(queryUrl, 'POST', body, buildApiAuthHeaders(accessToken));
20
+ } catch (error) {
21
+ return { found: false, reason: 'error', message: error.message };
22
+ }
23
+
24
+ const results = (response && response.data && Array.isArray(response.data.results))
25
+ ? response.data.results
26
+ : (Array.isArray(response) ? response : []);
27
+
28
+ if (!results.length) {
29
+ return { found: false, reason: 'no_results' };
30
+ }
31
+
32
+ const scored = results.map((r) => ({
33
+ id: r.id,
34
+ summary: r.summary,
35
+ score: r.rankScore != null ? r.rankScore : (r.similarityScore != null ? r.similarityScore : 0)
36
+ }));
37
+
38
+ scored.sort((a, b) => b.score - a.score);
39
+
40
+ // A caller-supplied filter (e.g. --tags) that narrows the server-side result set to exactly
41
+ // one memory is already a stronger disambiguation signal than the semantic score - trust it
42
+ // outright rather than falling through to the score check below. This matters because Atlas
43
+ // vector search has a real async indexing lag: a memory created moments ago can come back
44
+ // with score 0 (unindexed, not "not this one") even when a tag filter already proves it is
45
+ // the only candidate. Fixes a real bug found 2026-08-25 where delete --match failed on a
46
+ // memory `query` found instantly, because delete had no way to trust a filtered, unambiguous
47
+ // result over a stale/zero semantic score.
48
+ if (filters && Object.keys(filters).length && results.length === 1) {
49
+ const only = scored[0];
50
+ return { found: true, memory: { id: only.id, summary: only.summary, score: only.score } };
51
+ }
52
+
53
+ const top = scored[0];
54
+ const second = scored[1];
55
+
56
+ if (top.score < minScore) {
57
+ return { found: false, reason: 'low_score', candidates: scored.slice(0, 3) };
58
+ }
59
+
60
+ if (second && (top.score - second.score) <= 0.05) {
61
+ return { found: false, reason: 'ambiguous', candidates: scored.slice(0, 3) };
62
+ }
63
+
64
+ return { found: true, memory: { id: top.id, summary: top.summary, score: top.score } };
65
+ }
66
+
67
+ // --match resolution depends on Atlas vector search, which has a real async indexing lag:
68
+ // a memory created moments ago can be invisible to $vectorSearch even though it is already
69
+ // findable by tag. Letting --match commands pass --tags narrows the candidate pool directly
70
+ // instead of hoping semantic ranking surfaces the right one - the fix for a real bug found
71
+ // 2026-08-25 where delete --match returned unrelated score-0 candidates for a memory that
72
+ // `query` found instantly. Deliberately --tags (plural) only, not --category: `delete` already
73
+ // uses singular --tag/--category to mean "bulk delete everything in this category", so adding
74
+ // --category here would silently reroute a --match delete into a bulk delete instead of
75
+ // narrowing it - --tags never collides with that bulk-mode gate.
76
+ function parseMatchFilters(args) {
77
+ const tagsRaw = parseOption(args, '--tags', '');
78
+ const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
79
+ const filters = {};
80
+ if (tags.length) filters.tags = tags;
81
+ return filters;
82
+ }
83
+
84
+ function printMemoryResolveError(result) {
85
+ if (result.reason === 'no_results') {
86
+ warn(JSON.stringify({ error: true, code: 'no_results', message: 'No memories matched the query.' }));
87
+ } else if (result.reason === 'low_score') {
88
+ warn(JSON.stringify({
89
+ error: true,
90
+ code: 'low_score',
91
+ message: 'Top result score was below --min-score threshold.',
92
+ candidates: result.candidates
93
+ }));
94
+ } else if (result.reason === 'ambiguous') {
95
+ warn(JSON.stringify({
96
+ error: true,
97
+ code: 'ambiguous',
98
+ message: 'Multiple memories matched with similar scores. Use --memory-id to target one directly.',
99
+ candidates: result.candidates
100
+ }));
101
+ } else if (result.reason === 'error') {
102
+ warn(JSON.stringify({ error: true, code: 'query_error', message: result.message }));
103
+ } else {
104
+ warn(JSON.stringify({ error: true, code: 'resolve_failed', message: 'Could not resolve memory.' }));
105
+ }
106
+ }
107
+
108
+ module.exports = {
109
+ resolveMemoryByQuery,
110
+ parseMatchFilters,
111
+ printMemoryResolveError
112
+ };
@@ -4,7 +4,7 @@ const { log, warn, uiHeader, uiSuccess } = require('./ui');
4
4
  const { detectInstallTargets } = require('./installers/detect');
5
5
  const {
6
6
  installNativePlugin,
7
- OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo']
7
+ OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo', 'empirical-voice']
8
8
  } = require('./installers/nativePlugin');
9
9
  const { checkNativePluginUpdates } = require('./pluginUpdateCheck');
10
10
  const { addInstalledHosts } = require('./deviceConfig');
@@ -35,6 +35,7 @@ function resolvePluginNames(args = []) {
35
35
  if (args.includes('--all-optional') || args.includes('--work-history')) names.push('empirical-work-history');
36
36
  if (args.includes('--all-optional') || args.includes('--scar-tissue')) names.push('empirical-scar-tissue');
37
37
  if (args.includes('--all-optional') || args.includes('--todo')) names.push('empirical-todo');
38
+ if (args.includes('--all-optional') || args.includes('--voice')) names.push('empirical-voice');
38
39
  return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
39
40
  }
40
41
 
@@ -82,7 +83,7 @@ async function runPluginCommand(args = []) {
82
83
  }
83
84
  log('');
84
85
  log('Native plugin installation does not configure MCP or credentials.');
85
- log('Optional packages: --work-history, --scar-tissue, --todo, or --all-optional.');
86
+ log('Optional packages: --work-history, --scar-tissue, --todo, --voice, or --all-optional.');
86
87
  log('Use `empirical auth login` for normal CLI OAuth, or the full `empirical install` flow for MCP setup.');
87
88
  }
88
89