@empirical-gauzza/cli 1.0.38 → 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.
@@ -8,6 +8,7 @@ const { readStoredOAuthClient } = require('./oauthClient');
8
8
  const { deriveBaseUrlFromMcpUrl } = require('./constants');
9
9
  const { withCliAnalyticsContext } = require('./memoryCommands');
10
10
  const { runWorkspaceInterview, sanitizeForTerminal } = require('./setupCommands');
11
+ const { isManualShapeAction, runManualShapeCommand } = require('./shapeCommands');
11
12
  const { isInteractiveTerminal } = require('./empiricalTokensFormat');
12
13
  const {
13
14
  writeSelectedWorkspace,
@@ -52,7 +53,55 @@ async function resolveShapeTarget({ requested, local, answersFile, acceptCost, i
52
53
  function isWorkspaceCommand(args) {
53
54
  if (args[0] !== 'workspace') return false;
54
55
  if (['list', 'current', 'select', 'use', 'unbind', 'create', 'delete', 'shape'].includes(args[1])) return true;
55
- 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;
56
105
  }
57
106
 
58
107
  function describeSelectionSource(selection) {
@@ -71,7 +120,7 @@ function workspaceOptions(payload) {
71
120
  }
72
121
 
73
122
  function positionalArgs(args, startIndex) {
74
- 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']);
75
124
  const positional = [];
76
125
  for (let index = startIndex; index < args.length; index += 1) {
77
126
  if (valueOptions.has(args[index])) {
@@ -307,6 +356,13 @@ async function runWorkspaceCommand(args) {
307
356
  return;
308
357
  }
309
358
 
359
+ if (action === 'shape' && isManualShapeAction(args)) {
360
+ // Editing the shape by hand (show, set, edit, add-type, add-status, add-field): no interview, no charge.
361
+ // A dry run unless --apply, and a change is only applied to the shape that was previewed.
362
+ const bound = resolveLocalWorkspaceSelection();
363
+ return runManualShapeCommand({ args, baseUrl, authHeaders, resolveBound: () => (bound && bound.workspaceId) || '' });
364
+ }
365
+
310
366
  if (action === 'shape') {
311
367
  // Re-shapes a team workspace you manage through the interview. resolveShapeTarget decides which: an
312
368
  // explicit --workspace-id, or (at a terminal only, after a named yes) the bound workspace.
@@ -392,17 +448,23 @@ async function runWorkspaceCommand(args) {
392
448
  if (action === 'invite') {
393
449
  requireClientId(clientId);
394
450
  const inviteAction = args[2];
395
- const currentPayload = await httpJson(currentUrl, 'GET', null, authHeaders);
396
- const current = currentPayload?.current;
397
- if (!current?.key || current.key === 'default_brain') {
398
- 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
+ );
399
467
  }
400
- const teamPayload = await httpJson(teamBrainsUrl, 'GET', null, authHeaders);
401
- const target = resolveWorkspaceTarget(
402
- current.workspaceId || current.key,
403
- { workspaces: [{ key: current.key, label: current.label, workspaceId: current.workspaceId }] },
404
- teamPayload
405
- );
406
468
  if (!target?.workspaceId) {
407
469
  throw new Error('The active workspace is not a team workspace available for invites.');
408
470
  }
@@ -426,6 +488,11 @@ async function runWorkspaceCommand(args) {
426
488
  return;
427
489
  }
428
490
 
491
+ if (inviteAction === 'email') {
492
+ await runInviteEmail({ args, inviteUrl, authHeaders, target });
493
+ return;
494
+ }
495
+
429
496
  if (inviteAction === 'list') {
430
497
  const payload = await httpJson(inviteUrl, 'GET', null, authHeaders);
431
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.38",
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
+ }