@empirical-gauzza/cli 1.0.31 → 1.0.33
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/lib/alertBanner.js +11 -1
- package/lib/installers/nativePlugin.js +116 -15
- package/package.json +1 -1
package/lib/alertBanner.js
CHANGED
|
@@ -23,6 +23,16 @@ function colorize(code, text) {
|
|
|
23
23
|
return `${code}${text}${ANSI.reset}`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Due times are shown in this terminal's own time zone, never the server's UTC sentence.
|
|
27
|
+
function localDueLine(alert) {
|
|
28
|
+
if (!alert || !alert.dueAt) return alert ? alert.body : '';
|
|
29
|
+
const d = new Date(alert.dueAt);
|
|
30
|
+
if (Number.isNaN(d.getTime())) return alert.body;
|
|
31
|
+
const when = d.toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' });
|
|
32
|
+
const rest = String(alert.body || '').replace(/^Due [^\n]*? UTC\.\s*/, '');
|
|
33
|
+
return `Due ${when}.${rest ? ` ${rest}` : ''}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function reminderMemoryId(alert) {
|
|
27
37
|
const key = String(alert && alert.alertKey || '');
|
|
28
38
|
return key.startsWith('reminder_') ? key.slice('reminder_'.length) : null;
|
|
@@ -37,7 +47,7 @@ function renderOneAlert(alert) {
|
|
|
37
47
|
// A reminder the user set. Whoever reads this, person or agent, should surface it first;
|
|
38
48
|
// the agent case was observed being skipped when the banner read like a footnote.
|
|
39
49
|
lines.push(colorize(`${ANSI.bold}${color}`, `REMINDER DUE - tell the user now: ${alert.title.replace(/^Reminder:\s*/, '')}`));
|
|
40
|
-
lines.push(` ${alert
|
|
50
|
+
lines.push(` ${localDueLine(alert)}`);
|
|
41
51
|
for (const action of alert.actions || []) {
|
|
42
52
|
lines.push(` → ${action.label}: ${action.href}`);
|
|
43
53
|
}
|
|
@@ -23,19 +23,119 @@ function runPluginCommand(command, args, options = {}) {
|
|
|
23
23
|
return runResolvedCommand(command, args, { stdio: 'inherit', ...options });
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Install/update commands are run with their output captured (and echoed back so the user still
|
|
27
|
+
// sees the host CLI's own progress) because the failure text is what tells a locked plugin folder
|
|
28
|
+
// apart from a real install failure -- see installOneOrKeepExisting below.
|
|
29
|
+
function runPluginInstallCommand(command, args) {
|
|
30
|
+
const result = runResolvedCommand(command, args, { stdio: 'pipe' });
|
|
31
|
+
if (result && result.stdout) process.stdout.write(result.stdout);
|
|
32
|
+
if (result && result.stderr) process.stderr.write(result.stderr);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function assertPluginInstallSucceeded(agentId, result) {
|
|
27
37
|
if (result && result.status === 0) return;
|
|
28
38
|
const detail = result ? summarizeProcessOutput(result) : 'no process result';
|
|
29
39
|
throw new Error(`${agentId} plugin installation failed.${detail ? ` Details: ${detail}` : ''}`);
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
// ─── Locked plugin folder (Windows) ───────────────────────────────────────────
|
|
43
|
+
// Reproduced 2026-09-17: on Windows, a host CLI replaces an installed plugin by renaming its
|
|
44
|
+
// folder, and Windows refuses to rename a folder any process has open. One open handle -- a
|
|
45
|
+
// running Copilot CLI session, or the Copilot extension in VS Code watching that folder -- is
|
|
46
|
+
// enough, and the host reports it as "Access is denied. (os error 5)". Users read that as a
|
|
47
|
+
// permissions problem and reach for admin rights, which does nothing. Upstream: copilot-cli
|
|
48
|
+
// #4151 / #4095. So when an install fails that way:
|
|
49
|
+
// - if the plugin is already installed, keep it and carry on (the rest of the install -- the
|
|
50
|
+
// instructions block, the hooks -- must not be held hostage to a refresh), and say so;
|
|
51
|
+
// - if it is not installed, fail with the one instruction that fixes it, naming the programs
|
|
52
|
+
// we can see running, instead of the host CLI's bare "Access is denied".
|
|
53
|
+
const LOCKED_FOLDER_PATTERN = /access is denied|os error 5\b|EBUSY|EPERM|being used by another process/i;
|
|
54
|
+
|
|
55
|
+
// Process names (as Windows reports them) that are known to hold plugin folders open per host.
|
|
56
|
+
const LOCK_HOLDER_PROCESS_NAMES = {
|
|
57
|
+
copilot: ['copilot', 'Code'],
|
|
58
|
+
claude: ['claude', 'Code'],
|
|
59
|
+
codex: ['codex', 'Code']
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const LOCK_HOLDER_LABELS = {
|
|
63
|
+
copilot: 'GitHub Copilot CLI',
|
|
64
|
+
claude: 'Claude Code',
|
|
65
|
+
codex: 'Codex CLI',
|
|
66
|
+
Code: 'VS Code'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function looksLikeLockedPluginFolder(result) {
|
|
70
|
+
if (!result) return false;
|
|
71
|
+
const text = `${result.stdout || ''}\n${result.stderr || ''}\n${result.error ? result.error.message || result.error : ''}`;
|
|
72
|
+
return LOCKED_FOLDER_PATTERN.test(text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function detectLockHolders(agentId) {
|
|
76
|
+
if (process.platform !== 'win32') return [];
|
|
77
|
+
const names = LOCK_HOLDER_PROCESS_NAMES[agentId] || [];
|
|
78
|
+
if (!names.length) return [];
|
|
79
|
+
try {
|
|
80
|
+
const { spawnSync } = require('child_process');
|
|
81
|
+
const script = `Get-Process -Name ${names.join(',')} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ProcessName -Unique`;
|
|
82
|
+
const probe = spawnSync('pwsh', ['-NoProfile', '-Command', script], { encoding: 'utf8', timeout: 5000 });
|
|
83
|
+
return String(probe.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
84
|
+
} catch (_error) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function describeLockHolders(agentId, holders) {
|
|
90
|
+
const seen = holders.map((name) => LOCK_HOLDER_LABELS[name] || name);
|
|
91
|
+
const hostLabel = LOCK_HOLDER_LABELS[agentId] || agentId;
|
|
92
|
+
if (seen.length) {
|
|
93
|
+
return `Running now: ${seen.join(', ')}. Close ${seen.length === 1 ? 'it' : 'them'} (every window and terminal session)`;
|
|
94
|
+
}
|
|
95
|
+
return `Close every ${hostLabel} session and every VS Code window`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function currentlyInstalledVersion(agentId, name) {
|
|
99
|
+
try {
|
|
100
|
+
// Lazy: pluginUpdateCheck requires this module, so a top-level require would be a cycle.
|
|
101
|
+
const { readInstalledVersions } = require('../pluginUpdateCheck');
|
|
102
|
+
const versions = readInstalledVersions(agentId, [name]);
|
|
103
|
+
return versions && versions[name] ? versions[name] : null;
|
|
104
|
+
} catch (_error) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Runs one plugin install and interprets the result. Returns { name, refreshed, keptVersion? }.
|
|
111
|
+
* Throws only for a genuine failure, or for a locked folder when the plugin is not installed at
|
|
112
|
+
* all (there is nothing to keep, and the user must act).
|
|
113
|
+
*/
|
|
114
|
+
function installOneOrKeepExisting(agentId, name, runInstall) {
|
|
115
|
+
const result = runInstall();
|
|
116
|
+
if (result && result.status === 0) return { name, refreshed: true };
|
|
117
|
+
if (!looksLikeLockedPluginFolder(result)) assertPluginInstallSucceeded(agentId, result);
|
|
118
|
+
|
|
119
|
+
const holders = detectLockHolders(agentId);
|
|
120
|
+
const action = describeLockHolders(agentId, holders);
|
|
121
|
+
const keptVersion = currentlyInstalledVersion(agentId, name);
|
|
122
|
+
if (keptVersion) {
|
|
123
|
+
warn(` - ${LOCK_HOLDER_LABELS[agentId] || agentId} could not replace the ${name} plugin folder because another program has it open (Windows reported "Access is denied"; this is not a permissions problem). Kept the installed ${name}@${keptVersion} and continued. ${action}, then run: empirical plugin install ${agentId} to update it.`);
|
|
124
|
+
return { name, refreshed: false, keptVersion, lockedBy: holders };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(
|
|
128
|
+
`${agentId} plugin installation failed: ${LOCK_HOLDER_LABELS[agentId] || agentId} could not write the ${name} plugin folder because another program has it open `
|
|
129
|
+
+ `(Windows reported "Access is denied"; running as administrator will not help). ${action}, then run: empirical install`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
32
133
|
function updateOrInstall(agentId, command, updateArgs, installArgs) {
|
|
33
|
-
const updateResult =
|
|
34
|
-
if (updateResult && updateResult.status === 0) return
|
|
134
|
+
const updateResult = runPluginInstallCommand(command, updateArgs);
|
|
135
|
+
if (updateResult && updateResult.status === 0) return { refreshed: true };
|
|
35
136
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
return installResult;
|
|
137
|
+
const name = String(installArgs[2] || '').split('@')[0];
|
|
138
|
+
return installOneOrKeepExisting(agentId, name, () => runPluginInstallCommand(command, installArgs));
|
|
39
139
|
}
|
|
40
140
|
|
|
41
141
|
function normalizePluginNames(pluginNames = [CORE_PLUGIN_NAME]) {
|
|
@@ -92,21 +192,23 @@ function installNativePlugin(agentId, pluginNames = [CORE_PLUGIN_NAME], integrat
|
|
|
92
192
|
applySkillModeVariant(agentId, resolvedIntegrationMode);
|
|
93
193
|
};
|
|
94
194
|
|
|
195
|
+
const outcomes = [];
|
|
196
|
+
|
|
95
197
|
if (agentId === 'claude') {
|
|
96
198
|
// Marketplace registration is intentionally best-effort: native Claude returns non-zero
|
|
97
199
|
// when the same Git marketplace is already registered, but the install command is still
|
|
98
200
|
// authoritative and idempotent.
|
|
99
201
|
runPluginCommand('claude', ['plugin', 'marketplace', 'add', EMPIRICAL_PLUGIN_SOURCE], { stdio: 'pipe' });
|
|
100
202
|
for (const name of names) {
|
|
101
|
-
updateOrInstall(
|
|
203
|
+
outcomes.push(updateOrInstall(
|
|
102
204
|
agentId,
|
|
103
205
|
'claude',
|
|
104
206
|
['plugin', 'update', `${name}@empirical-memory`, '--scope', 'user'],
|
|
105
207
|
['plugin', 'install', name, '--scope', 'user']
|
|
106
|
-
);
|
|
208
|
+
));
|
|
107
209
|
}
|
|
108
210
|
applyVariantIfCore();
|
|
109
|
-
return;
|
|
211
|
+
return { outcomes };
|
|
110
212
|
}
|
|
111
213
|
|
|
112
214
|
if (agentId === 'codex') {
|
|
@@ -114,11 +216,10 @@ function installNativePlugin(agentId, pluginNames = [CORE_PLUGIN_NAME], integrat
|
|
|
114
216
|
// Refresh the Git marketplace snapshot before reinstalling an existing plugin.
|
|
115
217
|
runPluginCommand('codex', ['plugin', 'marketplace', 'upgrade', 'empirical-memory'], { stdio: 'pipe' });
|
|
116
218
|
for (const name of names) {
|
|
117
|
-
|
|
118
|
-
assertPluginInstallSucceeded(agentId, result);
|
|
219
|
+
outcomes.push(installOneOrKeepExisting(agentId, name, () => runPluginInstallCommand('codex', ['plugin', 'add', `${name}@empirical-memory`])));
|
|
119
220
|
}
|
|
120
221
|
applyVariantIfCore();
|
|
121
|
-
return;
|
|
222
|
+
return { outcomes };
|
|
122
223
|
}
|
|
123
224
|
|
|
124
225
|
if (agentId === 'copilot') {
|
|
@@ -127,11 +228,10 @@ function installNativePlugin(agentId, pluginNames = [CORE_PLUGIN_NAME], integrat
|
|
|
127
228
|
runPluginCommand('copilot', ['plugin', 'marketplace', 'add', EMPIRICAL_PLUGIN_SOURCE], { stdio: 'pipe' });
|
|
128
229
|
runPluginCommand('copilot', ['plugin', 'marketplace', 'update', 'empirical-memory'], { stdio: 'pipe' });
|
|
129
230
|
for (const name of names) {
|
|
130
|
-
|
|
131
|
-
assertPluginInstallSucceeded(agentId, result);
|
|
231
|
+
outcomes.push(installOneOrKeepExisting(agentId, name, () => runPluginInstallCommand('copilot', ['plugin', 'install', `${name}@empirical-memory`])));
|
|
132
232
|
}
|
|
133
233
|
applyVariantIfCore();
|
|
134
|
-
return;
|
|
234
|
+
return { outcomes };
|
|
135
235
|
}
|
|
136
236
|
|
|
137
237
|
throw new Error(`Unsupported plugin target: ${agentId}`);
|
|
@@ -143,5 +243,6 @@ module.exports = {
|
|
|
143
243
|
OPTIONAL_PLUGIN_NAMES,
|
|
144
244
|
normalizePluginNames,
|
|
145
245
|
installNativePlugin,
|
|
146
|
-
applySkillModeVariant
|
|
246
|
+
applySkillModeVariant,
|
|
247
|
+
looksLikeLockedPluginFolder
|
|
147
248
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empirical-gauzza/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.33",
|
|
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",
|