@empirical-gauzza/cli 1.0.20 → 1.0.21

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.
@@ -1,59 +1,59 @@
1
- 'use strict';
2
-
3
- // C-001 (docs/prd-cli-and-plugin-fixes.md): mode-aware empirical-memory Skill content.
4
- //
5
- // The plugin marketplace (mattgauzza/empirical-memory-plugin) is the source of truth for Skill
6
- // content, but its manifest schema (see meta/manifests.json / build-manifests.mjs in that repo)
7
- // has no field that selects among sibling SKILL.*.md files at install time -- every host's native
8
- // plugin mechanism resolves a plugin's skill from the literal filename `SKILL.md`. So the
9
- // marketplace always ships the "both" variant as SKILL.md, and this CLI is what actually makes
10
- // the user's chosen integration mode matter: it vendors its own copy of all three variants and
11
- // overwrites the on-disk SKILL.md for each host after a native plugin install/update (see
12
- // nativePlugin.js's applySkillModeVariant) and after a Kit sync (see skillSync.js).
13
- //
14
- // These files are copied from, and must be kept in sync with,
15
- // M:\Projects\empirical-memory-plugin\skills\empirical-memory\SKILL.{mcp,cli,both}.md. There is no
16
- // automated sync between the two repos; a human updates both when skill content changes.
17
- const fs = require('fs');
18
- const path = require('path');
19
-
20
- const VARIANTS_DIR = path.join(__dirname, 'skill-variants');
21
-
22
- const VARIANT_FILES = {
23
- mcp: 'SKILL.mcp.md',
24
- cli: 'SKILL.cli.md',
25
- both: 'SKILL.both.md'
26
- };
27
-
28
- // installWizard.js's `integrationMode`: 1 = MCP, 2 = CLI-direct, 3 = both.
29
- function variantNameForIntegrationMode(integrationMode) {
30
- if (integrationMode === 2) return 'cli';
31
- if (integrationMode === 3) return 'both';
32
- return 'mcp';
33
- }
34
-
35
- // Returns the variant's markdown content, or null if the vendored file is missing (e.g. a stale
36
- // CLI release published before a variant existed). Never throws -- callers must treat a missing
37
- // variant as "leave whatever is already installed" per the PRD's own risk mitigation, not as a
38
- // fatal error.
39
- function readSkillVariantContent(variantName) {
40
- const fileName = VARIANT_FILES[variantName];
41
- if (!fileName) return null;
42
- const filePath = path.join(VARIANTS_DIR, fileName);
43
- try {
44
- return fs.readFileSync(filePath, 'utf8');
45
- } catch (_error) {
46
- return null;
47
- }
48
- }
49
-
50
- function readSkillVariantForIntegrationMode(integrationMode) {
51
- return readSkillVariantContent(variantNameForIntegrationMode(integrationMode));
52
- }
53
-
54
- module.exports = {
55
- VARIANT_FILES,
56
- variantNameForIntegrationMode,
57
- readSkillVariantContent,
58
- readSkillVariantForIntegrationMode
59
- };
1
+ 'use strict';
2
+
3
+ // C-001 (docs/prd-cli-and-plugin-fixes.md): mode-aware empirical-memory Skill content.
4
+ //
5
+ // The plugin marketplace (mattgauzza/empirical-memory-plugin) is the source of truth for Skill
6
+ // content, but its manifest schema (see meta/manifests.json / build-manifests.mjs in that repo)
7
+ // has no field that selects among sibling SKILL.*.md files at install time -- every host's native
8
+ // plugin mechanism resolves a plugin's skill from the literal filename `SKILL.md`. So the
9
+ // marketplace always ships the "both" variant as SKILL.md, and this CLI is what actually makes
10
+ // the user's chosen integration mode matter: it vendors its own copy of all three variants and
11
+ // overwrites the on-disk SKILL.md for each host after a native plugin install/update (see
12
+ // nativePlugin.js's applySkillModeVariant) and after a Kit sync (see skillSync.js).
13
+ //
14
+ // These files are copied from, and must be kept in sync with,
15
+ // M:\Projects\empirical-memory-plugin\skills\empirical-memory\SKILL.{mcp,cli,both}.md. There is no
16
+ // automated sync between the two repos; a human updates both when skill content changes.
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const VARIANTS_DIR = path.join(__dirname, 'skill-variants');
21
+
22
+ const VARIANT_FILES = {
23
+ mcp: 'SKILL.mcp.md',
24
+ cli: 'SKILL.cli.md',
25
+ both: 'SKILL.both.md'
26
+ };
27
+
28
+ // installWizard.js's `integrationMode`: 1 = MCP, 2 = CLI-direct, 3 = both.
29
+ function variantNameForIntegrationMode(integrationMode) {
30
+ if (integrationMode === 2) return 'cli';
31
+ if (integrationMode === 3) return 'both';
32
+ return 'mcp';
33
+ }
34
+
35
+ // Returns the variant's markdown content, or null if the vendored file is missing (e.g. a stale
36
+ // CLI release published before a variant existed). Never throws -- callers must treat a missing
37
+ // variant as "leave whatever is already installed" per the PRD's own risk mitigation, not as a
38
+ // fatal error.
39
+ function readSkillVariantContent(variantName) {
40
+ const fileName = VARIANT_FILES[variantName];
41
+ if (!fileName) return null;
42
+ const filePath = path.join(VARIANTS_DIR, fileName);
43
+ try {
44
+ return fs.readFileSync(filePath, 'utf8');
45
+ } catch (_error) {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function readSkillVariantForIntegrationMode(integrationMode) {
51
+ return readSkillVariantContent(variantNameForIntegrationMode(integrationMode));
52
+ }
53
+
54
+ module.exports = {
55
+ VARIANT_FILES,
56
+ variantNameForIntegrationMode,
57
+ readSkillVariantContent,
58
+ readSkillVariantForIntegrationMode
59
+ };
@@ -78,11 +78,13 @@ async function withCliAnalyticsContext(context, fn) {
78
78
  }
79
79
  }
80
80
 
81
- async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl) {
81
+ async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters) {
82
82
  const queryUrl = new URL('/api/v1/memories/query', baseUrl).toString();
83
+ const body = { query: match, topK: 5 };
84
+ if (filters && Object.keys(filters).length) body.filters = filters;
83
85
  let response;
84
86
  try {
85
- response = await httpJson(queryUrl, 'POST', { query: match, topK: 5 }, buildApiAuthHeaders(accessToken));
87
+ response = await httpJson(queryUrl, 'POST', body, buildApiAuthHeaders(accessToken));
86
88
  } catch (error) {
87
89
  return { found: false, reason: 'error', message: error.message };
88
90
  }
@@ -103,6 +105,19 @@ async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl) {
103
105
 
104
106
  scored.sort((a, b) => b.score - a.score);
105
107
 
108
+ // A caller-supplied filter (e.g. --tags) that narrows the server-side result set to exactly
109
+ // one memory is already a stronger disambiguation signal than the semantic score - trust it
110
+ // outright rather than falling through to the score check below. This matters because Atlas
111
+ // vector search has a real async indexing lag: a memory created moments ago can come back
112
+ // with score 0 (unindexed, not "not this one") even when a tag filter already proves it is
113
+ // the only candidate. Fixes a real bug found 2026-08-25 where delete --match failed on a
114
+ // memory `query` found instantly, because delete had no way to trust a filtered, unambiguous
115
+ // result over a stale/zero semantic score.
116
+ if (filters && Object.keys(filters).length && results.length === 1) {
117
+ const only = scored[0];
118
+ return { found: true, memory: { id: only.id, summary: only.summary, score: only.score } };
119
+ }
120
+
106
121
  const top = scored[0];
107
122
  const second = scored[1];
108
123
 
@@ -117,6 +132,23 @@ async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl) {
117
132
  return { found: true, memory: { id: top.id, summary: top.summary, score: top.score } };
118
133
  }
