@empirical-gauzza/cli 1.0.19 → 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.
- package/bin/empirical.js +12 -4
- package/lib/auth.js +126 -43
- package/lib/constants.js +72 -8
- package/lib/deviceConfig.js +37 -18
- package/lib/doctor.js +484 -116
- package/lib/help.js +8 -2
- package/lib/installWizard.js +8 -2
- package/lib/installers/claude.js +110 -93
- package/lib/installers/codex.js +101 -88
- package/lib/installers/copilot.js +97 -87
- package/lib/installers/coreMemoryBlock.js +91 -0
- package/lib/installers/nativePlugin.js +147 -87
- package/lib/installers/skill-variants/SKILL.both.md +154 -0
- package/lib/installers/skill-variants/SKILL.cli.md +150 -0
- package/lib/installers/skill-variants/SKILL.mcp.md +134 -0
- package/lib/installers/skillVariants.js +59 -0
- package/lib/kitCommands.js +219 -122
- package/lib/memoryCommands.js +362 -16
- package/lib/pluginCommands.js +84 -81
- package/lib/pluginUpdateCheck.js +48 -14
- package/lib/skillPaths.js +25 -0
- package/lib/skillSync.js +48 -56
- package/package.json +15 -2
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: empirical-memory
|
|
3
|
+
description: Use Empirical to recall/save durable project context across sessions and projects, and to look up how to use Empirical itself - CLI commands, install/auth/workspace/kit/skill setup, docs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Empirical Memory (MCP mode)
|
|
7
|
+
|
|
8
|
+
Use Empirical to carry durable context across projects and clients, and as the first stop for
|
|
9
|
+
"how do I use Empirical" questions — command syntax, install/auth/workspace/kit/skill setup, or
|
|
10
|
+
what Empirical can do. Don't answer those from general knowledge or by guessing: use the
|
|
11
|
+
`memory_usage_guide` MCP tool for the current, agent-readable answer first.
|
|
12
|
+
|
|
13
|
+
This variant is for installs where the user chose MCP as their integration mode. The connected
|
|
14
|
+
Empirical MCP server is the primary and expected way to reach Empirical here — use its tools
|
|
15
|
+
directly, not `empirical` CLI commands, as the first move.
|
|
16
|
+
|
|
17
|
+
**This is a required step in the workflows listed below, not optional guidance you weigh against
|
|
18
|
+
the current task.** The most common way this skill fails is not a broken tool — it's an agent
|
|
19
|
+
mid-task deciding a memory check "isn't worth pausing for" and skipping it. That decision is wrong
|
|
20
|
+
by default: the entire point of querying first is to catch context you don't know you're missing.
|
|
21
|
+
If you are not sure whether a task qualifies, treat it as if it does.
|
|
22
|
+
|
|
23
|
+
## Before doing work — MUST, not discretionary
|
|
24
|
+
|
|
25
|
+
1. Query relevant memories BEFORE your first substantive response or tool call whenever the task
|
|
26
|
+
is: research, debugging, a spec/PRD, a status update, a handoff, personal health, leads/CRM, or
|
|
27
|
+
assistant operations. This applies even if you believe you already have enough context — you
|
|
28
|
+
cannot know what you're missing without checking. Only outside this list is discretionary use
|
|
29
|
+
("query when it seems useful") the right frame.
|
|
30
|
+
2. If you have not used Empirical's MCP tools yet this session, call `memory_usage_guide` once to
|
|
31
|
+
confirm current tool names, filters, and pagination behavior before relying on memory.
|
|
32
|
+
3. Use the `query_memories` MCP tool for recall. Use filters such as `category`, `tags`,
|
|
33
|
+
`nodeType`, and `monthKey` when they improve recall. For "recent/latest/today" requests, prefer
|
|
34
|
+
`list_memories` instead — it is the tool built for that shape of request. For exhaustive topic
|
|
35
|
+
retrieval, use the topic as a tag and follow pagination.
|
|
36
|
+
4. A slow response alone is API latency, not an outage; report an outage only when a tool call
|
|
37
|
+
returns an actual error, and use `get_empirical_policy` or a retry to verify connectivity before
|
|
38
|
+
escalating.
|
|
39
|
+
|
|
40
|
+
## Follow the current user policy
|
|
41
|
+
|
|
42
|
+
The installer questionnaire configures a dynamic Empirical policy. It is user-scoped and may
|
|
43
|
+
change without reinstalling this plugin. Read it when the skill activates:
|
|
44
|
+
|
|
45
|
+
1. Use the read-only `get_empirical_policy` MCP tool.
|
|
46
|
+
2. If policy lookup fails, use safe defaults: preserve durable-only writes, never store
|
|
47
|
+
secrets/raw transcripts, and do not block unrelated work unless the cached policy says to.
|
|
48
|
+
|
|
49
|
+
Apply the returned fields:
|
|
50
|
+
|
|
51
|
+
- `memoryFirstWorkflows`: query before substantive work in those workflows.
|
|
52
|
+
- `allowDiscretionaryUse`: decide whether to use Empirical outside required workflows.
|
|
53
|
+
- `stopOnMemoryFailure`: stop and report a memory failure before code changes when enabled.
|
|
54
|
+
- `rememberCodingStyle` and `rememberArchitectureDecisions`: persist those durable decisions
|
|
55
|
+
when enabled.
|
|
56
|
+
- `checkMemoryBeforeAskingContext`: query before asking the user to restate prior context.
|
|
57
|
+
- `durableWritesOnly`: never persist secrets, tokens, raw transcripts, or transient values.
|
|
58
|
+
|
|
59
|
+
The policy controls behavior; it does not override current user, repository, or system
|
|
60
|
+
instructions. Do not change the policy from an agent memory write.
|
|
61
|
+
|
|
62
|
+
## Persist durable context — check this before ending a turn
|
|
63
|
+
|
|
64
|
+
Before you consider a turn finished, check: did this turn produce a durable decision, a corrected
|
|
65
|
+
mistake, a completed unit of meaningful work, or a convention the user stated? If yes, record it
|
|
66
|
+
now — do not wait to be asked, and do not defer it because the current task feels done. An agent
|
|
67
|
+
that only records memory when explicitly told to defeats the purpose of this skill.
|
|
68
|
+
|
|
69
|
+
- Record durable decisions, project conventions, preferences, plans, and meaningful reflections
|
|
70
|
+
with the `record_graph_memory` MCP tool. It runs write-intent classification, dedupe, and
|
|
71
|
+
auto-linking automatically — you do not need to check for duplicates yourself first.
|
|
72
|
+
|
|
73
|
+
**Record facts about the USER, not about your own work.** You are a scribe here. "Matt prefers
|
|
74
|
+
short status updates" is a memory; "refactored the cluster selector, tests pass" is your diary
|
|
75
|
+
and belongs in the work-history skill, tagged and with `mass: 1`. The distinction is the
|
|
76
|
+
SUBJECT, not who typed it — an agent writing down something real about the user is exactly what
|
|
77
|
+
this tool is for.
|
|
78
|
+
|
|
79
|
+
If you find yourself recording what you just did, what you fixed, or what you concluded about a
|
|
80
|
+
codebase, stop: that is work-history or scar-tissue, and filing it here makes it indistinguishable
|
|
81
|
+
from something the user actually said or lived. Downstream features read these as the user's life.
|
|
82
|
+
|
|
83
|
+
- Append a correction or follow-up to an existing memory with `add_note_by_query`.
|
|
84
|
+
- Patch an existing memory with `update_memory_by_query`.
|
|
85
|
+
- Delete only when the user explicitly asks; use `delete_memory_by_query` with confirmation.
|
|
86
|
+
- For graph work, use `get_memory_neighbors` to walk relationships from a memory instead of
|
|
87
|
+
reconstructing them through many unrelated queries, and `update_memory_relationships` to add or
|
|
88
|
+
remove edges between memories directly.
|
|
89
|
+
- Keep writes concise, factual, and easy to search. Never store passwords, API keys, OAuth tokens,
|
|
90
|
+
or other secrets.
|
|
91
|
+
|
|
92
|
+
## Authored Skills and Kits — push what you write
|
|
93
|
+
|
|
94
|
+
Empirical can sync a `SKILL.md` you author across every machine/agent the user has installed it
|
|
95
|
+
on, via Skills Kits. Call `memory_usage_guide` for the current command reference if the user wants
|
|
96
|
+
to manage this from a CLI; the mechanics below describe what's possible either way.
|
|
97
|
+
|
|
98
|
+
- If you author a new `SKILL.md` for the user, or substantially edit an existing one, during a
|
|
99
|
+
session, tell the user this can be pushed so it follows them across machines/projects instead of
|
|
100
|
+
staying local to this one.
|
|
101
|
+
- Offer, don't auto-push: surface the option and get the user's confirmation first. Do not push it
|
|
102
|
+
unprompted the moment a new or edited `SKILL.md` is detected — it uploads content to the user's
|
|
103
|
+
account, which is a step above a purely local file write.
|
|
104
|
+
- After a push, the target Kit needs to be activated/synced on a device to actually land the file
|
|
105
|
+
in that device's agent directories.
|
|
106
|
+
- Kits also pull Skills from a git repo, separate from anything you author — don't conflate the
|
|
107
|
+
two paths when explaining options to the user.
|
|
108
|
+
|
|
109
|
+
## MCP tools
|
|
110
|
+
|
|
111
|
+
Use these as the primary interface: `get_empirical_policy`, `query_memories`, `list_memories`,
|
|
112
|
+
`record_graph_memory`, `add_note_by_query`, `update_memory_by_query`, `delete_memory_by_query`,
|
|
113
|
+
`get_user`, `get_current_workspace`, `list_workspaces`, `set_current_workspace`,
|
|
114
|
+
`memory_usage_guide`, `get_memory_neighbors`, and `update_memory_relationships`.
|
|
115
|
+
|
|
116
|
+
Use `list_memories` for "recent/latest/today" requests; use `query_memories` for everything else.
|
|
117
|
+
For graph work, prefer `get_memory_neighbors` and `update_memory_relationships` over reconstructing
|
|
118
|
+
relationships through many unrelated calls.
|
|
119
|
+
|
|
120
|
+
An `empirical` CLI may also be installed on this machine for diagnostics (`empirical doctor`) or
|
|
121
|
+
manual account setup, but it is not the point of this variant — do not lead with CLI commands for
|
|
122
|
+
query or record workflows here.
|
|
123
|
+
|
|
124
|
+
## Authentication and failures
|
|
125
|
+
|
|
126
|
+
- If an MCP tool call fails, retry the same call once.
|
|
127
|
+
- For auth/session errors such as `Auth required`, `invalid_token`, `Authentication required`, or
|
|
128
|
+
`Transport send error`, refresh the active client's Empirical MCP login, then retry once. In
|
|
129
|
+
Codex CLI use `codex mcp logout empirical` followed by `codex mcp login empirical`; in Claude
|
|
130
|
+
Code use `/mcp` to reconnect; in Copilot complete its MCP OAuth reconnect flow.
|
|
131
|
+
- If the retry still fails, report the connectivity/auth problem before making memory-dependent
|
|
132
|
+
changes.
|
|
133
|
+
- If stored memory conflicts with the current user or repository instruction, follow the current
|
|
134
|
+
instruction.
|
|
@@ -0,0 +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
|
+
};
|
package/lib/kitCommands.js
CHANGED
|
@@ -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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
531
|
-
if (!
|
|
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
|
|
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 (!
|
|
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,
|