@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
package/lib/pluginCommands.js
CHANGED
|
@@ -1,81 +1,84 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { log, warn, uiHeader, uiSuccess } = require('./ui');
|
|
4
|
-
const { detectInstallTargets } = require('./installers/detect');
|
|
5
|
-
const {
|
|
6
|
-
installNativePlugin,
|
|
7
|
-
OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo']
|
|
8
|
-
} = require('./installers/nativePlugin');
|
|
9
|
-
const { checkNativePluginUpdates } = require('./pluginUpdateCheck');
|
|
10
|
-
|
|
11
|
-
const SUPPORTED_TARGETS = ['codex', 'copilot', 'claude'];
|
|
12
|
-
|
|
13
|
-
function isPluginCommand(args = []) {
|
|
14
|
-
return args[0] === 'plugin';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function resolveTargets(targetArg) {
|
|
18
|
-
if (targetArg === 'all') return SUPPORTED_TARGETS;
|
|
19
|
-
if (targetArg && SUPPORTED_TARGETS.includes(targetArg)) return [targetArg];
|
|
20
|
-
if (targetArg) {
|
|
21
|
-
throw new Error(`Unsupported plugin target "${targetArg}". Use codex, copilot, claude, or all.`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const detected = detectInstallTargets().map((target) => target.id);
|
|
25
|
-
if (detected.length === 0) {
|
|
26
|
-
throw new Error('No supported CLI detected. Specify a target explicitly: codex, copilot, claude, or all.');
|
|
27
|
-
}
|
|
28
|
-
return detected;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function resolvePluginNames(args = []) {
|
|
32
|
-
const names = ['empirical-memory'];
|
|
33
|
-
if (args.includes('--all-optional') || args.includes('--work-history')) names.push('empirical-work-history');
|
|
34
|
-
if (args.includes('--all-optional') || args.includes('--scar-tissue')) names.push('empirical-scar-tissue');
|
|
35
|
-
if (args.includes('--all-optional') || args.includes('--todo')) names.push('empirical-todo');
|
|
36
|
-
return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function runPluginCommand(args = []) {
|
|
40
|
-
const action = args[0];
|
|
41
|
-
if (action === 'check') {
|
|
42
|
-
const result = await checkNativePluginUpdates({
|
|
43
|
-
force: args.includes('--force'),
|
|
44
|
-
interactive: Boolean(process.stdin.isTTY)
|
|
45
|
-
});
|
|
46
|
-
if (result.skipped && result.reason === 'rate_limited') {
|
|
47
|
-
log('Native plugin update check was recently completed.');
|
|
48
|
-
} else if (result.outdated?.length) {
|
|
49
|
-
log(uiSuccess('Native plugin update check complete.'));
|
|
50
|
-
} else if (!result.skipped) {
|
|
51
|
-
log('Native plugins are up to date.');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
log(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
log('
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { log, warn, uiHeader, uiSuccess } = require('./ui');
|
|
4
|
+
const { detectInstallTargets } = require('./installers/detect');
|
|
5
|
+
const {
|
|
6
|
+
installNativePlugin,
|
|
7
|
+
OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo']
|
|
8
|
+
} = require('./installers/nativePlugin');
|
|
9
|
+
const { checkNativePluginUpdates } = require('./pluginUpdateCheck');
|
|
10
|
+
|
|
11
|
+
const SUPPORTED_TARGETS = ['codex', 'copilot', 'claude'];
|
|
12
|
+
|
|
13
|
+
function isPluginCommand(args = []) {
|
|
14
|
+
return args[0] === 'plugin';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveTargets(targetArg) {
|
|
18
|
+
if (targetArg === 'all') return SUPPORTED_TARGETS;
|
|
19
|
+
if (targetArg && SUPPORTED_TARGETS.includes(targetArg)) return [targetArg];
|
|
20
|
+
if (targetArg) {
|
|
21
|
+
throw new Error(`Unsupported plugin target "${targetArg}". Use codex, copilot, claude, or all.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const detected = detectInstallTargets().map((target) => target.id);
|
|
25
|
+
if (detected.length === 0) {
|
|
26
|
+
throw new Error('No supported CLI detected. Specify a target explicitly: codex, copilot, claude, or all.');
|
|
27
|
+
}
|
|
28
|
+
return detected;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolvePluginNames(args = []) {
|
|
32
|
+
const names = ['empirical-memory'];
|
|
33
|
+
if (args.includes('--all-optional') || args.includes('--work-history')) names.push('empirical-work-history');
|
|
34
|
+
if (args.includes('--all-optional') || args.includes('--scar-tissue')) names.push('empirical-scar-tissue');
|
|
35
|
+
if (args.includes('--all-optional') || args.includes('--todo')) names.push('empirical-todo');
|
|
36
|
+
return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function runPluginCommand(args = []) {
|
|
40
|
+
const action = args[0];
|
|
41
|
+
if (action === 'check') {
|
|
42
|
+
const result = await checkNativePluginUpdates({
|
|
43
|
+
force: args.includes('--force'),
|
|
44
|
+
interactive: Boolean(process.stdin.isTTY)
|
|
45
|
+
});
|
|
46
|
+
if (result.skipped && result.reason === 'rate_limited') {
|
|
47
|
+
log('Native plugin update check was recently completed.');
|
|
48
|
+
} else if (result.outdated?.length) {
|
|
49
|
+
log(uiSuccess('Native plugin update check complete.'));
|
|
50
|
+
} else if (!result.skipped) {
|
|
51
|
+
log('Native plugins are up to date.');
|
|
52
|
+
}
|
|
53
|
+
if (result.notInstalled?.length) {
|
|
54
|
+
for (const target of result.notInstalled) log(`${target}: not installed`);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (action !== 'install') {
|
|
59
|
+
throw new Error('Usage: empirical plugin install <codex|copilot|claude|all> or empirical plugin check [--force]');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const targets = resolveTargets(args[1]);
|
|
63
|
+
const pluginNames = resolvePluginNames(args.slice(2));
|
|
64
|
+
log(uiHeader('Empirical native plugin installer'));
|
|
65
|
+
log('');
|
|
66
|
+
for (const target of targets) {
|
|
67
|
+
log(`Installing empirical-memory for ${target}...`);
|
|
68
|
+
if (pluginNames.length === 1) installNativePlugin(target);
|
|
69
|
+
else installNativePlugin(target, pluginNames);
|
|
70
|
+
log(uiSuccess(`${target} plugins installed through its marketplace: ${pluginNames.join(', ')}.`));
|
|
71
|
+
}
|
|
72
|
+
log('');
|
|
73
|
+
log('Native plugin installation does not configure MCP or credentials.');
|
|
74
|
+
log('Optional packages: --work-history, --scar-tissue, --todo, or --all-optional.');
|
|
75
|
+
log('Use `empirical auth login` for normal CLI OAuth, or the full `empirical install` flow for MCP setup.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
SUPPORTED_TARGETS,
|
|
80
|
+
isPluginCommand,
|
|
81
|
+
resolveTargets,
|
|
82
|
+
resolvePluginNames,
|
|
83
|
+
runPluginCommand
|
|
84
|
+
};
|
package/lib/pluginUpdateCheck.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { askYesNo, ask,
|
|
3
|
+
const { askYesNo, ask, warn, uiHeader, parseSelection } = require('./ui');
|
|
4
4
|
const { runResolvedCommand, commandExists, summarizeProcessOutput } = require('./fsUtils');
|
|
5
5
|
const { readDeviceConfig, writeDeviceConfig } = require('./deviceConfig');
|
|
6
6
|
const { installNativePlugin } = require('./installers/nativePlugin');
|
|
@@ -34,12 +34,35 @@ function extractVersion(output, pluginName = 'empirical-memory') {
|
|
|
34
34
|
const match = rows.find((row) => row && (row.name === pluginName || row.plugin === pluginName));
|
|
35
35
|
if (match?.version) return String(match.version);
|
|
36
36
|
} catch (_error) {
|
|
37
|
-
// Native CLIs may print a table instead of JSON.
|
|
37
|
+
// Native CLIs may print a table or a human-readable list instead of JSON.
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
|
|
41
|
+
// Neither native CLI emits JSON for `plugin list`. Codex prints a table row per plugin
|
|
42
|
+
// ("empirical-memory@empirical-memory installed, enabled 1.1.1 <path>"); Claude prints a
|
|
43
|
+
// bulleted list with the version on its OWN line two lines below the name ("❯ empirical-
|
|
44
|
+
// memory@empirical-memory" / " Version: 1.1.2"). A naive "first line containing pluginName"
|
|
45
|
+
// search (the previous approach) false-matched marketplace-header/path lines that mention the
|
|
46
|
+
// plugin name but carry no version at all (e.g. "Marketplace `empirical-memory`"), and for
|
|
47
|
+
// Claude's format could never find a version regardless since name and version never share a
|
|
48
|
+
// line. Anchoring on "<name>@<marketplace>" is what both real formats share and headers/paths
|
|
49
|
+
// don't, so that's the marker used to find the actual entry line before looking for a version
|
|
50
|
+
// on it or just below it.
|
|
51
|
+
const escapedName = pluginName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
const entryLinePattern = new RegExp(`${escapedName}@[\\w.-]+`, 'i');
|
|
53
|
+
const lines = text.split(/\r?\n/);
|
|
54
|
+
const entryIndex = lines.findIndex((line) => entryLinePattern.test(line));
|
|
55
|
+
if (entryIndex === -1) return null;
|
|
56
|
+
|
|
57
|
+
const sameLineMatch = lines[entryIndex].match(/(\d+\.\d+\.\d+[\w.+-]*)/);
|
|
58
|
+
if (sameLineMatch) return sameLineMatch[1];
|
|
59
|
+
|
|
60
|
+
for (let i = entryIndex + 1; i < Math.min(lines.length, entryIndex + 5); i += 1) {
|
|
61
|
+
const versionMatch = lines[i].match(/Version:\s*(\S+)/i);
|
|
62
|
+
if (versionMatch) return versionMatch[1] === 'unknown' ? null : versionMatch[1];
|
|
63
|
+
if (entryLinePattern.test(lines[i])) break;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
function readInstalledVersions(target, pluginNames) {
|
|
@@ -91,32 +114,42 @@ async function checkNativePluginUpdates({ force = false, interactive = process.s
|
|
|
91
114
|
config.pluginUpdates = { checkedAt: Date.now(), latestVersions: latestVersion, installed };
|
|
92
115
|
writeDeviceConfig(config);
|
|
93
116
|
|
|
94
|
-
|
|
117
|
+
// `installed[target]` is null when that target's CLI is not on PATH (or its `plugin list`
|
|
118
|
+
// call failed) -- readInstalledVersions() returns null in that case, and it is not "no
|
|
119
|
+
// outdated plugins", it is "we could not check this target at all". Skip those targets
|
|
120
|
+
// instead of crashing on `installed[target][name]`.
|
|
121
|
+
const outdated = SUPPORTED_TARGETS.filter((target) => installed[target]).flatMap((target) => pluginNames
|
|
95
122
|
.filter((name) => compareVersions(latestVersion[name], installed[target][name]) === 1)
|
|
96
123
|
.map((name) => ({ target, name, installed: installed[target][name], latest: latestVersion[name] })));
|
|
124
|
+
const notInstalled = SUPPORTED_TARGETS.filter((target) => !installed[target]);
|
|
97
125
|
const latestSignature = JSON.stringify(latestVersion);
|
|
98
126
|
if (outdated.length === 0 || cache.offeredVersion === latestSignature) {
|
|
99
|
-
return { latestVersion, installed, outdated };
|
|
127
|
+
return { latestVersion, installed, outdated, notInstalled };
|
|
100
128
|
}
|
|
101
129
|
|
|
102
130
|
if (!interactive) {
|
|
103
|
-
return { latestVersion, installed, outdated, promptSuppressed: true };
|
|
131
|
+
return { latestVersion, installed, outdated, notInstalled, promptSuppressed: true };
|
|
104
132
|
}
|
|
105
133
|
|
|
134
|
+
// All output below is advisory/interactive (header + prompts), and this check runs
|
|
135
|
+
// automatically ahead of most commands (see bin/empirical.js), so it must never land on
|
|
136
|
+
// stdout: any command whose stdout contract is machine-parsed (e.g. `memory query`) would
|
|
137
|
+
// otherwise get this header and prompt text as preamble ahead of its JSON. Route it through
|
|
138
|
+
// stderr instead -- ask()/askYesNo() already accept an output-stream parameter for this.
|
|
106
139
|
// latestVersion is a map of pluginName -> version (e.g. {'empirical-memory': '1.1.2', ...}),
|
|
107
140
|
// not a single scalar -- interpolating it directly stringified to "[object Object]". Prefer
|
|
108
141
|
// the core plugin's version since that's the one most users recognize; the per-target
|
|
109
142
|
// breakdown line right below already lists every outdated plugin/version precisely.
|
|
110
143
|
const headlineVersion = latestVersion['empirical-memory'] || Object.values(latestVersion)[0] || 'update available';
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const update = await askYesNo('Update the native plugin now?', true);
|
|
144
|
+
warn(uiHeader(`A newer empirical-memory plugin is available (${headlineVersion}).`));
|
|
145
|
+
warn(`Installed versions: ${outdated.map((item) => `${item.target} ${item.name} ${item.installed} -> ${item.latest}`).join(', ')}`);
|
|
146
|
+
const update = await askYesNo('Update the native plugin now?', true, process.stderr);
|
|
114
147
|
if (!update) {
|
|
115
148
|
config.pluginUpdates = { ...config.pluginUpdates, offeredVersion: latestSignature, outcome: 'declined' };
|
|
116
149
|
writeDeviceConfig(config);
|
|
117
|
-
return { latestVersion, installed, outdated, declined: true };
|
|
150
|
+
return { latestVersion, installed, outdated, notInstalled, declined: true };
|
|
118
151
|
}
|
|
119
|
-
const selection = (await ask('Which agents should be updated? (all, or numbers 1-' + outdated.length + ') ')).trim().toLowerCase();
|
|
152
|
+
const selection = (await ask('Which agents should be updated? (all, or numbers 1-' + outdated.length + ') ', process.stderr)).trim().toLowerCase();
|
|
120
153
|
const outdatedTargets = [...new Set(outdated.map((item) => item.target))];
|
|
121
154
|
const selectedTargets = selection === 'all' || !selection
|
|
122
155
|
? outdatedTargets
|
|
@@ -133,7 +166,7 @@ async function checkNativePluginUpdates({ force = false, interactive = process.s
|
|
|
133
166
|
}
|
|
134
167
|
config.pluginUpdates = { ...config.pluginUpdates, offeredVersion: latestSignature, outcome: results };
|
|
135
168
|
writeDeviceConfig(config);
|
|
136
|
-
return { latestVersion, installed, outdated, results };
|
|
169
|
+
return { latestVersion, installed, outdated, notInstalled, results };
|
|
137
170
|
}
|
|
138
171
|
|
|
139
172
|
module.exports = {
|
|
@@ -142,6 +175,7 @@ module.exports = {
|
|
|
142
175
|
parseVersion,
|
|
143
176
|
compareVersions,
|
|
144
177
|
extractVersion,
|
|
178
|
+
readInstalledVersions,
|
|
145
179
|
shouldCheck,
|
|
146
180
|
fetchLatestVersions,
|
|
147
181
|
checkNativePluginUpdates
|
|
@@ -0,0 +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
|
+
};
|
package/lib/skillSync.js
CHANGED
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const matter = require('gray-matter');
|
|
10
|
-
const os = require('os');
|
|
11
10
|
const path = require('path');
|
|
12
11
|
const { httpJson } = require('./http');
|
|
13
12
|
const { log, warn, createPrompter } = require('./ui');
|
|
14
|
-
const { detectInstallTargets } = require('./installers/detect');
|
|
13
|
+
const { detectInstallTargets } = require('./installers/detect');
|
|
15
14
|
const {
|
|
16
15
|
readDeviceConfig,
|
|
17
16
|
writeDeviceConfig,
|
|
@@ -20,47 +19,40 @@ const {
|
|
|
20
19
|
writeSyncManifest,
|
|
21
20
|
readPendingReview,
|
|
22
21
|
writePendingReview
|
|
23
|
-
} = require('./deviceConfig');
|
|
24
|
-
|
|
25
|
-
const SYNC_READ_MAX_ATTEMPTS = 2;
|
|
26
|
-
const SYNC_READ_RETRY_DELAY_MS = 250;
|
|
27
|
-
|
|
28
|
-
function isRetryableSyncReadError(error) {
|
|
29
|
-
if (error?.statusCode) return false;
|
|
30
|
-
return /socket hang up|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|ENOTFOUND|network/i.test(error?.message || '');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function fetchSyncRead(url, headers, label) {
|
|
34
|
-
let lastError;
|
|
35
|
-
for (let attempt = 1; attempt <= SYNC_READ_MAX_ATTEMPTS; attempt += 1) {
|
|
36
|
-
try {
|
|
37
|
-
return await httpJson(url, 'GET', undefined, headers);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
lastError = error;
|
|
40
|
-
if (attempt >= SYNC_READ_MAX_ATTEMPTS || !isRetryableSyncReadError(error)) break;
|
|
41
|
-
await new Promise((resolve) => setTimeout(resolve, SYNC_READ_RETRY_DELAY_MS));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const detail = lastError?.message || 'unknown request error';
|
|
46
|
-
const wrapped = new Error(`${label} request failed: ${detail}`);
|
|
47
|
-
if (lastError?.code) wrapped.code = lastError.code;
|
|
48
|
-
if (lastError?.statusCode) wrapped.statusCode = lastError.statusCode;
|
|
49
|
-
wrapped.cause = lastError;
|
|
50
|
-
throw wrapped;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Per-agent skill file path conventions, mirrored exactly from the hardcoded paths already
|
|
54
|
-
// used by installers/claude.js, installers/codex.js, installers/copilot.js (for the single
|
|
55
|
-
// empirical-memory skill) — just parameterized by skill name instead of hardcoded to it.
|
|
56
|
-
function skillFilePath(targetId, skillName) {
|
|
57
|
-
const home = os.homedir();
|
|
58
|
-
if (targetId === 'claude') return path.join(home, '.claude', 'skills', skillName, 'SKILL.md');
|
|
59
|
-
if (targetId === 'codex') return path.join(home, '.agents', 'skills', skillName, 'SKILL.md');
|
|
60
|
-
if (targetId === 'copilot') return path.join(home, '.copilot', 'skills', skillName, 'SKILL.md');
|
|
61
|
-
return null;
|
|
22
|
+
} = require('./deviceConfig');
|
|
23
|
+
|
|
24
|
+
const SYNC_READ_MAX_ATTEMPTS = 2;
|
|
25
|
+
const SYNC_READ_RETRY_DELAY_MS = 250;
|
|
26
|
+
|
|
27
|
+
function isRetryableSyncReadError(error) {
|
|
28
|
+
if (error?.statusCode) return false;
|
|
29
|
+
return /socket hang up|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|ENOTFOUND|network/i.test(error?.message || '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function fetchSyncRead(url, headers, label) {
|
|
33
|
+
let lastError;
|
|
34
|
+
for (let attempt = 1; attempt <= SYNC_READ_MAX_ATTEMPTS; attempt += 1) {
|
|
35
|
+
try {
|
|
36
|
+
return await httpJson(url, 'GET', undefined, headers);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
lastError = error;
|
|
39
|
+
if (attempt >= SYNC_READ_MAX_ATTEMPTS || !isRetryableSyncReadError(error)) break;
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, SYNC_READ_RETRY_DELAY_MS));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const detail = lastError?.message || 'unknown request error';
|
|
45
|
+
const wrapped = new Error(`${label} request failed: ${detail}`);
|
|
46
|
+
if (lastError?.code) wrapped.code = lastError.code;
|
|
47
|
+
if (lastError?.statusCode) wrapped.statusCode = lastError.statusCode;
|
|
48
|
+
wrapped.cause = lastError;
|
|
49
|
+
throw wrapped;
|
|
62
50
|
}
|
|
63
51
|
|
|
52
|
+
// skillFilePath moved to ./skillPaths.js (see that file for why) — re-exported below for
|
|
53
|
+
// existing callers of skillSync.js that import it from here.
|
|
54
|
+
const { skillFilePath } = require('./skillPaths');
|
|
55
|
+
|
|
64
56
|
function hashContent(content) {
|
|
65
57
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
66
58
|
}
|
|
@@ -102,14 +94,14 @@ function buildSkillFileContent(skill) {
|
|
|
102
94
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
|
103
95
|
// CRITICAL TRAP TO AVOID (see PRD US-011): the server's resolveBuiltin() (see
|
|
104
96
|
// packages/server/src/services/sourceResolver.service.ts) currently returns ONE hardcoded
|
|
105
|
-
// PLACEHOLDER Skill named `empirical-memory`, whose bodyMarkdown is literally
|
|
106
|
-
// "Placeholder body — real content wiring is a later story." — it is NOT the native
|
|
107
|
-
// empirical-memory plugin content. The plugin repository owns the real Skill, and native
|
|
108
|
-
// agent plugin installers are responsible for installing it.
|
|
97
|
+
// PLACEHOLDER Skill named `empirical-memory`, whose bodyMarkdown is literally
|
|
98
|
+
// "Placeholder body — real content wiring is a later story." — it is NOT the native
|
|
99
|
+
// empirical-memory plugin content. The plugin repository owns the real Skill, and native
|
|
100
|
+
// agent plugin installers are responsible for installing it.
|
|
109
101
|
//
|
|
110
|
-
// If this generalized writer were allowed to write a resolved Skill named `empirical-memory`,
|
|
111
|
-
// it would silently overwrite that real plugin file with the server's placeholder stub text on
|
|
112
|
-
// every sync — a real regression for every existing user. So: any
|
|
102
|
+
// If this generalized writer were allowed to write a resolved Skill named `empirical-memory`,
|
|
103
|
+
// it would silently overwrite that real plugin file with the server's placeholder stub text on
|
|
104
|
+
// every sync — a real regression for every existing user. So: any
|
|
113
105
|
// resolved Skill named exactly `empirical-memory` is filtered out here, unconditionally,
|
|
114
106
|
// before any writing/tracking/removal-consideration happens. A future story that finally
|
|
115
107
|
// wires up real builtin content server-side (not this one) needs to revisit this filter.
|
|
@@ -352,10 +344,10 @@ async function performSync({ accessToken, baseUrl, dryRun = false, force = false
|
|
|
352
344
|
}
|
|
353
345
|
|
|
354
346
|
const versionUrl = new URL(`/api/v1/devices/${encodeURIComponent(deviceId)}/version`, baseUrl).toString();
|
|
355
|
-
const versionResponse = await fetchSyncRead(versionUrl, {
|
|
356
|
-
Authorization: `Bearer ${accessToken}`,
|
|
357
|
-
'x-empirical-surface': 'cli'
|
|
358
|
-
}, 'Kit version');
|
|
347
|
+
const versionResponse = await fetchSyncRead(versionUrl, {
|
|
348
|
+
Authorization: `Bearer ${accessToken}`,
|
|
349
|
+
'x-empirical-surface': 'cli'
|
|
350
|
+
}, 'Kit version');
|
|
359
351
|
const versionData = (versionResponse && versionResponse.data) ? versionResponse.data : versionResponse;
|
|
360
352
|
const version = versionData.version;
|
|
361
353
|
|
|
@@ -364,10 +356,10 @@ async function performSync({ accessToken, baseUrl, dryRun = false, force = false
|
|
|
364
356
|
}
|
|
365
357
|
|
|
366
358
|
const resolvedUrl = new URL(`/api/v1/devices/${encodeURIComponent(deviceId)}/resolved`, baseUrl).toString();
|
|
367
|
-
const resolvedResponse = await fetchSyncRead(resolvedUrl, {
|
|
368
|
-
Authorization: `Bearer ${accessToken}`,
|
|
369
|
-
'x-empirical-surface': 'cli'
|
|
370
|
-
}, 'Resolved Kit skills');
|
|
359
|
+
const resolvedResponse = await fetchSyncRead(resolvedUrl, {
|
|
360
|
+
Authorization: `Bearer ${accessToken}`,
|
|
361
|
+
'x-empirical-surface': 'cli'
|
|
362
|
+
}, 'Resolved Kit skills');
|
|
371
363
|
const resolvedData = (resolvedResponse && resolvedResponse.data) ? resolvedResponse.data : resolvedResponse;
|
|
372
364
|
const skills = filterOutEmpiricalMemory(resolvedData.skills);
|
|
373
365
|
const tierSkipped = resolvedData.skipped || [];
|
package/package.json
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empirical-gauzza/cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.21",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"memory",
|
|
7
|
+
"mcp",
|
|
8
|
+
"agent memory",
|
|
9
|
+
"ai memory",
|
|
10
|
+
"claude",
|
|
11
|
+
"chatgpt",
|
|
12
|
+
"model context protocol",
|
|
13
|
+
"persistent memory",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"codex",
|
|
16
|
+
"knowledge base"
|
|
17
|
+
],
|
|
5
18
|
"type": "commonjs",
|
|
6
19
|
"license": "MIT",
|
|
7
20
|
"homepage": "https://empirical.gauzza.com/docs/cli-details",
|