119
134
 
135
+ // --match resolution depends on Atlas vector search, which has a real async indexing lag:
136
+ // a memory created moments ago can be invisible to $vectorSearch even though it is already
137
+ // findable by tag. Letting --match commands pass --tags narrows the candidate pool directly
138
+ // instead of hoping semantic ranking surfaces the right one - the fix for a real bug found
139
+ // 2026-08-25 where delete --match returned unrelated score-0 candidates for a memory that
140
+ // `query` found instantly. Deliberately --tags (plural) only, not --category: `delete` already
141
+ // uses singular --tag/--category to mean "bulk delete everything in this category", so adding
142
+ // --category here would silently reroute a --match delete into a bulk delete instead of
143
+ // narrowing it - --tags never collides with that bulk-mode gate.
144
+ function parseMatchFilters(args) {
145
+ const tagsRaw = parseOption(args, '--tags', '');
146
+ const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
147
+ const filters = {};
148
+ if (tags.length) filters.tags = tags;
149
+ return filters;
150
+ }
151
+
120
152
  function printMemoryResolveError(result) {
121
153
  if (result.reason === 'no_results') {
122
154
  warn(JSON.stringify({ error: true, code: 'no_results', message: 'No memories matched the query.' }));
@@ -340,12 +372,12 @@ async function runMemoryNote(args) {
340
372
  const minScore = Number(parseOption(args, '--min-score', '0.35'));
341
373
 
342
374
  if (!text) {
343
- warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--min-score 0.35]');
375
+ warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--tags <t1,t2>] [--min-score 0.35]');
344
376
  process.exit(1);
345
377
  }
346
378
 
347
379
  if (!match && !memoryId) {
348
- warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>)');
380
+ warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--tags <t1,t2>]');
349
381
  process.exit(1);
350
382
  }
351
383
 
