@empirical-gauzza/cli 1.0.18 → 1.0.20

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.
@@ -14,9 +14,30 @@ const { httpJson, printApiError } = require('./http');
14
14
  const { parseOption, parseFlag } = require('./env');
15
15
  const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
16
16
  const { withCliAnalyticsContext } = require('./memoryCommands');
17
- const { readDeviceConfig, writeDeviceConfig, ensureDeviceId, readPendingReview } = require('./deviceConfig');
17
+ const { readDeviceConfig, writeDeviceConfig, ensureDeviceId, readPendingReview, readIntegrationMode } = require('./deviceConfig');
18
18
  const { performSync, printSyncSummary } = require('./skillSync');
19
19
  const { registerScheduledSync, deregisterScheduledSync } = require('./scheduledSync');
20
+ const { detectInstallTargets } = require('./installers/detect');
21
+ const { applySkillModeVariant } = require('./installers/nativePlugin');
22
+
23
+ // C-001: `kit use`/`kit sync` are the second distribution path the PRD names for mode-aware
24
+ // Skill content. skillSync.js's generalized Kit writer deliberately never writes empirical-memory
25
+ // (see filterOutEmpiricalMemory in skillSync.js -- the server's resolved builtin for that name is
26
+ // a placeholder, not the real plugin content), so a Kit sync alone would never re-apply a mode
27
+ // change. Refresh it here instead, right after every sync, so `kit sync`/`kit use` -- not just
28
+ // re-running the full installer -- keeps empirical-memory's Skill content matching whatever
29
+ // integration mode the user last chose in `empirical install`.
30
+ function refreshEmpiricalMemorySkillForCurrentMode({ dryRun, quiet }) {
31
+ if (dryRun) return;
32
+ const integrationMode = readIntegrationMode();
33
+ for (const target of detectInstallTargets()) {
34
+ try {
35
+ applySkillModeVariant(target.id, integrationMode);
36
+ } catch (error) {
37
+ if (!quiet) warn(`Warning: could not refresh empirical-memory skill content for ${target.label}: ${error.message}`);
38
+ }
39
+ }
40
+ }
20
41
 
21
42
  // US-016: nag banner surfaced at the START of every interactive (non-`--quiet`) `kit`
22
43
  // subcommand — informational only, never blocks the subcommand's own work. `kit sync` is the
@@ -143,7 +164,7 @@ async function runKitRemoveSource(args) {
143
164
  log(JSON.stringify(result));
144
165
  }
145
166
 
