@empirical-gauzza/cli 1.0.35 → 1.0.37
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 +7 -1
- package/lib/auth.js +30 -2
- package/lib/blueprintCommands.js +23 -3
- package/lib/constants.js +672 -657
- package/lib/empiricalTokensFormat.js +66 -0
- package/lib/help.js +15 -3
- package/lib/http.js +11 -3
- package/lib/installWizard.js +3 -1
- package/lib/installers/coreMemoryBlock.js +2 -1
- package/lib/installers/nativePlugin.js +1 -1
- package/lib/installers/promptBlocks.js +9 -0
- package/lib/memoryCommands.js +74 -2
- package/lib/pluginCommands.js +3 -2
- package/lib/setupCommands.js +574 -31
- package/lib/usageCommands.js +52 -0
- package/lib/workspaceCommands.js +59 -7
- package/lib/workspaceContext.js +176 -76
- package/package.json +33 -33
|
@@ -0,0 +1,66 @@
|
|
|
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. It is not documented in --help on purpose,
|
|
37
|
+
// it must be exactly '1', and nothing in the product sets it; setting it only lets a person who typed
|
|
38
|
+
// 'y' at the prompt through a pipe, it never accepts a cost by itself.
|
|
39
|
+
function isInteractiveTerminal() {
|
|
40
|
+
return Boolean(process.stdin.isTTY) || process.env.EMPIRICAL_TEST_ASSUME_TTY === '1';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A priced start answered 200 with status 'cost_confirmation_required' and started nothing. This
|
|
44
|
+
// resolves only when a person at a terminal typed y or yes; every other path (no prompter, no terminal,
|
|
45
|
+
// any other reply) prints why nothing started and exits 1. The cost is never accepted on the person's
|
|
46
|
+
// behalf: acceptance reaches the server only from --accept-cost or from this prompt.
|
|
47
|
+
// preview the server's preview payload (message, empiricalTokenCost)
|
|
48
|
+
// prompter createPrompter(...) result, or null for a non-interactive run
|
|
49
|
+
// warn the CLI's stderr printer
|
|
50
|
+
// sanitize terminal sanitiser for server text
|
|
51
|
+
async function confirmCostOrExit(preview, { prompter, warn, sanitize = String }) {
|
|
52
|
+
warn(sanitize(preview.message || `This uses ${preview.empiricalTokenCost} Empirical Tokens.`));
|
|
53
|
+
if (!prompter || !isInteractiveTerminal()) {
|
|
54
|
+
warn('Nothing was started. Re-run with --accept-cost to start.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const reply = (await prompter.askText('Start? (y/N)')).trim();
|
|
58
|
+
if (!/^(y|yes)$/i.test(reply)) {
|
|
59
|
+
warn('Nothing was started.');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isCostPreview = (value) => Boolean(value) && value.status === 'cost_confirmation_required';
|
|
65
|
+
|
|
66
|
+
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>]');
|
|
@@ -38,9 +39,11 @@ function showHelp() {
|
|
|
38
39
|
log(' empirical oauth client show');
|
|
39
40
|
log(' empirical oauth client clear');
|
|
40
41
|
log(' empirical workspace list List workspaces you can select, and which one is active');
|
|
41
|
-
log(' empirical workspace current Show the
|
|
42
|
+
log(' empirical workspace current Show the active workspace and where it comes from (env, project, global)');
|
|
42
43
|
log(' empirical workspace select [<number|name>] Set the active workspace (prompts if omitted)');
|
|
43
|
-
log(' empirical workspace use <number|name|id> Alias for `workspace select
|
|
44
|
+
log(' empirical workspace use <number|name|id> Alias for `workspace select`. Inside a git repo binds THIS project;');
|
|
45
|
+
log(' --global sets the machine-wide default, --project forces a binding');
|
|
46
|
+
log(' empirical workspace unbind Remove this project\'s workspace binding');
|
|
44
47
|
log(' empirical workspace create <name> [--description <text>]');
|
|
45
48
|
log(' Create a new shared team workspace');
|
|
46
49
|
log(' empirical workspace invite create [--role reader|writer|manager] [--expires-in-days <n>] [--max-uses <n>]');
|
|
@@ -50,6 +53,15 @@ function showHelp() {
|
|
|
50
53
|
log(' empirical workspace delete <number|name|id> [--confirm]');
|
|
51
54
|
log(' Soft-delete a team workspace (dry-run without --confirm)');
|
|
52
55
|
log('');
|
|
56
|
+
log('Guided setups:');
|
|
57
|
+
log(' empirical setup list List guided setups');
|
|
58
|
+
log(' empirical setup run <setupId> [--accept-cost] Run one interactively');
|
|
59
|
+
log(' empirical setup run <setupId> --answers <f> Run from a JSON file of answers');
|
|
60
|
+
log(' empirical setup resume <sessionId> Show the current question');
|
|
61
|
+
log(' empirical setup answer <sessionId> <key> <v> Answer a question');
|
|
62
|
+
log(' empirical setup confirm <sessionId> <hash> Confirm and install a reviewed draft');
|
|
63
|
+
log(' empirical setup finish <sessionId> Finish a scripted setup early');
|
|
64
|
+
log('');
|
|
53
65
|
log('Options:');
|
|
54
66
|
log(' install Interactive installer for Codex/Copilot/Claude CLI integration');
|
|
55
67
|
log(' plugin install <target> Install core plugin; add --all-optional or capability flags');
|
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) {
|
|
@@ -186,6 +193,7 @@ function httpForm(urlString, method, params, headers = {}) {
|
|
|
186
193
|
module.exports = {
|
|
187
194
|
httpGet,
|
|
188
195
|
httpErrorFromResponse,
|
|
196
|
+
formatApiError,
|
|
189
197
|
printApiError,
|
|
190
198
|
httpJson,
|
|
191
199
|
httpForm
|
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) {
|
|
@@ -17,7 +17,7 @@ const { skillFilePath } = require('../skillPaths');
|
|
|
17
17
|
|
|
18
18
|
const EMPIRICAL_PLUGIN_SOURCE = process.env.EMPIRICAL_PLUGIN_SOURCE || 'mattgauzza/empirical-memory-plugin';
|
|
19
19
|
const CORE_PLUGIN_NAME = 'empirical-memory';
|
|
20
|
-
const OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo'];
|
|
20
|
+
const OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo', 'empirical-voice'];
|
|
21
21
|
|
|
22
22
|
function runPluginCommand(command, args, options = {}) {
|
|
23
23
|
return runResolvedCommand(command, args, { stdio: 'inherit', ...options });
|
|
@@ -23,6 +23,7 @@ const INSTALL_PROMPT_OPTIONS_DEFAULTS = Object.freeze({
|
|
|
23
23
|
recordWorkHistory: true,
|
|
24
24
|
recordScarTissue: true,
|
|
25
25
|
recordTodos: false,
|
|
26
|
+
installVoicePlugin: false,
|
|
26
27
|
storeInstructionArtifacts: false,
|
|
27
28
|
checkMemoryBeforeAskingContext: true,
|
|
28
29
|
durableWritesOnly: true
|
|
@@ -75,6 +76,8 @@ function normalizeInstallPromptOptions(options = {}) {
|
|
|
75
76
|
options.recordScarTissue !== false,
|
|
76
77
|
recordTodos:
|
|
77
78
|
options.recordTodos === true,
|
|
79
|
+
installVoicePlugin:
|
|
80
|
+
options.installVoicePlugin === true,
|
|
78
81
|
storeInstructionArtifacts:
|
|
79
82
|
options.storeInstructionArtifacts === true,
|
|
80
83
|
checkMemoryBeforeAskingContext:
|
|
@@ -228,6 +231,7 @@ function formatPromptOptionSummary(promptOptions) {
|
|
|
228
231
|
['Record timestamped work history', promptOptions.recordWorkHistory],
|
|
229
232
|
['Record scar-tissue guidance', promptOptions.recordScarTissue],
|
|
230
233
|
['Enable project-based Todo skill', promptOptions.recordTodos],
|
|
234
|
+
['Install the writing voice skill', promptOptions.installVoicePlugin],
|
|
231
235
|
['Store/recall instruction artifacts', promptOptions.storeInstructionArtifacts],
|
|
232
236
|
['Check memory before asking for context', promptOptions.checkMemoryBeforeAskingContext],
|
|
233
237
|
['Store durable context only', promptOptions.durableWritesOnly]
|
|
@@ -313,6 +317,10 @@ async function collectInstallerPromptOptions() {
|
|
|
313
317
|
'Install the project-based Todo skill for actionable follow-ups?',
|
|
314
318
|
false
|
|
315
319
|
);
|
|
320
|
+
const installVoicePlugin = await askYesNo(
|
|
321
|
+
'Install the writing voice skill so agents write as you once you have saved a voice?',
|
|
322
|
+
false
|
|
323
|
+
);
|
|
316
324
|
const storeInstructionArtifacts = await askYesNo(
|
|
317
325
|
'Store and recall instruction artifacts (AGENTS.md/CLAUDE.md/skills-style rules) in Empirical?',
|
|
318
326
|
false
|
|
@@ -337,6 +345,7 @@ async function collectInstallerPromptOptions() {
|
|
|
337
345
|
recordWorkHistory,
|
|
338
346
|
recordScarTissue,
|
|
339
347
|
recordTodos,
|
|
348
|
+
installVoicePlugin,
|
|
340
349
|
storeInstructionArtifacts,
|
|
341
350
|
checkMemoryBeforeAskingContext,
|
|
342
351
|
durableWritesOnly
|
package/lib/memoryCommands.js
CHANGED
|
@@ -33,7 +33,9 @@ function renderMemoryResultsTable(items) {
|
|
|
33
33
|
const rows = (Array.isArray(items) ? items : []).map((m) => ({
|
|
34
34
|
id: m.id || m._id || '',
|
|
35
35
|
category: m.category || '',
|
|
36
|
-
|
|
36
|
+
// A row marked usage 'style_preference_only' (a writing voice memory) says so, so a person
|
|
37
|
+
// reading the table sees what an agent reading the JSON sees.
|
|
38
|
+
summary: truncateForTable(m.summary, 60) + (m.usage === 'style_preference_only' ? ' [style]' : ''),
|
|
37
39
|
// The recall front door emits `score` (toQueryRow plus score in mcp/tools/memory.ts); the
|
|
38
40
|
// older /query route emitted rankScore/similarityScore. Read all three so the column is not
|
|
39
41
|
// blank against a real /recall row. On the list pathways score is deliberately absent, and
|
|
@@ -59,7 +61,7 @@ const REMIND_STATES = ['pending', 'notified', 'acknowledged', 'cancelled'];
|
|
|
59
61
|
// the 90-line global help wall bin/empirical.js otherwise short-circuits every --help to.
|
|
60
62
|
|
|
61
63
|
const MEMORY_SUBCOMMAND_USAGE = {
|
|
62
|
-
query: 'Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--tags-all <t1,t2>] [--status <s1,s2>] [--due-before <ISO date>] [--due-after <ISO date>] [--remind-state pending,notified] [--path semantic|list|graph|hebbian] [--hops 1|2] [--pretty] [--format table]',
|
|
64
|
+
query: 'Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--tags-all <t1,t2>] [--status <s1,s2>] [--due-before <ISO date>] [--due-after <ISO date>] [--remind-state pending,notified] [--path semantic|list|graph|hebbian] [--hops 1|2] [--workspaces <n|name|id,...>] [--all-workspaces] [--pretty] [--format table]',
|
|
63
65
|
get: 'Usage: empirical memory get --memory-id <id> [--pretty]',
|
|
64
66
|
list: 'Usage: empirical memory list [--category <cat>] [--tags <t1,t2>] [--start-date <ISO>] [--end-date <ISO>] [--limit 20] [--cursor <cursor>] [--oldest] [--pretty] [--format table]',
|
|
65
67
|
record: 'Usage: empirical memory record --category <cat> (--summary <text> | --summary - | --summary-file <path>) [--tags t1,t2] [--node-type observation] [--json-file <path>] [--no-dedupe] [--no-auto-link] [--author-type user|agent]',
|
|
@@ -192,6 +194,56 @@ function printMemoryResolveError(result) {
|
|
|
192
194
|
|
|
193
195
|
// ─── Memory subcommands ───────────────────────────────────────────────────────
|
|
194
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Turn `--workspaces` selectors (list numbers, names, or ids) into workspace ids using the
|
|
199
|
+
* per-user listing (/api/v1/users/me/workspaces, the same list `workspace list` prints; number 1
|
|
200
|
+
* is always the Default Workspace). An id (ws_...) passes through without a lookup. An unknown
|
|
201
|
+
* name is fatal at the terminal rather than a 403 or an empty answer after the round trip.
|
|
202
|
+
*/
|
|
203
|
+
async function resolveWorkspaceIdsForQuery({ baseUrl, accessToken, selectors = [], all = false }) {
|
|
204
|
+
const needsListing = all || selectors.some((s) => !/^ws_/.test(s));
|
|
205
|
+
let options = [];
|
|
206
|
+
if (needsListing) {
|
|
207
|
+
const listUrl = new URL('/api/v1/users/me/workspaces', baseUrl).toString();
|
|
208
|
+
let payload;
|
|
209
|
+
try {
|
|
210
|
+
payload = await httpJson(listUrl, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
211
|
+
} catch (error) {
|
|
212
|
+
printApiError(error);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
options = [
|
|
216
|
+
...(payload?.defaultOption ? [payload.defaultOption] : []),
|
|
217
|
+
...(Array.isArray(payload?.workspaces) ? payload.workspaces : [])
|
|
218
|
+
].filter((o) => o && o.workspaceId);
|
|
219
|
+
}
|
|
220
|
+
if (all) return [...new Set(options.map((o) => o.workspaceId))];
|
|
221
|
+
|
|
222
|
+
const ids = [];
|
|
223
|
+
for (const selector of selectors) {
|
|
224
|
+
if (/^ws_/.test(selector)) { ids.push(selector); continue; }
|
|
225
|
+
const asNumber = Number(selector);
|
|
226
|
+
let match = null;
|
|
227
|
+
if (Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= options.length) {
|
|
228
|
+
match = options[asNumber - 1];
|
|
229
|
+
} else {
|
|
230
|
+
const lower = selector.toLowerCase();
|
|
231
|
+
const byLabel = options.filter((o) => String(o.label || '').toLowerCase() === lower || String(o.key || '').toLowerCase() === lower);
|
|
232
|
+
if (byLabel.length > 1) {
|
|
233
|
+
warn(`Ambiguous workspace "${selector}". Use its list number or workspace id.`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
match = byLabel[0] || null;
|
|
237
|
+
}
|
|
238
|
+
if (!match) {
|
|
239
|
+
warn(`Workspace not found: "${selector}". Run \`empirical workspace list\` to see names and numbers.`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
ids.push(match.workspaceId);
|
|
243
|
+
}
|
|
244
|
+
return [...new Set(ids)];
|
|
245
|
+
}
|
|
246
|
+
|
|
195
247
|
async function runMemoryQuery(args) {
|
|
196
248
|
const match = parseOption(args, '--match', '');
|
|
197
249
|
if (!match) {
|
|
@@ -208,6 +260,17 @@ async function runMemoryQuery(args) {
|
|
|
208
260
|
// --status filters data.status. Both are optional and additive to --tags/--category.
|
|
209
261
|
const tagsAllRaw = parseOption(args, '--tags-all', '');
|
|
210
262
|
const tagsAll = tagsAllRaw ? tagsAllRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
263
|
+
// Multi-workspace recall (2026-09-24). --workspaces takes list numbers, names, or ids; names
|
|
264
|
+
// and numbers are resolved against the same listing `workspace list` prints, so the server
|
|
265
|
+
// contract stays ids-only. --all-workspaces sends every selectable workspace. Without either
|
|
266
|
+
// flag the query is scoped to the single active workspace exactly as before.
|
|
267
|
+
const workspacesRaw = parseOption(args, '--workspaces', '');
|
|
268
|
+
const workspaceSelectors = workspacesRaw ? workspacesRaw.split(',').map((w) => w.trim()).filter(Boolean) : [];
|
|
269
|
+
const allWorkspaces = parseFlag(args, '--all-workspaces');
|
|
270
|
+
if (workspaceSelectors.length && allWorkspaces) {
|
|
271
|
+
warn('Pass either --workspaces or --all-workspaces, not both.');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
211
274
|
const statusRaw = parseOption(args, '--status', '');
|
|
212
275
|
const status = statusRaw ? statusRaw.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
213
276
|
const pretty = parseFlag(args, '--pretty');
|
|
@@ -278,6 +341,14 @@ async function runMemoryQuery(args) {
|
|
|
278
341
|
if (category) body.category = category;
|
|
279
342
|
if (tags.length) body.tags = tags;
|
|
280
343
|
if (tagsAll.length) body.tagsAll = tagsAll;
|
|
344
|
+
if (workspaceSelectors.length || allWorkspaces) {
|
|
345
|
+
body.workspaceIds = await resolveWorkspaceIdsForQuery({
|
|
346
|
+
baseUrl,
|
|
347
|
+
accessToken,
|
|
348
|
+
selectors: workspaceSelectors,
|
|
349
|
+
all: allWorkspaces
|
|
350
|
+
});
|
|
351
|
+
}
|
|
281
352
|
if (status.length) body.status = status;
|
|
282
353
|
if (dueFilters.dueBefore) body.dueBefore = dueFilters.dueBefore;
|
|
283
354
|
if (dueFilters.dueAfter) body.dueAfter = dueFilters.dueAfter;
|
|
@@ -1130,6 +1201,7 @@ module.exports = {
|
|
|
1130
1201
|
withCliAnalyticsContext,
|
|
1131
1202
|
resolveMemoryByQuery,
|
|
1132
1203
|
printMemoryResolveError,
|
|
1204
|
+
resolveWorkspaceIdsForQuery,
|
|
1133
1205
|
runMemoryQuery,
|
|
1134
1206
|
runMemoryGet,
|
|
1135
1207
|
runMemoryList,
|
package/lib/pluginCommands.js
CHANGED
|
@@ -4,7 +4,7 @@ const { log, warn, uiHeader, uiSuccess } = require('./ui');
|
|
|
4
4
|
const { detectInstallTargets } = require('./installers/detect');
|
|
5
5
|
const {
|
|
6
6
|
installNativePlugin,
|
|
7
|
-
OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo']
|
|
7
|
+
OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo', 'empirical-voice']
|
|
8
8
|
} = require('./installers/nativePlugin');
|
|
9
9
|
const { checkNativePluginUpdates } = require('./pluginUpdateCheck');
|
|
10
10
|
const { addInstalledHosts } = require('./deviceConfig');
|
|
@@ -35,6 +35,7 @@ function resolvePluginNames(args = []) {
|
|
|
35
35
|
if (args.includes('--all-optional') || args.includes('--work-history')) names.push('empirical-work-history');
|
|
36
36
|
if (args.includes('--all-optional') || args.includes('--scar-tissue')) names.push('empirical-scar-tissue');
|
|
37
37
|
if (args.includes('--all-optional') || args.includes('--todo')) names.push('empirical-todo');
|
|
38
|
+
if (args.includes('--all-optional') || args.includes('--voice')) names.push('empirical-voice');
|
|
38
39
|
return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -82,7 +83,7 @@ async function runPluginCommand(args = []) {
|
|
|
82
83
|
}
|
|
83
84
|
log('');
|
|
84
85
|
log('Native plugin installation does not configure MCP or credentials.');
|
|
85
|
-
log('Optional packages: --work-history, --scar-tissue, --todo, or --all-optional.');
|
|
86
|
+
log('Optional packages: --work-history, --scar-tissue, --todo, --voice, or --all-optional.');
|
|
86
87
|
log('Use `empirical auth login` for normal CLI OAuth, or the full `empirical install` flow for MCP setup.');
|
|
87
88
|
}
|
|
88
89
|
|