@@ -354,7 +386,8 @@ async function runMemoryNote(args) {
354
386
 
355
387
  let targetId = memoryId;
356
388
  if (!targetId) {
357
- const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
389
+ const filters = parseMatchFilters(args);
390
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
358
391
  if (!result.found) {
359
392
  printMemoryResolveError(result);
360
393
  process.exit(1);
@@ -383,12 +416,12 @@ async function runMemoryUpdate(args) {
383
416
  const minScore = Number(parseOption(args, '--min-score', '0.35'));
384
417
 
385
418
  if (!patchJson && !jsonFile) {
386
- warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>)');
419
+ warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--tags <t1,t2>]');
387
420
  process.exit(1);
388
421
  }
389
422
 
390
423
  if (!match && !memoryId) {
391
- warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>)');
424
+ warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--tags <t1,t2>]');
392
425
  process.exit(1);
393
426
  }
394
427
 
@@ -414,7 +447,8 @@ async function runMemoryUpdate(args) {
414
447
 
415
448
  let targetId = memoryId;
416
449
  if (!targetId) {
417
- const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
450
+ const filters = parseMatchFilters(args);
451
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
418
452
  if (!result.found) {
419
453
  printMemoryResolveError(result);
420
454
  process.exit(1);
@@ -484,7 +518,7 @@ async function runMemoryDelete(args) {
484
518
  const confirm = parseFlag(args, '--confirm');
485
519
 
486
520
  if (!match && !memoryId) {
487
- warn('Usage: empirical memory delete (--match <query> | --memory-id <id>) [--confirm] [--min-score 0.35]');
521
+ warn('Usage: empirical memory delete (--match <query> | --memory-id <id>) [--tags <t1,t2>] [--confirm] [--min-score 0.35]');
488
522
  warn(' or: empirical memory delete (--tag <t> | --category <c> | --all) --confirm');
489
523
  process.exit(1);
490
524
  }
@@ -496,7 +530,8 @@ async function runMemoryDelete(args) {
496
530
  let targetSummary = '';
497
531
 
498
532
  if (!targetId) {
499
- const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
533
+ const filters = parseMatchFilters(args);
534
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
500
535
  if (!result.found) {
501
536
  if (!confirm && (result.reason === 'ambiguous' || result.reason === 'low_score')) {
502
537
  // Dry-run: return candidates info
package/lib/skillPaths.js CHANGED
@@ -1,25 +1,25 @@
1
- 'use strict';
2
-
3
- // Per-agent skill file path conventions, mirrored exactly from the hardcoded paths already used
4
- // by installers/claude.js, installers/codex.js, installers/copilot.js (for the single
5
- // empirical-memory skill) — parameterized by skill name instead of hardcoded to it.
6
- //
7
- // Split out of skillSync.js into its own dependency-free module so both skillSync.js and
8
- // installers/nativePlugin.js (C-001's mode-aware Skill overwrite) can use the same path
9
- // convention without creating a require cycle: skillSync.js -> installers/detect.js ->
10
- // installers/claude.js|codex.js|copilot.js -> installers/nativePlugin.js would loop straight
11
- // back into skillSync.js if nativePlugin.js required it directly.
12
- const os = require('os');
13
- const path = require('path');
14
-
15
- function skillFilePath(targetId, skillName) {
16
- const home = os.homedir();
17
- if (targetId === 'claude') return path.join(home, '.claude', 'skills', skillName, 'SKILL.md');
18
- if (targetId === 'codex') return path.join(home, '.agents', 'skills', skillName, 'SKILL.md');
19
- if (targetId === 'copilot') return path.join(home, '.copilot', 'skills', skillName, 'SKILL.md');
20
- return null;
21
- }
22
-
23
- module.exports = {
24
- skillFilePath
25
- };
1
+ 'use strict';
2
+
3
+ // Per-agent skill file path conventions, mirrored exactly from the hardcoded paths already used
4
+ // by installers/claude.js, installers/codex.js, installers/copilot.js (for the single
5
+ // empirical-memory skill) — parameterized by skill name instead of hardcoded to it.
6
+ //
7
+ // Split out of skillSync.js into its own dependency-free module so both skillSync.js and
8
+ // installers/nativePlugin.js (C-001's mode-aware Skill overwrite) can use the same path
9
+ // convention without creating a require cycle: skillSync.js -> installers/detect.js ->
10
+ // installers/claude.js|codex.js|copilot.js -> installers/nativePlugin.js would loop straight
11
+ // back into skillSync.js if nativePlugin.js required it directly.
12
+ const os = require('os');
13
+ const path = require('path');
14
+
15
+ function skillFilePath(targetId, skillName) {
16
+ const home = os.homedir();
17
+ if (targetId === 'claude') return path.join(home, '.claude', 'skills', skillName, 'SKILL.md');
18
+ if (targetId === 'codex') return path.join(home, '.agents', 'skills', skillName, 'SKILL.md');
19
+ if (targetId === 'copilot') return path.join(home, '.copilot', 'skills', skillName, 'SKILL.md');
20
+ return null;
21
+ }
22
+
23
+ module.exports = {
24
+ skillFilePath
25
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empirical-gauzza/cli",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
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",