@empirical-gauzza/cli 1.0.36 → 1.0.38
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 +6 -0
- package/lib/attachmentCommands.js +388 -0
- package/lib/auth.js +16 -2
- package/lib/blueprintCommands.js +23 -3
- package/lib/constants.js +722 -670
- package/lib/empiricalTokensFormat.js +68 -0
- package/lib/help.js +23 -1
- package/lib/http.js +171 -3
- package/lib/installWizard.js +3 -1
- package/lib/installers/coreMemoryBlock.js +2 -1
- package/lib/installers/hooks.js +1312 -1312
- package/lib/installers/nativePlugin.js +1 -1
- package/lib/installers/promptBlocks.js +9 -0
- package/lib/installers/skill-variants/SKILL.both.md +20 -0
- package/lib/installers/skill-variants/SKILL.cli.md +231 -221
- package/lib/installers/skill-variants/SKILL.mcp.md +18 -0
- package/lib/memoryCommands.js +1267 -1220
- package/lib/memoryResolve.js +112 -0
- package/lib/pluginCommands.js +3 -2
- package/lib/setupCommands.js +751 -31
- package/lib/usageCommands.js +52 -0
- package/lib/workspaceCommands.js +72 -5
- package/lib/workspaceContext.js +176 -176
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Empirical Tokens wording and the cost gate shared by every CLI command that can start a priced
|
|
4
|
+
// action (`setup run`, `blueprint install`) or print a reset date (`usage`, `whoami`, error hints).
|
|
5
|
+
|
|
6
|
+
// '<Month> <D> at <h:mm AM/PM> <TZ>' in this machine's time zone, e.g. 'October 31 at 8:00 PM EDT'.
|
|
7
|
+
// The reset instant is 00:00 UTC on the 1st, so the local date can be the day before: date AND time
|
|
8
|
+
// are always shown. This is a standalone copy of formatResetInstant in packages/shared (the CLI is
|
|
9
|
+
// published on its own and does not import shared); both are pinned to the same fixtures in tests.
|
|
10
|
+
// timeZone defaults to the machine's own; an invalid or empty one reads as UTC and says so. Returns
|
|
11
|
+
// null for a missing or unreadable date so callers can fall back to neutral wording.
|
|
12
|
+
function resetLabel(resetsAt, timeZone) {
|
|
13
|
+
if (resetsAt === undefined || resetsAt === null || resetsAt === '') return null;
|
|
14
|
+
const date = new Date(resetsAt);
|
|
15
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
16
|
+
const machineZone = (() => {
|
|
17
|
+
try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (_error) { return null; }
|
|
18
|
+
})();
|
|
19
|
+
const requested = timeZone === undefined ? machineZone : timeZone;
|
|
20
|
+
const zones = [typeof requested === 'string' && requested.trim() ? requested.trim() : null, 'UTC'].filter(Boolean);
|
|
21
|
+
for (const zone of zones) {
|
|
22
|
+
try {
|
|
23
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
24
|
+
timeZone: zone, month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short'
|
|
25
|
+
}).formatToParts(date);
|
|
26
|
+
const get = (type) => (parts.find((p) => p.type === type) || {}).value || '';
|
|
27
|
+
return `${get('month')} ${get('day')} at ${get('hour')}:${get('minute')} ${get('dayPeriod')} ${get('timeZoneName')}`;
|
|
28
|
+
} catch (_error) {
|
|
29
|
+
// Invalid time zone: try UTC.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A charge is offered only to a real terminal; piped input never accepts one. EMPIRICAL_TEST_ASSUME_TTY=1
|
|
36
|
+
// exists only so tests can drive the prompt through a pipe, and it applies ONLY when NODE_ENV is 'test':
|
|
37
|
+
// a stray copy of it in a real shell cannot switch off the non-interactive refusals (a paid re-shape from
|
|
38
|
+
// a binding, a cost accepted with no one at the keyboard). It is not documented in --help on purpose and
|
|
39
|
+
// nothing in the product sets it.
|
|
40
|
+
function isInteractiveTerminal() {
|
|
41
|
+
if (process.stdin.isTTY) return true;
|
|
42
|
+
return process.env.NODE_ENV === 'test' && process.env.EMPIRICAL_TEST_ASSUME_TTY === '1';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A priced start answered 200 with status 'cost_confirmation_required' and started nothing. This
|
|
46
|
+
// resolves only when a person at a terminal typed y or yes; every other path (no prompter, no terminal,
|
|
47
|
+
// any other reply) prints why nothing started and exits 1. The cost is never accepted on the person's
|
|
48
|
+
// behalf: acceptance reaches the server only from --accept-cost or from this prompt.
|
|
49
|
+
// preview the server's preview payload (message, empiricalTokenCost)
|
|
50
|
+
// prompter createPrompter(...) result, or null for a non-interactive run
|
|
51
|
+
// warn the CLI's stderr printer
|
|
52
|
+
// sanitize terminal sanitiser for server text
|
|
53
|
+
async function confirmCostOrExit(preview, { prompter, warn, sanitize = String }) {
|
|
54
|
+
warn(sanitize(preview.message || `This uses ${preview.empiricalTokenCost} Empirical Tokens.`));
|
|
55
|
+
if (!prompter || !isInteractiveTerminal()) {
|
|
56
|
+
warn('Nothing was started. Re-run with --accept-cost to start.');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const reply = (await prompter.askText('Start? (y/N)')).trim();
|
|
60
|
+
if (!/^(y|yes)$/i.test(reply)) {
|
|
61
|
+
warn('Nothing was started.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isCostPreview = (value) => Boolean(value) && value.status === 'cost_confirmation_required';
|
|
67
|
+
|
|
68
|
+
module.exports = { resetLabel, isInteractiveTerminal, confirmCostOrExit, isCostPreview };
|
package/lib/help.js
CHANGED
|
@@ -13,7 +13,7 @@ function showHelp() {
|
|
|
13
13
|
log('');
|
|
14
14
|
log('Usage:');
|
|
15
15
|
log(' empirical install');
|
|
16
|
-
log(' empirical plugin install <codex|copilot|claude|all> [--all-optional]');
|
|
16
|
+
log(' empirical plugin install <codex|copilot|claude|all> [--all-optional|--voice]');
|
|
17
17
|
log(' empirical plugin check [--force]');
|
|
18
18
|
log(' empirical policy show [--refresh]');
|
|
19
19
|
log(' empirical policy set --json <policy>');
|
|
@@ -25,6 +25,7 @@ function showHelp() {
|
|
|
25
25
|
log(' Mint a new API key (--replace-all wipes the existing ones)');
|
|
26
26
|
log(' empirical auth revoke-key (--key-id <id> | --all)');
|
|
27
27
|
log(' Revoke one API key, or every key on the account');
|
|
28
|
+
log(' empirical usage [--json] Memory, installs and Empirical Tokens this month');
|
|
28
29
|
log(' empirical oauth bootstrap headless [--print-access-token]');
|
|
29
30
|
log(' First-time token setup for CI/headless/API-key use');
|
|
30
31
|
log(' empirical oauth bootstrap headless --write-env [--env-output <path>]');
|
|
@@ -45,6 +46,13 @@ function showHelp() {
|
|
|
45
46
|
log(' empirical workspace unbind Remove this project\'s workspace binding');
|
|
46
47
|
log(' empirical workspace create <name> [--description <text>]');
|
|
47
48
|
log(' Create a new shared team workspace');
|
|
49
|
+
log(' empirical workspace create --interview [--answers <file.json>] [--accept-cost]');
|
|
50
|
+
log(' Set up a new shared workspace by interview: it offers options, shows a');
|
|
51
|
+
log(' draft, and creates the workspace and invites only when you say yes');
|
|
52
|
+
log(' empirical workspace shape [--workspace-id <id>] [--answers <file.json>] [--accept-cost]');
|
|
53
|
+
log(' Re-shape a team workspace you manage by interview. Without --workspace-id it');
|
|
54
|
+
log(' confirms the workspace bound to this directory with you, at a terminal only;');
|
|
55
|
+
log(' in a script or with --accept-cost it needs --workspace-id. A new interview is charged');
|
|
48
56
|
log(' empirical workspace invite create [--role reader|writer|manager] [--expires-in-days <n>] [--max-uses <n>]');
|
|
49
57
|
log(' Create a shareable invite link for the active team workspace');
|
|
50
58
|
log(' empirical workspace invite list List active invites for the active team workspace');
|
|
@@ -52,6 +60,16 @@ function showHelp() {
|
|
|
52
60
|
log(' empirical workspace delete <number|name|id> [--confirm]');
|
|
53
61
|
log(' Soft-delete a team workspace (dry-run without --confirm)');
|
|
54
62
|
log('');
|
|
63
|
+
log('Guided setups:');
|
|
64
|
+
log(' empirical setup list List guided setups');
|
|
65
|
+
log(' empirical setup run <setupId> [--accept-cost] Run one interactively');
|
|
66
|
+
log(' empirical setup run <setupId> --answers <f> Run from a JSON file of answers');
|
|
67
|
+
log(' empirical setup resume <sessionId> Show the current question');
|
|
68
|
+
log(' empirical setup answer <sessionId> <key> <v> Answer a question');
|
|
69
|
+
log(' empirical setup confirm <sessionId> <hash> Confirm and install a reviewed draft');
|
|
70
|
+
log(' empirical setup finish <sessionId> Finish a scripted setup early');
|
|
71
|
+
log(' empirical setup abandon <sessionId> Stop a workspace interview (what it cost is not returned)');
|
|
72
|
+
log('');
|
|
55
73
|
log('Options:');
|
|
56
74
|
log(' install Interactive installer for Codex/Copilot/Claude CLI integration');
|
|
57
75
|
log(' plugin install <target> Install core plugin; add --all-optional or capability flags');
|
|
@@ -79,6 +97,10 @@ function showHelp() {
|
|
|
79
97
|
log(' empirical memory note Add a note to an existing memory');
|
|
80
98
|
log(' empirical memory update Update an existing memory');
|
|
81
99
|
log(' empirical memory delete Delete an existing memory');
|
|
100
|
+
log(' empirical memory attach Attach a file to a memory [--file <path>]');
|
|
101
|
+
log(' empirical memory attachments List the files on a memory [--include-deleted] [--format table]');
|
|
102
|
+
log(' empirical memory attachment Full file commands: list, add, get, delete, restore');
|
|
103
|
+
log(' empirical memory tree One item plus everything nested under it, as a document');
|
|
82
104
|
log(' empirical memory duplicates List duplicate candidates for a memory');
|
|
83
105
|
log(' empirical memory merge Merge a duplicate memory into a canonical one');
|
|
84
106
|
log(' empirical memory export Export memories as portable JSON [--scope <personal|workspace>] [--out <path>]');
|
package/lib/http.js
CHANGED
|
@@ -52,8 +52,10 @@ function httpErrorFromResponse(method, urlString, statusCode, data) {
|
|
|
52
52
|
|
|
53
53
|
const error = new Error(parts.join(' '));
|
|
54
54
|
error.statusCode = statusCode;
|
|
55
|
+
error.serverMessage = baseMsg;
|
|
55
56
|
error.code = data.code || null;
|
|
56
57
|
error.upgradeUrl = upgradeUrl;
|
|
58
|
+
error.details = data.details && typeof data.details === 'object' ? data.details : null;
|
|
57
59
|
error.retryAfterSeconds = retryAfterSeconds;
|
|
58
60
|
return error;
|
|
59
61
|
}
|
|
@@ -61,14 +63,19 @@ function httpErrorFromResponse(method, urlString, statusCode, data) {
|
|
|
61
63
|
// Prints a structured JSON error line to stderr so scripts/agents parsing CLI
|
|
62
64
|
// output can branch on `code` (e.g. RATE_LIMITED, PLAN_LIMIT_MEMORY_REACHED)
|
|
63
65
|
// and act on `upgradeUrl`/`retryAfterSeconds` without string-parsing `message`.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
// JSON.stringify escapes C0 controls but not C1 (0x80 to 0x9f), which some terminals act on.
|
|
67
|
+
function formatApiError(error) {
|
|
68
|
+
return JSON.stringify({
|
|
66
69
|
error: true,
|
|
67
70
|
code: error.code || 'api_error',
|
|
68
71
|
message: error.message,
|
|
69
72
|
...(error.retryAfterSeconds != null && { retryAfterSeconds: error.retryAfterSeconds }),
|
|
70
73
|
...(error.upgradeUrl && { upgradeUrl: error.upgradeUrl })
|
|
71
|
-
}));
|
|
74
|
+
}).replace(/[\x80-\x9f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printApiError(error) {
|
|
78
|
+
warn(formatApiError(error));
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
function httpJson(urlString, method, payload, headers = {}, timeoutMs = 0) {
|
|
@@ -131,6 +138,163 @@ function httpJson(urlString, method, payload, headers = {}, timeoutMs = 0) {
|
|
|
131
138
|
});
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
// A filename travels to the server inside a Content-Disposition header we build by hand. Local
|
|
142
|
+
// filenames are arbitrary bytes: a quote ends the parameter early and a CR/LF splits the part
|
|
143
|
+
// header, so either one lets a filename forge headers or a whole extra part. multer/busboy also
|
|
144
|
+
// has no portable unescaping here, so these characters are replaced rather than escaped.
|
|
145
|
+
function sanitizeMultipartFilename(value) {
|
|
146
|
+
const cleaned = String(value || '')
|
|
147
|
+
.replace(/[\r\n"\\]/g, '_')
|
|
148
|
+
// C0 controls would be sent verbatim into a header line.
|
|
149
|
+
.replace(/[\x00-\x1f\x7f]/g, '_')
|
|
150
|
+
.trim();
|
|
151
|
+
return cleaned || 'attachment';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildMultipartBody(boundary, fields, file) {
|
|
155
|
+
const parts = [];
|
|
156
|
+
for (const [name, value] of Object.entries(fields || {})) {
|
|
157
|
+
if (value === undefined || value === null || value === '') continue;
|
|
158
|
+
parts.push(Buffer.from(
|
|
159
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${sanitizeMultipartFilename(name)}"\r\n\r\n${String(value)}\r\n`,
|
|
160
|
+
'utf8'
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
parts.push(Buffer.from(
|
|
164
|
+
`--${boundary}\r\n`
|
|
165
|
+
+ `Content-Disposition: form-data; name="${sanitizeMultipartFilename(file.field || 'file')}";`
|
|
166
|
+
+ ` filename="${sanitizeMultipartFilename(file.filename)}"\r\n`
|
|
167
|
+
+ `Content-Type: ${sanitizeMultipartFilename(file.contentType || 'application/octet-stream')}\r\n\r\n`,
|
|
168
|
+
'utf8'
|
|
169
|
+
));
|
|
170
|
+
parts.push(file.buffer);
|
|
171
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8'));
|
|
172
|
+
return Buffer.concat(parts);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* multipart/form-data with exactly one file part, for attachment uploads. The CLI ships with one
|
|
177
|
+
* dependency and no form-data library, and the body is already fully buffered (the per-file cap
|
|
178
|
+
* is 25MB), so the body is assembled here rather than streamed.
|
|
179
|
+
*
|
|
180
|
+
* Resolves/rejects on the same contract as httpJson, so callers can use printApiError unchanged.
|
|
181
|
+
*/
|
|
182
|
+
function httpMultipart(urlString, method, { fields = {}, file }, headers = {}, timeoutMs = 0) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
if (!file || !Buffer.isBuffer(file.buffer)) {
|
|
185
|
+
reject(new Error('httpMultipart requires file.buffer'));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const parsed = new URL(urlString);
|
|
189
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
190
|
+
const boundary = `----empiricalcli${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;
|
|
191
|
+
const body = buildMultipartBody(boundary, fields, file);
|
|
192
|
+
|
|
193
|
+
const req = lib.request(
|
|
194
|
+
parsed,
|
|
195
|
+
{
|
|
196
|
+
method,
|
|
197
|
+
headers: {
|
|
198
|
+
Accept: 'application/json',
|
|
199
|
+
// The server infers source 'cli' from this (see @empirical/core memoryAuthorship).
|
|
200
|
+
'User-Agent': 'empirical-cli/1.0',
|
|
201
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
202
|
+
'Content-Length': body.length,
|
|
203
|
+
...headers
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
(res) => {
|
|
207
|
+
let raw = '';
|
|
208
|
+
res.setEncoding('utf8');
|
|
209
|
+
res.on('data', (chunk) => {
|
|
210
|
+
raw += chunk;
|
|
211
|
+
});
|
|
212
|
+
res.on('end', () => {
|
|
213
|
+
let data = {};
|
|
214
|
+
try {
|
|
215
|
+
data = raw ? JSON.parse(raw) : {};
|
|
216
|
+
} catch (_error) {
|
|
217
|
+
data = { raw };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (data && Array.isArray(data.alerts) && data.alerts.length) {
|
|
221
|
+
process.stderr.write(renderCliAlertBanner(data.alerts));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const statusCode = res.statusCode || 0;
|
|
225
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
226
|
+
reject(httpErrorFromResponse(method, urlString, statusCode, data));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
resolve(data);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
req.on('error', reject);
|
|
235
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
236
|
+
req.setTimeout(timeoutMs, () => {
|
|
237
|
+
req.destroy(new Error(`request timeout after ${timeoutMs}ms`));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
req.write(body);
|
|
241
|
+
req.end();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* GET that keeps the response as BYTES. httpJson would decode the body as utf8 and corrupt any
|
|
247
|
+
* binary, so downloads cannot reuse it. A non-2xx body is still JSON here (the attachment
|
|
248
|
+
* download route answers 404/410 as JSON), so failures are parsed and rejected on the same
|
|
249
|
+
* contract as httpJson.
|
|
250
|
+
*
|
|
251
|
+
* @returns {Promise<{ statusCode: number, headers: object, buffer: Buffer }>}
|
|
252
|
+
*/
|
|
253
|
+
function httpDownload(urlString, headers = {}, timeoutMs = 0) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const parsed = new URL(urlString);
|
|
256
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
257
|
+
|
|
258
|
+
const req = lib.request(
|
|
259
|
+
parsed,
|
|
260
|
+
{
|
|
261
|
+
method: 'GET',
|
|
262
|
+
headers: {
|
|
263
|
+
'User-Agent': 'empirical-cli/1.0',
|
|
264
|
+
...headers
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
(res) => {
|
|
268
|
+
const chunks = [];
|
|
269
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
270
|
+
res.on('end', () => {
|
|
271
|
+
const buffer = Buffer.concat(chunks);
|
|
272
|
+
const statusCode = res.statusCode || 0;
|
|
273
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
274
|
+
let data = {};
|
|
275
|
+
try {
|
|
276
|
+
data = JSON.parse(buffer.toString('utf8'));
|
|
277
|
+
} catch (_error) {
|
|
278
|
+
data = { raw: buffer.toString('utf8').slice(0, 500) };
|
|
279
|
+
}
|
|
280
|
+
reject(httpErrorFromResponse('GET', urlString, statusCode, data));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
resolve({ statusCode, headers: res.headers || {}, buffer });
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
req.on('error', reject);
|
|
289
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
290
|
+
req.setTimeout(timeoutMs, () => {
|
|
291
|
+
req.destroy(new Error(`request timeout after ${timeoutMs}ms`));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
req.end();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
134
298
|
function httpForm(urlString, method, params, headers = {}) {
|
|
135
299
|
return new Promise((resolve, reject) => {
|
|
136
300
|
const parsed = new URL(urlString);
|
|
@@ -186,7 +350,11 @@ function httpForm(urlString, method, params, headers = {}) {
|
|
|
186
350
|
module.exports = {
|
|
187
351
|
httpGet,
|
|
188
352
|
httpErrorFromResponse,
|
|
353
|
+
formatApiError,
|
|
189
354
|
printApiError,
|
|
190
355
|
httpJson,
|
|
356
|
+
httpMultipart,
|
|
357
|
+
httpDownload,
|
|
358
|
+
sanitizeMultipartFilename,
|
|
191
359
|
httpForm
|
|
192
360
|
};
|
package/lib/installWizard.js
CHANGED
|
@@ -32,6 +32,7 @@ function pluginNamesForInstallerOptions(options = {}) {
|
|
|
32
32
|
if (options.recordWorkHistory !== false) names.push('empirical-work-history');
|
|
33
33
|
if (options.recordScarTissue !== false) names.push('empirical-scar-tissue');
|
|
34
34
|
if (options.recordTodos === true) names.push('empirical-todo');
|
|
35
|
+
if (options.installVoicePlugin === true) names.push('empirical-voice');
|
|
35
36
|
return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -405,5 +406,6 @@ async function runInstallFlow() {
|
|
|
405
406
|
}
|
|
406
407
|
|
|
407
408
|
module.exports = {
|
|
408
|
-
runInstallFlow
|
|
409
|
+
runInstallFlow,
|
|
410
|
+
pluginNamesForInstallerOptions
|
|
409
411
|
};
|
|
@@ -23,7 +23,8 @@ const PLUGIN_DESCRIPTIONS = {
|
|
|
23
23
|
[CORE_PLUGIN_NAME]: 'query and save durable project/user context - the core memory skill',
|
|
24
24
|
'empirical-work-history': 'record concise summaries of completed work for future agents',
|
|
25
25
|
'empirical-scar-tissue': 'record recurring failure patterns and prevention guidance',
|
|
26
|
-
'empirical-todo': 'track actionable follow-ups by project'
|
|
26
|
+
'empirical-todo': 'track actionable follow-ups by project',
|
|
27
|
+
'empirical-voice': "write in the person's saved voice whenever you write as them"
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
function describePlugin(name) {
|