146
- async function runKitList(args) {
167
+ async function runKitList(args) {
147
168
  const accessToken = requireAccessToken();
148
169
  const baseUrl = resolveApiBaseUrl();
149
170
  const url = new URL('/api/v1/kits', baseUrl).toString();
@@ -157,112 +178,166 @@ async function runKitList(args) {
157
178
  }
158
179
 
159
180
  const result = (response && response.data) ? response.data : response;
160
- log(JSON.stringify(result));
161
- }
162
-
163
- async function fetchInventoryJson(url, accessToken) {
164
- try {
165
- return await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
166
- } catch (error) {
167
- printApiError(error);
168
- process.exit(1);
169
- }
170
- }
171
-
172
- async function listOwnedKits(baseUrl, accessToken) {
173
- const response = await fetchInventoryJson(new URL('/api/v1/kits', baseUrl).toString(), accessToken);
174
- return response?.data?.kits || response?.kits || [];
175
- }
176
-
177
- async function runSkillList(args) {
178
- const accessToken = requireAccessToken();
179
- const baseUrl = resolveApiBaseUrl();
180
- const kitFilter = parseOption(args, '--kit', '');
181
- const kits = await listOwnedKits(baseUrl, accessToken);
182
- const selectedKits = kitFilter
183
- ? kits.filter((kit) => kit.kitId === kitFilter || kit.name === kitFilter)
184
- : kits;
185
- const skills = [];
186
-
187
- for (const kit of selectedKits) {
188
- const response = await fetchInventoryJson(
189
- new URL(`/api/v1/kits/${encodeURIComponent(kit.kitId)}/resolved`, baseUrl).toString(),
190
- accessToken
191
- );
192
- const kitSkills = response?.data?.skills || response?.skills || [];
193
- for (const skill of kitSkills) {
194
- skills.push({
195
- ...skill,
196
- kitId: kit.kitId,
197
- kitName: kit.name,
198
- isDefault: Boolean(kit.isDefault)
199
- });
200
- }
201
- }
202
-
203
- log(JSON.stringify({ skills }));
204
- }
205
-
206
- async function runDeviceList() {
207
- const accessToken = requireAccessToken();
208
- const baseUrl = resolveApiBaseUrl();
209
- const kits = await listOwnedKits(baseUrl, accessToken);
210
- const devicesById = new Map();
211
-
212
- for (const kit of kits.filter((candidate) => !candidate.isDefault)) {
213
- const response = await fetchInventoryJson(
214
- new URL(`/api/v1/kits/${encodeURIComponent(kit.kitId)}/devices`, baseUrl).toString(),
215
- accessToken
216
- );
217
- const devices = response?.data?.devices || response?.devices || [];
218
- for (const device of devices) {
219
- const existing = devicesById.get(device.deviceId) || {
220
- ...device,
221
- kits: []
222
- };
223
- existing.kits.push({
224
- kitId: kit.kitId,
225
- kitName: kit.name,
226
- active: Boolean(device.active)
227
- });
228
- devicesById.set(device.deviceId, existing);
229
- }
230
- }
231
-
232
- log(JSON.stringify({ devices: [...devicesById.values()] }));
233
- }
234
-
235
- async function runDeviceShow(args) {
236
- const deviceId = args[0] && !args[0].startsWith('--') ? args[0] : '';
237
- if (!deviceId) {
238
- warn('Usage: empirical device show <deviceId>');
239
- process.exit(1);
240
- }
241
-
242
- const accessToken = requireAccessToken();
243
- const baseUrl = resolveApiBaseUrl();
244
- const response = await fetchInventoryJson(
245
- new URL(`/api/v1/devices/${encodeURIComponent(deviceId)}/resolved`, baseUrl).toString(),
246
- accessToken
247
- );
248
- const data = response?.data || response || {};
249
- log(JSON.stringify({ deviceId, ...data }));
250
- }
251
-
252
- async function runDeviceCommand(subArgs) {
253
- const sub = subArgs[0];
254
- const rest = subArgs.slice(1);
255
- return withCliAnalyticsContext(
256
- { command: `device.${sub || 'unknown'}`, subcommand: sub || 'unknown' },
257
- async () => {
258
- if (sub === 'list') return runDeviceList(rest);
259
- if (sub === 'show') return runDeviceShow(rest);
260
- warn('Unknown device subcommand: ' + (sub || '(none)'));
261
- warn('Usage: empirical device <list|show> [options]');
262
- process.exit(1);
263
- }
264
- );
265
- }
181
+ log(JSON.stringify(result));
182
+ }
183
+
184
+ async function fetchInventoryJson(url, accessToken) {
185
+ try {
186
+ return await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
187
+ } catch (error) {
188
+ printApiError(error);
189
+ process.exit(1);
190
+ }
191
+ }
192
+
193
+ async function listOwnedKits(baseUrl, accessToken) {
194
+ const response = await fetchInventoryJson(new URL('/api/v1/kits', baseUrl).toString(), accessToken);
195
+ return response?.data?.kits || response?.kits || [];
196
+ }
197
+
198
+ // C-006: fetch every resolved skill across the caller's kits (or just `kitFilter`'s kit), each
199
+ // annotated with which kit it came from. Shared by `skill list` (the human-facing listing) and
200
+ // `resolveAuthoredSkillId` (name -> authoredSkillId lookup for `skill history`/`skill rollback`)
201
+ // so both read the exact same data — a name that resolves for one resolves the same way for
202
+ // the other.
203
+ async function fetchAllResolvedSkills(baseUrl, accessToken, kitFilter) {
204
+ const kits = await listOwnedKits(baseUrl, accessToken);
205
+ const selectedKits = kitFilter
206
+ ? kits.filter((kit) => kit.kitId === kitFilter || kit.name === kitFilter)
207
+ : kits;
208
+ const skills = [];
209
+
210
+ for (const kit of selectedKits) {
211
+ const response = await fetchInventoryJson(
212
+ new URL(`/api/v1/kits/${encodeURIComponent(kit.kitId)}/resolved`, baseUrl).toString(),
213
+ accessToken
214
+ );
215
+ const kitSkills = response?.data?.skills || response?.skills || [];
216
+ for (const skill of kitSkills) {
217
+ skills.push({
218
+ ...skill,
219
+ kitId: kit.kitId,
220
+ kitName: kit.name,
221
+ isDefault: Boolean(kit.isDefault)
222
+ });
223
+ }
224
+ }
225
+
226
+ return skills;
227
+ }
228
+
229
+ async function runSkillList(args) {
230
+ const accessToken = requireAccessToken();
231
+ const baseUrl = resolveApiBaseUrl();
232
+ const kitFilter = parseOption(args, '--kit', '');
233
+ const skills = await fetchAllResolvedSkills(baseUrl, accessToken, kitFilter);
234
+
235
+ // C-006: name, resolvedVersion (its version) and authoredSkillId are the fields that matter
236
+ // for finding/using a skill again later — authoredSkillId in particular is the ONLY way to
237
+ // reach `skill history`/`skill rollback`, and until now it was never shown here (only ever
238
+ // printed once, in the JSON blob `skill push` prints at push time). Every other field from
239
+ // the resolved payload is still included; this just guarantees those three are always present
240
+ // per row, even for skill types (git_custom/builtin) where authoredSkillId is absent.
241
+ const rows = skills.map((skill) => ({
242
+ name: skill.name,
243
+ resolvedVersion: skill.resolvedVersion,
244
+ authoredSkillId: skill.authoredSkillId || null,
245
+ ...skill
246
+ }));
247
+
248
+ log(JSON.stringify({ skills: rows }));
249
+ }
250
+
251
+ // C-006: `skill history`/`skill rollback` historically required the raw authoredSkillId, which
252
+ // the user only ever sees once (the JSON `skill push` prints at push time). This lets either
253
+ // command accept the skill's NAME instead, resolved against the same data `skill list` shows.
254
+ // Direct authoredSkillId matches win outright (no ambiguity possible — it's a unique id); name
255
+ // matches must be unique among authored skills (only authored skills have an authoredSkillId,
256
+ // hence history/rollback) or this throws a clear, actionable error.
257
+ async function resolveAuthoredSkillId(identifier, baseUrl, accessToken, kitFilter) {
258
+ const skills = await fetchAllResolvedSkills(baseUrl, accessToken, kitFilter);
259
+
260
+ const directMatch = skills.find((skill) => skill.authoredSkillId === identifier);
261
+ if (directMatch) return directMatch.authoredSkillId;
262
+
263
+ const nameMatches = skills.filter((skill) => skill.authoredSkillId && skill.name === identifier);
264
+
265
+ if (nameMatches.length === 1) return nameMatches[0].authoredSkillId;
266
+
267
+ if (nameMatches.length > 1) {
268
+ const locations = nameMatches
269
+ .map((skill) => `${skill.kitName || skill.kitId} (${skill.authoredSkillId})`)
270
+ .join(', ');
271
+ throw new Error(
272
+ `"${identifier}" matches ${nameMatches.length} authored skills: ${locations}. Pass the authoredSkillId instead, or narrow with --kit.`
273
+ );
274
+ }
275
+
276
+ throw new Error(
277
+ `No authored skill named or with id "${identifier}" was found${kitFilter ? ` in kit "${kitFilter}"` : ''}. Run 'empirical skill list' to see authoredSkillId per skill.`
278
+ );
279
+ }
280
+
281
+ async function runDeviceList() {
282
+ const accessToken = requireAccessToken();
283
+ const baseUrl = resolveApiBaseUrl();
284
+ const kits = await listOwnedKits(baseUrl, accessToken);
285
+ const devicesById = new Map();
286
+
287
+ for (const kit of kits.filter((candidate) => !candidate.isDefault)) {
288
+ const response = await fetchInventoryJson(
289
+ new URL(`/api/v1/kits/${encodeURIComponent(kit.kitId)}/devices`, baseUrl).toString(),
290
+ accessToken
291
+ );
292
+ const devices = response?.data?.devices || response?.devices || [];
293
+ for (const device of devices) {
294
+ const existing = devicesById.get(device.deviceId) || {
295
+ ...device,
296
+ kits: []
297
+ };
298
+ existing.kits.push({
299
+ kitId: kit.kitId,
300
+ kitName: kit.name,
301
+ active: Boolean(device.active)
302
+ });
303
+ devicesById.set(device.deviceId, existing);
304
+ }
305
+ }
306
+
307
+ log(JSON.stringify({ devices: [...devicesById.values()] }));
308
+ }
309
+
310
+ async function runDeviceShow(args) {
311
+ const deviceId = args[0] && !args[0].startsWith('--') ? args[0] : '';
312
+ if (!deviceId) {
313
+ warn('Usage: empirical device show <deviceId>');
314
+ process.exit(1);
315
+ }
316
+
317
+ const accessToken = requireAccessToken();
318
+ const baseUrl = resolveApiBaseUrl();
319
+ const response = await fetchInventoryJson(
320
+ new URL(`/api/v1/devices/${encodeURIComponent(deviceId)}/resolved`, baseUrl).toString(),
321
+ accessToken
322
+ );
323
+ const data = response?.data || response || {};
324
+ log(JSON.stringify({ deviceId, ...data }));
325
+ }
326
+
327
+ async function runDeviceCommand(subArgs) {
328
+ const sub = subArgs[0];
329
+ const rest = subArgs.slice(1);
330
+ return withCliAnalyticsContext(
331
+ { command: `device.${sub || 'unknown'}`, subcommand: sub || 'unknown' },
332
+ async () => {
333
+ if (sub === 'list') return runDeviceList(rest);
334
+ if (sub === 'show') return runDeviceShow(rest);
335
+ warn('Unknown device subcommand: ' + (sub || '(none)'));
336
+ warn('Usage: empirical device <list|show> [options]');
337
+ process.exit(1);
338
+ }
339
+ );
340
+ }
266
341
 
