@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,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `empirical usage`: memory, blueprint installs this month, and Empirical Tokens (spec 8.4).
|
|
4
|
+
// Reads GET /api/v1/billing/me. The Empirical Tokens line appears only while they are enforced
|
|
5
|
+
// for this account, so the command stays dark until rollout. A missing or null
|
|
6
|
+
// empiricalTokenUsage, or any mode other than 'enforce', is hidden.
|
|
7
|
+
const { httpJson, printApiError } = require('./http');
|
|
8
|
+
const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
|
|
9
|
+
const { log } = require('./ui');
|
|
10
|
+
const { resetLabel } = require('./empiricalTokensFormat');
|
|
11
|
+
|
|
12
|
+
const fmt = (n) => Number(n).toLocaleString('en-US');
|
|
13
|
+
|
|
14
|
+
function empiricalTokensLine(usage) {
|
|
15
|
+
if (!usage || usage.mode !== 'enforce') return null;
|
|
16
|
+
// An admin account has no limit: the server sends unlimited true with null granted and remaining.
|
|
17
|
+
if (usage.unlimited) return `Empirical Tokens: unlimited. ${fmt(usage.spent || 0)} used this month.`;
|
|
18
|
+
// Free has nothing granted (Writing Voice is paid only): same idea as the portal card, not '0 of 0 left'.
|
|
19
|
+
if (usage.tier === 'free' && !(Number(usage.granted) + Number(usage.adjust || 0) > 0)) return 'Empirical Tokens: building a writing voice is part of the paid plans.';
|
|
20
|
+
const reset = resetLabel(usage.resetsAt);
|
|
21
|
+
return `Empirical Tokens: ${fmt(usage.remaining)} of ${fmt(usage.granted + usage.adjust)} left this month.${reset ? ` Resets ${reset}.` : ''}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function fetchBillingMe(timeoutMs = 0) {
|
|
25
|
+
const token = requireAccessToken();
|
|
26
|
+
const url = new URL('/api/v1/billing/me', resolveApiBaseUrl()).toString();
|
|
27
|
+
const response = await httpJson(url, 'GET', null, buildApiAuthHeaders(token), timeoutMs);
|
|
28
|
+
return response && response.data ? response.data : response;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runUsageCommand(args) {
|
|
32
|
+
let data;
|
|
33
|
+
try {
|
|
34
|
+
data = await fetchBillingMe();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
printApiError(error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (args.includes('--json')) {
|
|
40
|
+
log(JSON.stringify({ memoryUsage: data.memoryUsage, blueprintUsage: data.blueprintUsage, empiricalTokenUsage: data.empiricalTokenUsage && data.empiricalTokenUsage.mode === 'enforce' ? data.empiricalTokenUsage : null }, null, 2));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const memory = data.memoryUsage || {};
|
|
44
|
+
log(memory.unlimited ? `Memory: ${fmt(memory.used)} used (no plan cap).` : `Memory: ${fmt(memory.used)} of ${fmt(memory.limit)} used.`);
|
|
45
|
+
const installs = data.blueprintUsage || {};
|
|
46
|
+
const across = typeof installs.accountUsed === 'number' ? ` (${fmt(installs.accountUsed)} across all workspaces)` : '';
|
|
47
|
+
log(installs.unlimited ? `Blueprint installs this month: ${fmt(installs.used)}${across}.` : `Blueprint installs this month: ${fmt(installs.used)} of ${fmt(installs.limit)}${across}.`);
|
|
48
|
+
const line = empiricalTokensLine(data.empiricalTokenUsage);
|
|
49
|
+
if (line) log(line);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { runUsageCommand, empiricalTokensLine, fetchBillingMe };
|
package/lib/workspaceCommands.js
CHANGED
|
@@ -7,14 +7,31 @@ const { ensureHeadlessToken, resolveAccessToken, buildApiAuthHeaders } = require
|
|
|
7
7
|
const { readStoredOAuthClient } = require('./oauthClient');
|
|
8
8
|
const { deriveBaseUrlFromMcpUrl } = require('./constants');
|
|
9
9
|
const { withCliAnalyticsContext } = require('./memoryCommands');
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
writeSelectedWorkspace,
|
|
12
|
+
clearSelectedWorkspace,
|
|
13
|
+
resolveLocalWorkspaceSelection,
|
|
14
|
+
findGitRoot,
|
|
15
|
+
writeProjectWorkspace,
|
|
16
|
+
clearProjectWorkspace,
|
|
17
|
+
projectWorkspacePathFor,
|
|
18
|
+
WORKSPACE_ENV_VAR
|
|
19
|
+
} = require('./workspaceContext');
|
|
11
20
|
|
|
12
21
|
function isWorkspaceCommand(args) {
|
|
13
22
|
if (args[0] !== 'workspace') return false;
|
|
14
|
-
if (['list', 'current', 'select', 'use', 'create', 'delete'].includes(args[1])) return true;
|
|
23
|
+
if (['list', 'current', 'select', 'use', 'unbind', 'create', 'delete'].includes(args[1])) return true;
|
|
15
24
|
return args[1] === 'invite' && ['create', 'list', 'revoke'].includes(args[2]);
|
|
16
25
|
}
|
|
17
26
|
|
|
27
|
+
function describeSelectionSource(selection) {
|
|
28
|
+
if (!selection) return '';
|
|
29
|
+
if (selection.source === 'env') return ` (from ${WORKSPACE_ENV_VAR})`;
|
|
30
|
+
if (selection.source === 'project') return ` (bound to this project: ${selection.path})`;
|
|
31
|
+
if (selection.source === 'global') return ' (global CLI selection)';
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
function workspaceOptions(payload) {
|
|
19
36
|
return [
|
|
20
37
|
...(payload?.defaultOption ? [{ ...payload.defaultOption, isDefault: true }] : []),
|
|
@@ -164,9 +181,11 @@ async function runWorkspaceCommand(args) {
|
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
if (action === 'current') {
|
|
167
|
-
|
|
184
|
+
// Same resolver every memory command uses (env, project binding, global file), so what
|
|
185
|
+
// this prints is what the next `memory query` will actually be scoped to.
|
|
186
|
+
const locallySelected = resolveLocalWorkspaceSelection();
|
|
168
187
|
if (locallySelected) {
|
|
169
|
-
log(`Current workspace: ${locallySelected.label || locallySelected.workspaceId}`);
|
|
188
|
+
log(`Current workspace: ${locallySelected.label || locallySelected.workspaceId}${describeSelectionSource(locallySelected)}`);
|
|
170
189
|
return;
|
|
171
190
|
}
|
|
172
191
|
if (!clientId) {
|
|
@@ -185,19 +204,40 @@ async function runWorkspaceCommand(args) {
|
|
|
185
204
|
return;
|
|
186
205
|
}
|
|
187
206
|
|
|
207
|
+
if (action === 'unbind') {
|
|
208
|
+
const gitRoot = findGitRoot();
|
|
209
|
+
if (!gitRoot) throw new Error('Not inside a git repository; there is no project binding here to remove.');
|
|
210
|
+
const removed = clearProjectWorkspace(gitRoot);
|
|
211
|
+
log(removed
|
|
212
|
+
? uiSuccess(`Removed project workspace binding: ${removed}`)
|
|
213
|
+
: `No project workspace binding at ${projectWorkspacePathFor(gitRoot)}.`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
188
217
|
if (action === 'select' || action === 'use') {
|
|
189
|
-
|
|
218
|
+
// Where the choice lands (2026-09-24): inside a git repo the default is a PROJECT binding
|
|
219
|
+
// (<repo>/.empirical/workspace.json, local only, never the server's per-client selection),
|
|
220
|
+
// so one repo's session cannot re-scope another's. --global keeps the old machine-wide
|
|
221
|
+
// behavior; --project forces a binding even when git is missing (uses cwd).
|
|
222
|
+
const wantGlobal = parseFlag(args, '--global');
|
|
223
|
+
const wantProject = parseFlag(args, '--project');
|
|
224
|
+
if (wantGlobal && wantProject) throw new Error('Pass either --global or --project, not both.');
|
|
225
|
+
const gitRoot = wantGlobal ? null : findGitRoot();
|
|
226
|
+
const projectRoot = wantProject ? (gitRoot || process.cwd()) : gitRoot;
|
|
227
|
+
const bindToProject = !wantGlobal && Boolean(projectRoot);
|
|
228
|
+
|
|
229
|
+
if (!bindToProject) requireClientId(clientId);
|
|
190
230
|
const [payload, teamPayload] = await Promise.all([
|
|
191
231
|
httpJson(listUrl, 'GET', null, authHeaders),
|
|
192
232
|
httpJson(teamBrainsUrl, 'GET', null, authHeaders),
|
|
193
233
|
]);
|
|
194
|
-
if (payload.locked) {
|
|
234
|
+
if (payload.locked && !bindToProject) {
|
|
195
235
|
throw new Error('Workspace is admin-locked for this OAuth client and cannot be changed.');
|
|
196
236
|
}
|
|
197
237
|
|
|
198
238
|
const options = workspaceOptions(payload);
|
|
199
239
|
|
|
200
|
-
let requested = args.slice(2).join(' ').trim();
|
|
240
|
+
let requested = args.slice(2).filter((arg) => !['--global', '--project'].includes(arg)).join(' ').trim();
|
|
201
241
|
if (!requested && process.stdin.isTTY) {
|
|
202
242
|
log('Select workspace:');
|
|
203
243
|
options.forEach((item, idx) => log(` ${idx + 1}. ${item.label}`));
|
|
@@ -213,6 +253,18 @@ async function runWorkspaceCommand(args) {
|
|
|
213
253
|
if (target?.ambiguous) throw new Error(`Ambiguous workspace: ${requested}. Use its list number or workspace ID.`);
|
|
214
254
|
if (!target?.selectionKey) throw new Error(`Workspace not found: ${requested}`);
|
|
215
255
|
|
|
256
|
+
if (bindToProject) {
|
|
257
|
+
// The Default Workspace is written explicitly (its personal id), never as "clear": an
|
|
258
|
+
// explicit binding is the whole point when a sibling session may have changed the global.
|
|
259
|
+
const workspaceId = target.workspaceId || payload?.defaultOption?.workspaceId || null;
|
|
260
|
+
if (!workspaceId) throw new Error(`Could not resolve a workspace id for ${target.label || requested}.`);
|
|
261
|
+
const written = writeProjectWorkspace(projectRoot, { workspaceId, key: target.key || null, label: target.label || null });
|
|
262
|
+
log(uiSuccess(`Workspace bound to this project: ${target.label || workspaceId}`));
|
|
263
|
+
log(` ${written}`);
|
|
264
|
+
log(' Commands run inside this repository use it. Use --global to change the machine-wide default instead.');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
216
268
|
const updateUrl = new URL(`/oauth/clients/${encodedClientId}/workspace-selection`, baseUrl).toString();
|
|
217
269
|
const updated = await httpJson(updateUrl, 'POST', { key: target.selectionKey }, authHeaders);
|
|
218
270
|
if (target.isDefault) {
|
package/lib/workspaceContext.js
CHANGED
|
@@ -1,76 +1,176 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
8
|
-
const {
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
const { httpJson } = require('./http');
|
|
8
|
+
const { getEnv } = require('./env');
|
|
9
|
+
const { resolveAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
|
|
10
|
+
const { readStoredOAuthClient } = require('./oauthClient');
|
|
11
|
+
const { readJsonIfExists, writeFile, ensureDir } = require('./fsUtils');
|
|
12
|
+
|
|
13
|
+
const WORKSPACE_CONTEXT_TIMEOUT_MS = 2000;
|
|
14
|
+
const SELECTED_WORKSPACE_PATH = path.join(os.homedir(), '.empirical', 'workspace-selection.json');
|
|
15
|
+
|
|
16
|
+
// Per-project binding (2026-09-24). The global selection above is one file per machine, so any
|
|
17
|
+
// session in any repo flips it for every other session: a WOB session left the CLI on the WOB team
|
|
18
|
+
// workspace and every tag-scoped recall from an empirical_v2 session returned [] while the memories
|
|
19
|
+
// existed. The unit that actually varies is the project directory, like git config, so a project
|
|
20
|
+
// can pin its workspace in <repo>/.empirical/workspace.json and `use` writes there by default when
|
|
21
|
+
// run inside a git repo. Resolution order, first hit wins: env, project, global, then the server's
|
|
22
|
+
// per-client selection exactly as before. No project file and no env means today's behavior.
|
|
23
|
+
const PROJECT_WORKSPACE_DIRNAME = '.empirical';
|
|
24
|
+
const PROJECT_WORKSPACE_FILENAME = 'workspace.json';
|
|
25
|
+
const WORKSPACE_ENV_VAR = 'EMPIRICAL_WORKSPACE_ID';
|
|
26
|
+
|
|
27
|
+
function readSelectedWorkspace() {
|
|
28
|
+
const value = readJsonIfExists(SELECTED_WORKSPACE_PATH);
|
|
29
|
+
return value && typeof value.workspaceId === 'string' ? value : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeSelectedWorkspace(workspace) {
|
|
33
|
+
ensureDir(path.dirname(SELECTED_WORKSPACE_PATH));
|
|
34
|
+
writeFile(SELECTED_WORKSPACE_PATH, `${JSON.stringify(workspace, null, 2)}\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function clearSelectedWorkspace() {
|
|
38
|
+
if (fs.existsSync(SELECTED_WORKSPACE_PATH)) fs.unlinkSync(SELECTED_WORKSPACE_PATH);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Nearest <dir>/.empirical/workspace.json walking up from `startDir` to the filesystem root.
|
|
43
|
+
* Returns { path, value } or null. A malformed file is ignored rather than fatal: a broken
|
|
44
|
+
* binding must not take the CLI down, it just falls through to the next source.
|
|
45
|
+
*/
|
|
46
|
+
function findProjectWorkspaceFile(startDir = process.cwd()) {
|
|
47
|
+
let dir = path.resolve(startDir);
|
|
48
|
+
for (;;) {
|
|
49
|
+
const candidate = path.join(dir, PROJECT_WORKSPACE_DIRNAME, PROJECT_WORKSPACE_FILENAME);
|
|
50
|
+
const value = readJsonIfExists(candidate);
|
|
51
|
+
if (value && typeof value.workspaceId === 'string' && value.workspaceId.trim()) {
|
|
52
|
+
return { path: candidate, value };
|
|
53
|
+
}
|
|
54
|
+
const parent = path.dirname(dir);
|
|
55
|
+
if (parent === dir) return null;
|
|
56
|
+
dir = parent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The git toplevel for `cwd`, or null outside a repo (or without git). Used to decide where
|
|
62
|
+
* `workspace use` writes by default and where the project binding lives.
|
|
63
|
+
*/
|
|
64
|
+
function findGitRoot(cwd = process.cwd()) {
|
|
65
|
+
try {
|
|
66
|
+
const out = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
67
|
+
cwd,
|
|
68
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
69
|
+
encoding: 'utf8'
|
|
70
|
+
});
|
|
71
|
+
const root = String(out || '').trim();
|
|
72
|
+
return root ? path.resolve(root) : null;
|
|
73
|
+
} catch (_error) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function projectWorkspacePathFor(rootDir) {
|
|
79
|
+
return path.join(rootDir, PROJECT_WORKSPACE_DIRNAME, PROJECT_WORKSPACE_FILENAME);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeProjectWorkspace(rootDir, workspace) {
|
|
83
|
+
const filePath = projectWorkspacePathFor(rootDir);
|
|
84
|
+
ensureDir(path.dirname(filePath));
|
|
85
|
+
writeFile(filePath, `${JSON.stringify({ ...workspace, boundAt: new Date().toISOString() }, null, 2)}\n`);
|
|
86
|
+
return filePath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clearProjectWorkspace(rootDir) {
|
|
90
|
+
const filePath = projectWorkspacePathFor(rootDir);
|
|
91
|
+
if (fs.existsSync(filePath)) {
|
|
92
|
+
fs.unlinkSync(filePath);
|
|
93
|
+
return filePath;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The single answer to "which workspace does this CLI invocation act on", before asking the
|
|
100
|
+
* server. Every memory command and `workspace current` read this, so reads and writes can never
|
|
101
|
+
* disagree again. Returns { workspaceId, label, key, source, path } or null when nothing local
|
|
102
|
+
* is set (the caller then falls back to the server's per-client selection).
|
|
103
|
+
*/
|
|
104
|
+
function resolveLocalWorkspaceSelection({ cwd = process.cwd(), env = process.env } = {}) {
|
|
105
|
+
const fromEnv = String(env[WORKSPACE_ENV_VAR] || '').trim();
|
|
106
|
+
if (fromEnv) {
|
|
107
|
+
return { workspaceId: fromEnv, label: null, key: null, source: 'env', path: WORKSPACE_ENV_VAR };
|
|
108
|
+
}
|
|
109
|
+
const project = findProjectWorkspaceFile(cwd);
|
|
110
|
+
if (project) {
|
|
111
|
+
const { workspaceId, label = null, key = null } = project.value;
|
|
112
|
+
return { workspaceId: workspaceId.trim(), label, key, source: 'project', path: project.path };
|
|
113
|
+
}
|
|
114
|
+
const global = readSelectedWorkspace();
|
|
115
|
+
if (global) {
|
|
116
|
+
return { workspaceId: global.workspaceId, label: global.label || null, key: global.key || null, source: 'global', path: SELECTED_WORKSPACE_PATH };
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function withSelectedWorkspaceContext(fn) {
|
|
122
|
+
const local = resolveLocalWorkspaceSelection();
|
|
123
|
+
if (local?.workspaceId) {
|
|
124
|
+
const previousWorkspaceId = globalThis.__empiricalCliWorkspaceId || null;
|
|
125
|
+
globalThis.__empiricalCliWorkspaceId = local.workspaceId;
|
|
126
|
+
try {
|
|
127
|
+
return await fn();
|
|
128
|
+
} finally {
|
|
129
|
+
globalThis.__empiricalCliWorkspaceId = previousWorkspaceId;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const accessToken = resolveAccessToken();
|
|
134
|
+
const clientId = getEnv('EMPIRICAL_CLIENT_ID') || readStoredOAuthClient()?.clientId;
|
|
135
|
+
if (!accessToken || !clientId) return fn();
|
|
136
|
+
|
|
137
|
+
const previousWorkspaceId = globalThis.__empiricalCliWorkspaceId || null;
|
|
138
|
+
globalThis.__empiricalCliWorkspaceId = null;
|
|
139
|
+
try {
|
|
140
|
+
const baseUrl = resolveApiBaseUrl();
|
|
141
|
+
const currentUrl = new URL(
|
|
142
|
+
`/oauth/clients/${encodeURIComponent(clientId)}/workspace-selection`,
|
|
143
|
+
baseUrl
|
|
144
|
+
).toString();
|
|
145
|
+
try {
|
|
146
|
+
const payload = await httpJson(
|
|
147
|
+
currentUrl,
|
|
148
|
+
'GET',
|
|
149
|
+
null,
|
|
150
|
+
buildApiAuthHeaders(accessToken),
|
|
151
|
+
WORKSPACE_CONTEXT_TIMEOUT_MS
|
|
152
|
+
);
|
|
153
|
+
globalThis.__empiricalCliWorkspaceId = payload?.current?.workspaceId || null;
|
|
154
|
+
} catch (_error) {
|
|
155
|
+
globalThis.__empiricalCliWorkspaceId = null;
|
|
156
|
+
}
|
|
157
|
+
return await fn();
|
|
158
|
+
} finally {
|
|
159
|
+
globalThis.__empiricalCliWorkspaceId = previousWorkspaceId;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
withSelectedWorkspaceContext,
|
|
165
|
+
resolveLocalWorkspaceSelection,
|
|
166
|
+
readSelectedWorkspace,
|
|
167
|
+
writeSelectedWorkspace,
|
|
168
|
+
clearSelectedWorkspace,
|
|
169
|
+
findProjectWorkspaceFile,
|
|
170
|
+
findGitRoot,
|
|
171
|
+
projectWorkspacePathFor,
|
|
172
|
+
writeProjectWorkspace,
|
|
173
|
+
clearProjectWorkspace,
|
|
174
|
+
WORKSPACE_ENV_VAR,
|
|
175
|
+
SELECTED_WORKSPACE_PATH
|
|
176
|
+
};
|
package/package.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@empirical-gauzza/cli",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
],
|
|
18
|
-
"type": "commonjs",
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"homepage": "https://empirical.gauzza.com/docs/cli-details",
|
|
21
|
-
"publishConfig": {
|
|
22
|
-
"access": "public"
|
|
23
|
-
},
|
|
24
|
-
"bin": {
|
|
25
|
-
"empirical": "bin/empirical.js"
|
|
26
|
-
},
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=18.0.0"
|
|
29
|
-
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"gray-matter": "^4.0.3"
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@empirical-gauzza/cli",
|
|
3
|
+
"version": "1.0.37",
|
|
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
|
+
],
|
|
18
|
+
"type": "commonjs",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://empirical.gauzza.com/docs/cli-details",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"empirical": "bin/empirical.js"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"gray-matter": "^4.0.3"
|
|
32
|
+
}
|
|
33
|
+
}
|