@empirical-gauzza/cli 1.0.39 → 1.0.40

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/constants.js CHANGED
@@ -386,8 +386,10 @@ Resolution order for every memory command, first match wins: the \`EMPIRICAL_WOR
386
386
  empirical workspace invite create --role writer --expires-in-days 7 --max-uses 3
387
387
  empirical workspace invite list
388
388
  empirical workspace invite revoke <token>
389
+ empirical workspace invite email ana@example.com ben@example.com --role writer --note "Come help us review the book."
390
+ empirical workspace invite email ana@example.com --note-file ./invite-note.txt --from-name "Matt" --workspace-id ws_team_abc
389
391
 
390
- Invite commands operate on the active team workspace. Create prints a shareable URL.
392
+ Invite commands operate on the active team workspace, or the one named by \`--workspace-id\`. Create prints a shareable URL. Email sends each address its own invite (one address per seat, expires in 14 days) and prints who was invited and why anyone was not; \`--note\` (up to 1000 characters) appears in the email as your own words, and \`--from-name\` changes the name the email says it is from ("Matt invited you") for that send only; without it your account's display name is used.
391
393
 
392
394
  ### Soft-delete a demo workspace
393
395
 
package/lib/help.js CHANGED
@@ -60,6 +60,8 @@ function showHelp() {
60
60
  log(' Create a shareable invite link for the active team workspace');
61
61
  log(' empirical workspace invite list List active invites for the active team workspace');
62
62
  log(' empirical workspace invite revoke <token> Revoke a previously created invite');
63
+ log(' empirical workspace invite email <address...> [--role reader|writer] [--note <text> | --note-file <path>] [--from-name <name>]');
64
+ log(' Email each person an invite, with your own note in the email');
63
65
  log(' empirical workspace delete <number|name|id> [--confirm]');
64
66
  log(' Soft-delete a team workspace (dry-run without --confirm)');
65
67
  log('');
@@ -36,6 +36,17 @@ function pluginNamesForInstallerOptions(options = {}) {
36
36
  return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
37
37
  }
38
38
 
39
+ // Which detected CLIs to install for. At a terminal the person picks (Enter for all). With no
40
+ // terminal, which is how an agent like Claude Code runs `empirical install` for someone, there is
41
+ // nobody to answer, so it takes the default the list already shows: every detected CLI. Asking
42
+ // anyway read an empty stdin and stopped with "nothing installed", so the agent's install did
43
+ // nothing unless it knew to pipe an answer in.
44
+ async function chooseInstallTargets(targets, { interactive = Boolean(process.stdin.isTTY), askFn = ask } = {}) {
45
+ if (!interactive) return targets.map((_, index) => index);
46
+ const answer = await askFn(uiQuestion('Press Enter for all, or select numbers (e.g. 1,2): '));
47
+ return parseSelection(answer, targets.length);
48
+ }
49
+
39
50
  async function runInstallFlow() {
40
51
  log(uiHeader(`Empirical installer v${getCliVersion()}`));
41
52
  log('');
@@ -54,8 +65,10 @@ async function runInstallFlow() {
54
65
  });
55
66
  log('');
56
67
 
57
- const answer = await ask(uiQuestion('Press Enter for all, or select numbers (e.g. 1,2): '));
58
- const selectedIndexes = parseSelection(answer, targets.length);
68
+ const selectedIndexes = await chooseInstallTargets(targets);
69
+ if (!process.stdin.isTTY) {
70
+ log(`No terminal attached, so installing for every detected CLI: ${targets.map((t) => t.label).join(', ')}.`);
71
+ }
59
72
 
60
73
  if (selectedIndexes.length === 0) {
61
74
  log('No valid selection; nothing installed.');
@@ -407,5 +420,6 @@ async function runInstallFlow() {
407
420
 
408
421
  module.exports = {
409
422
  runInstallFlow,
410
- pluginNamesForInstallerOptions
423
+ pluginNamesForInstallerOptions,
424
+ chooseInstallTargets
411
425
  };
@@ -53,7 +53,55 @@ async function resolveShapeTarget({ requested, local, answersFile, acceptCost, i
53
53
  function isWorkspaceCommand(args) {
54
54
  if (args[0] !== 'workspace') return false;
55
55
  if (['list', 'current', 'select', 'use', 'unbind', 'create', 'delete', 'shape'].includes(args[1])) return true;
56
- return args[1] === 'invite' && ['create', 'list', 'revoke'].includes(args[2]);
56
+ return args[1] === 'invite' && ['create', 'list', 'revoke', 'email'].includes(args[2]);
57
+ }
58
+
59
+ // Mirrors WORKSPACE_INVITE_NOTE_MAX in @empirical/shared for a local pre-check only; the CLI is
60
+ // published on its own and cannot import it. The server re-checks and is the one that decides.
61
+ const INVITE_NOTE_MAX = 1000;
62
+
63
+ const NOT_SENT_REASONS = {
64
+ invalid_email: 'not a valid email address',
65
+ already_member: 'already a member',
66
+ already_invited: 'already has a pending invite',
67
+ seat_limit: 'the workspace has no free seat',
68
+ recipient_suppressed: 'this address has bounced or unsubscribed before',
69
+ email_not_configured: 'email is not set up on this server',
70
+ send_failed: 'the email could not be sent'
71
+ };
72
+
73
+ // empirical workspace invite email <address...> [--role reader|writer] [--note <text> | --note-file <path>] [--from-name <name>]
74
+ async function runInviteEmail({ args, inviteUrl, authHeaders, target }) {
75
+ const emails = positionalArgs(args, 3).join(' ').split(/[\s,;]+/).map((e) => e.trim()).filter(Boolean);
76
+ if (!emails.length) {
77
+ throw new Error('Usage: empirical workspace invite email <address...> [--role reader|writer] [--note <text> | --note-file <path>] [--from-name <name>] [--workspace-id <id>]');
78
+ }
79
+ const role = parseOption(args, '--role', 'reader');
80
+ if (!['reader', 'writer'].includes(role)) throw new Error('--role must be reader or writer for an emailed invite.');
81
+ const noteFile = parseOption(args, '--note-file', '');
82
+ let note = parseOption(args, '--note', '');
83
+ if (noteFile) {
84
+ if (note) throw new Error('Use --note or --note-file, not both.');
85
+ note = require('fs').readFileSync(noteFile, 'utf8');
86
+ }
87
+ note = String(note || '').replace(/\r\n?/g, '\n').trim();
88
+ if (note.length > INVITE_NOTE_MAX) throw new Error(`Keep the note under ${INVITE_NOTE_MAX} characters (it is ${note.length}).`);
89
+ // The name the email says it is from, for this send only. Empty means your account's display name.
90
+ const fromName = String(parseOption(args, '--from-name', '') || '').replace(/\s+/g, ' ').trim();
91
+ if (fromName && fromName.includes('@')) throw new Error('--from-name takes a name, not an email address.');
92
+ if (fromName && (fromName.length < 2 || fromName.length > 64)) throw new Error('--from-name must be 2 to 64 characters.');
93
+
94
+ const payload = await httpJson(`${inviteUrl}/email`, 'POST', { emails, accessRole: role, ...(note ? { note } : {}), ...(fromName ? { fromName } : {}) }, authHeaders);
95
+ const results = Array.isArray(payload?.results) ? payload.results : [];
96
+ const sent = results.filter((r) => r.sent);
97
+ const notSent = results.filter((r) => !r.sent);
98
+ if (sent.length) log(uiSuccess(`Invited to ${target.label} as ${role}: ${sent.map((r) => r.email).join(', ')}`));
99
+ for (const r of notSent) {
100
+ log(`Not sent to ${r.email}: ${NOT_SENT_REASONS[r.skippedReason] || r.skippedReason || 'unknown reason'}.${r.detail ? ` ${r.detail}` : ''}`);
101
+ }
102
+ if (sent.length && payload?.fromName) log(`Sent as ${payload.fromName}.`);
103
+ if (sent.length && note) log('Your note was included.');
104
+ if (!sent.length) process.exitCode = 1;
57
105
  }
58
106
 
59
107
  function describeSelectionSource(selection) {
@@ -72,7 +120,7 @@ function workspaceOptions(payload) {
72
120
  }
73
121
 
74
122
  function positionalArgs(args, startIndex) {
75
- const valueOptions = new Set(['--description', '--role', '--expires-in-days', '--max-uses', '--answers', '--workspace-id']);
123
+ const valueOptions = new Set(['--description', '--role', '--expires-in-days', '--max-uses', '--answers', '--workspace-id', '--note', '--note-file', '--from-name']);
76
124
  const positional = [];
77
125
  for (let index = startIndex; index < args.length; index += 1) {
78
126
  if (valueOptions.has(args[index])) {
@@ -400,17 +448,23 @@ async function runWorkspaceCommand(args) {
400
448
  if (action === 'invite') {
401
449
  requireClientId(clientId);
402
450
  const inviteAction = args[2];
403
- const currentPayload = await httpJson(currentUrl, 'GET', null, authHeaders);
404
- const current = currentPayload?.current;
405
- if (!current?.key || current.key === 'default_brain') {
406
- throw new Error('Workspace invites require an active team workspace. Use `empirical workspace use <workspace>` first.');
451
+ const namedWorkspace = parseOption(args, '--workspace-id', '');
452
+ let target;
453
+ if (namedWorkspace) {
454
+ target = { workspaceId: namedWorkspace, label: namedWorkspace };
455
+ } else {
456
+ const currentPayload = await httpJson(currentUrl, 'GET', null, authHeaders);
457
+ const current = currentPayload?.current;
458
+ if (!current?.key || current.key === 'default_brain') {
459
+ throw new Error('Workspace invites require an active team workspace. Use `empirical workspace use <workspace>` first, or pass --workspace-id.');
460
+ }
461
+ const teamPayload = await httpJson(teamBrainsUrl, 'GET', null, authHeaders);
462
+ target = resolveWorkspaceTarget(
463
+ current.workspaceId || current.key,
464
+ { workspaces: [{ key: current.key, label: current.label, workspaceId: current.workspaceId }] },
465
+ teamPayload
466
+ );
407
467
  }
408
- const teamPayload = await httpJson(teamBrainsUrl, 'GET', null, authHeaders);
409
- const target = resolveWorkspaceTarget(
410
- current.workspaceId || current.key,
411
- { workspaces: [{ key: current.key, label: current.label, workspaceId: current.workspaceId }] },
412
- teamPayload
413
- );
414
468
  if (!target?.workspaceId) {
415
469
  throw new Error('The active workspace is not a team workspace available for invites.');
416
470
  }
@@ -434,6 +488,11 @@ async function runWorkspaceCommand(args) {
434
488
  return;
435
489
  }
436
490
 
491
+ if (inviteAction === 'email') {
492
+ await runInviteEmail({ args, inviteUrl, authHeaders, target });
493
+ return;
494
+ }
495
+
437
496
  if (inviteAction === 'list') {
438
497
  const payload = await httpJson(inviteUrl, 'GET', null, authHeaders);
439
498
  const invites = Array.isArray(payload?.invites) ? payload.invites : [];
package/package.json CHANGED
@@ -1,33 +1,33 @@
1
- {
2
- "name": "@empirical-gauzza/cli",
3
- "version": "1.0.39",
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.40",
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
+ }