267
342
  async function runKitAutosync(args) {
268
343
  const kitId = args[0] && !args[0].startsWith('--') ? args[0] : '';
@@ -398,6 +473,7 @@ async function runKitUse(args) {
398
473
  process.exit(1);
399
474
  }
400
475
  printSyncSummary(syncResult, quiet);
476
+ refreshEmpiricalMemorySkillForCurrentMode({ dryRun, quiet });
401
477
 
402
478
  // US-012: fetch this Kit's own autoSyncEnabled flag and reconcile the local scheduled task to
403
479
  // match it. A failure here is fetched best-effort — it must never turn a successful `kit use`
@@ -434,6 +510,7 @@ async function runKitSync(args) {
434
510
  }
435
511
 
436
512
  printSyncSummary(result, quiet);
513
+ refreshEmpiricalMemorySkillForCurrentMode({ dryRun, quiet });
437
514
  }
438
515
 
439
516
  async function runKitCommand(subArgs) {
@@ -527,14 +604,24 @@ async function runSkillPush(args) {
527
604
  }
528
605
 
529
606
  async function runSkillHistory(args) {
530
- const authoredSkillId = args[0] && !args[0].startsWith('--') ? args[0] : '';
531
- if (!authoredSkillId) {
532
- warn('Usage: empirical skill history <authoredSkillId>');
607
+ const identifier = args[0] && !args[0].startsWith('--') ? args[0] : '';
608
+ if (!identifier) {
609
+ warn('Usage: empirical skill history <authoredSkillId|name> [--kit <kitId>]');
533
610
  process.exit(1);
534
611
  }
535
612
 
613
+ const kitFilter = parseOption(args, '--kit', '');
536
614
  const accessToken = requireAccessToken();
537
615
  const baseUrl = resolveApiBaseUrl();
616
+
617
+ let authoredSkillId;
618
+ try {
619
+ authoredSkillId = await resolveAuthoredSkillId(identifier, baseUrl, accessToken, kitFilter);
620
+ } catch (error) {
621
+ warn(error.message);
622
+ process.exit(1);
623
+ }
624
+
538
625
  const url = new URL(
539
626
  `/api/v1/skills/${encodeURIComponent(authoredSkillId)}/history`,
540
627
  baseUrl
@@ -553,17 +640,27 @@ async function runSkillHistory(args) {
553
640
  }
554
641
 
555
642
  async function runSkillRollback(args) {
556
- const authoredSkillId = args[0] && !args[0].startsWith('--') ? args[0] : '';
643
+ const identifier = args[0] && !args[0].startsWith('--') ? args[0] : '';
557
644
  const toVersionRaw = parseOption(args, '--to-version', '');
558
645
  const toVersion = Number(toVersionRaw);
559
646
 
560
- if (!authoredSkillId || !toVersionRaw || !Number.isInteger(toVersion) || toVersion < 1) {
561
- warn('Usage: empirical skill rollback <authoredSkillId> --to-version <n>');
647
+ if (!identifier || !toVersionRaw || !Number.isInteger(toVersion) || toVersion < 1) {
648
+ warn('Usage: empirical skill rollback <authoredSkillId|name> --to-version <n> [--kit <kitId>]');
562
649
  process.exit(1);
563
650
  }
564
651
 
652
+ const kitFilter = parseOption(args, '--kit', '');
565
653
  const accessToken = requireAccessToken();
566
654
  const baseUrl = resolveApiBaseUrl();
655
+
656
+ let authoredSkillId;
657
+ try {
658
+ authoredSkillId = await resolveAuthoredSkillId(identifier, baseUrl, accessToken, kitFilter);
659
+ } catch (error) {
660
+ warn(error.message);
661
+ process.exit(1);
662
+ }
663
+
567
664
  const url = new URL(
568
665
  `/api/v1/skills/${encodeURIComponent(authoredSkillId)}/rollback`,
569
666
  baseUrl
@@ -588,13 +685,13 @@ async function runSkillCommand(subArgs) {
588
685
  return withCliAnalyticsContext(
589
686
  { command: `skill.${sub || 'unknown'}`, subcommand: sub || 'unknown' },
590
687
  async () => {
591
- if (sub === 'push') return runSkillPush(rest);
592
- if (sub === 'list') return runSkillList(rest);
688
+ if (sub === 'push') return runSkillPush(rest);
689
+ if (sub === 'list') return runSkillList(rest);
593
690
  if (sub === 'history') return runSkillHistory(rest);
594
691
  if (sub === 'rollback') return runSkillRollback(rest);
595
692
 
596
693
  warn('Unknown skill subcommand: ' + (sub || '(none)'));
597
- warn('Usage: empirical skill <list|push|history|rollback> [options]');
694
+ warn('Usage: empirical skill <list|push|history|rollback> [options]');
598
695
  process.exit(1);
599
696
  }
600
697
  );
@@ -609,11 +706,11 @@ module.exports = {
609
706
  runKitDevices,
610
707
  runKitDeactivate,
611
708
  runKitUse,
612
- runKitSync,
613
- runSkillList,
614
- runDeviceList,
615
- runDeviceShow,
616
- runDeviceCommand,
709
+ runKitSync,
710
+ runSkillList,
711
+ runDeviceList,
712
+ runDeviceShow,
713
+ runDeviceCommand,
617
714
  runKitCommand,
618
715
  runSkillPush,
619
716
  runSkillHistory,