@empirical-gauzza/cli 1.0.27 → 1.0.29

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 CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  } = require('../lib/auth');
18
18
  const { isWorkspaceCommand, runWorkspaceCommand } = require('../lib/workspaceCommands');
19
19
  const { runMemoryCommand } = require('../lib/memoryCommands');
20
+ const { runAlertCommand } = require('../lib/alertCommands');
20
21
  const { runKitCommand, runSkillCommand, runDeviceCommand } = require('../lib/kitCommands');
21
22
  const { runDoctor } = require('../lib/doctor');
22
23
  const { runDoc } = require('../lib/doc');
@@ -91,6 +92,11 @@ async function main() {
91
92
  return;
92
93
  }
93
94
 
95
+ if (args[0] === 'alert') {
96
+ await runAlertCommand(args.slice(1));
97
+ return;
98
+ }
99
+
94
100
  if (args[0] === 'kit') {
95
101
  await runKitCommand(args.slice(1));
96
102
  return;
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ // Plain-text rendering for a human reading stderr directly — deliberately NOT the MCP wording
4
+ // (renderAlertNotice's "NOTICE FOR THE USER - relay this verbatim"), which instructs a host
5
+ // MODEL to relay text to a person who can't see stderr at all. Here the person IS the reader.
6
+ const ANSI = {
7
+ reset: '\x1b[0m',
8
+ bold: '\x1b[1m',
9
+ yellow: '\x1b[33m',
10
+ red: '\x1b[31m',
11
+ cyan: '\x1b[36m'
12
+ };
13
+
14
+ function supportsColorStderr() {
15
+ return Boolean(process.stderr && process.stderr.isTTY) && !process.env.NO_COLOR;
16
+ }
17
+
18
+ const TONE_PREFIX = { info: 'i', success: '✓', warning: '!', error: '✗' };
19
+ const TONE_COLOR = { info: ANSI.cyan, success: ANSI.cyan, warning: ANSI.yellow, error: ANSI.red };
20
+
21
+ function colorize(code, text) {
22
+ if (!supportsColorStderr()) return text;
23
+ return `${code}${text}${ANSI.reset}`;
24
+ }
25
+
26
+ function renderOneAlert(alert) {
27
+ const prefix = TONE_PREFIX[alert.tone] || TONE_PREFIX.info;
28
+ const color = TONE_COLOR[alert.tone] || TONE_COLOR.info;
29
+ const lines = [];
30
+ lines.push(colorize(`${ANSI.bold}${color}`, `${prefix} ${alert.title}`));
31
+ lines.push(` ${alert.body}`);
32
+ for (const action of alert.actions || []) {
33
+ lines.push(` → ${action.label}: ${action.href}`);
34
+ }
35
+ lines.push(` (dismiss: empirical alert dismiss --alert-id ${alert.id})`);
36
+ return lines.join('\n');
37
+ }
38
+
39
+ function renderCliAlertBanner(alerts) {
40
+ if (!Array.isArray(alerts) || !alerts.length) return '';
41
+ return alerts.map(renderOneAlert).join('\n\n') + '\n';
42
+ }
43
+
44
+ module.exports = { renderCliAlertBanner };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
4
+ const { httpJson, printApiError } = require('./http');
5
+ const { log, warn } = require('./ui');
6
+
7
+ function parseOption(args, name, fallback) {
8
+ const idx = args.indexOf(name);
9
+ if (idx === -1 || idx === args.length - 1) return fallback;
10
+ return args[idx + 1];
11
+ }
12
+
13
+ async function runAlertList(_args) {
14
+ const accessToken = requireAccessToken();
15
+ const baseUrl = resolveApiBaseUrl();
16
+ const url = new URL('/api/v1/alerts', baseUrl).toString();
17
+ let response;
18
+ try {
19
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
20
+ } catch (error) {
21
+ printApiError(error);
22
+ process.exit(1);
23
+ return;
24
+ }
25
+ const data = response && response.data ? response.data : response;
26
+ log(JSON.stringify(data.alerts !== undefined ? data.alerts : data));
27
+ }
28
+
29
+ async function runAlertDismiss(args) {
30
+ const alertId = parseOption(args, '--alert-id', '');
31
+ if (!alertId) {
32
+ warn('Usage: empirical alert dismiss --alert-id <id>');
33
+ process.exit(1);
34
+ return;
35
+ }
36
+ const accessToken = requireAccessToken();
37
+ const baseUrl = resolveApiBaseUrl();
38
+ const url = new URL(`/api/v1/alerts/${encodeURIComponent(alertId)}/dismiss`, baseUrl).toString();
39
+ let response;
40
+ try {
41
+ response = await httpJson(url, 'POST', undefined, buildApiAuthHeaders(accessToken));
42
+ } catch (error) {
43
+ printApiError(error);
44
+ process.exit(1);
45
+ return;
46
+ }
47
+ const data = response && response.data ? response.data : response;
48
+ log(JSON.stringify(data));
49
+ }
50
+
51
+ async function runAlertCommand(subArgs) {
52
+ const sub = subArgs[0] && !subArgs[0].startsWith('-') ? subArgs[0] : undefined;
53
+ const rest = subArgs.slice(1);
54
+
55
+ if (sub === 'list') return runAlertList(rest);
56
+ if (sub === 'dismiss') return runAlertDismiss(rest);
57
+
58
+ warn('Usage: empirical alert <list|dismiss> [options]');
59
+ process.exit(1);
60
+ }
61
+
62
+ module.exports = { runAlertCommand };
package/lib/constants.js CHANGED
@@ -8,12 +8,21 @@ const EMPIRICAL_MCP_LEGACY_NAMES = ['empirical-live'];
8
8
  const EMPIRICAL_MCP_URL = process.env.EMPIRICAL_MCP_URL || 'https://empirical.gauzza.com/mcp';
9
9
  const COPILOT_OAUTH_CLIENT_ID = process.env.EMPIRICAL_COPILOT_OAUTH_CLIENT_ID || 'copilot-cli-empirical';
10
10
 
11
+ // These two lists mirror the schema-level enums in packages/shared/src/memoryGraph.ts.
12
+ // They are duplicated rather than imported on purpose: this CLI publishes to npm as
13
+ // @empirical-gauzza/cli, and @empirical/shared is a private workspace package, so depending
14
+ // on it would break `npm install` for end users. tests/unit/cliMemoryEnumParity.test.js
15
+ // fails CI if the copies ever diverge.
11
16
  const MEMORY_CATEGORIES = [
12
17
  'health', 'fitness', 'food', 'supplement', 'mood', 'idea', 'business',
13
18
  'build', 'growth', 'learning', 'leadership', 'operations', 'workflow',
14
19
  'lead', 'project', 'recipe', 'preference', 'reflection', 'relationship', 'heartbeat'
15
20
  ];
16
21
 
22
+ const NODE_TYPES = [
23
+ 'event', 'entity', 'observation', 'reflection', 'goal', 'concept'
24
+ ];
25
+
17
26
  const DOC_TOPICS = {
18
27
  memory: `# Empirical Memory Commands
19
28
 
@@ -33,9 +42,26 @@ Flags:
33
42
  - \`--top-k\`: number of results, 1–100, default 3
34
43
  - \`--category\`: filter by category
35
44
  - \`--tags\`: comma-separated tags filter
45
+ - \`--tags-all\`: comma-separated tags that must ALL be present (AND match)
46
+ - \`--status\`: comma-separated status values to keep (for example open,in_progress)
47
+ - \`--path\`: pin one retrieval pathway (see below)
48
+ - \`--hops\`: 1 or 2, graph depth; only valid with \`--path graph\` or \`--path hebbian\`
36
49
  - \`--pretty\`: pretty-print JSON output
37
50
  - \`--format table\`: render results as a table instead of JSON (JSON stays the default)
38
51
 
52
+ #### Retrieval pathways
53
+
54
+ - \`--path semantic\`: ranked meaning match. Runs the embedding search and scores candidates.
55
+ Use it when you are asking a question and want the closest few answers.
56
+ - \`--path list\`: filtered browse. Skips ranking and returns the matching set in order, with
57
+ paging. Use it when you want everything carrying a tag, status, or category, not a top few.
58
+ - \`--path graph\`: relationship walk from the best entry points, out \`--hops\` steps. Use it
59
+ when what you want is connected to what you can name rather than worded like it.
60
+ - \`--path hebbian\`: co-recall walk, following the links that repeated retrieval strengthened.
61
+ Use it when you want what usually comes up alongside this, not what merely resembles it.
62
+
63
+ Omitting \`--path\` keeps the default, where the classifier decides which pathway answers.
64
+
39
65
  ### get
40
66
  Retrieve one memory by exact ID.
41
67
 
@@ -85,6 +111,18 @@ Flags:
85
111
  - \`--node-type\`: node type, default \`observation\`
86
112
  - \`--json-file\`: path to JSON file with full memory payload
87
113
 
114
+ Both \`--category\` and \`--node-type\` are closed sets. Do not guess a value: anything outside
115
+ these lists is rejected and the write does not happen.
116
+
117
+ Valid categories (20): health, fitness, food, supplement, mood, idea, business, build, growth,
118
+ learning, leadership, operations, workflow, lead, project, recipe, preference, reflection,
119
+ relationship, heartbeat.
120
+
121
+ Valid node types (6): event, entity, observation, reflection, goal, concept.
122
+
123
+ There is no \`product\` category and no \`decision\` node type. Record a decision as
124
+ \`--category project --node-type event\`.
125
+
88
126
  ### note
89
127
  Add a note to an existing memory.
90
128
 
@@ -595,6 +633,7 @@ module.exports = {
595
633
  EMPIRICAL_MCP_URL,
596
634
  COPILOT_OAUTH_CLIENT_ID,
597
635
  MEMORY_CATEGORIES,
636
+ NODE_TYPES,
598
637
  DOC_TOPICS,
599
638
  getCliVersion,
600
639
  deriveBaseUrlFromMcpUrl
package/lib/http.js CHANGED
@@ -1,187 +1,192 @@
1
- 'use strict';
2
-
3
- const http = require('http');
4
- const https = require('https');
5
- const { warn } = require('./ui');
6
-
7
- function httpGet(urlString, timeoutMs = 8000) {
8
- return new Promise((resolve, reject) => {
9
- const parsed = new URL(urlString);
10
- const lib = parsed.protocol === 'https:' ? https : http;
11
- const req = lib.request(
12
- parsed,
13
- {
14
- method: 'GET',
15
- headers: {
16
- 'User-Agent': 'empirical-cli/1.0'
17
- }
18
- },
19
- (res) => {
20
- let body = '';
21
- res.setEncoding('utf8');
22
- res.on('data', (chunk) => {
23
- body += chunk;
24
- });
25
- res.on('end', () => {
26
- resolve({
27
- statusCode: res.statusCode || 0,
28
- headers: res.headers || {},
29
- body
30
- });
31
- });
32
- }
33
- );
34
-
35
- req.setTimeout(timeoutMs, () => {
36
- req.destroy(new Error('request timeout'));
37
- });
38
- req.on('error', reject);
39
- req.end();
40
- });
41
- }
42
-
43
- function httpErrorFromResponse(method, urlString, statusCode, data) {
44
- const baseMsg = data.error_description || data.message || data.error || `HTTP ${statusCode}`;
45
- const upgradeUrl = data.upgrade_url || data.details?.upgradeUrl || null;
46
- const retryAfterSeconds = data.retry_after_seconds ?? null;
47
-
48
- const parts = [`${method} ${urlString} failed: ${baseMsg}`];
49
- if (retryAfterSeconds != null) parts.push(`Retry after ${retryAfterSeconds}s.`);
50
- if (upgradeUrl) parts.push(`Upgrade: ${upgradeUrl}`);
51
-
52
- const error = new Error(parts.join(' '));
53
- error.statusCode = statusCode;
54
- error.code = data.code || null;
55
- error.upgradeUrl = upgradeUrl;
56
- error.retryAfterSeconds = retryAfterSeconds;
57
- return error;
58
- }
59
-
60
- // Prints a structured JSON error line to stderr so scripts/agents parsing CLI
61
- // output can branch on `code` (e.g. RATE_LIMITED, PLAN_LIMIT_MEMORY_REACHED)
62
- // and act on `upgradeUrl`/`retryAfterSeconds` without string-parsing `message`.
63
- function printApiError(error) {
64
- warn(JSON.stringify({
65
- error: true,
66
- code: error.code || 'api_error',
67
- message: error.message,
68
- ...(error.retryAfterSeconds != null && { retryAfterSeconds: error.retryAfterSeconds }),
69
- ...(error.upgradeUrl && { upgradeUrl: error.upgradeUrl })
70
- }));
71
- }
72
-
73
- function httpJson(urlString, method, payload, headers = {}, timeoutMs = 0) {
74
- return new Promise((resolve, reject) => {
75
- const parsed = new URL(urlString);
76
- const lib = parsed.protocol === 'https:' ? https : http;
77
- const body = payload ? JSON.stringify(payload) : null;
78
-
79
- const req = lib.request(
80
- parsed,
81
- {
82
- method,
83
- headers: {
84
- Accept: 'application/json',
85
- // Identifies the client so the server can tell a CLI write from a portal write
86
- // (services/memoryAuthorship.ts). Writes also send source: 'cli' in the body,
87
- // which is authoritative; this covers every other endpoint.
88
- 'User-Agent': 'empirical-cli/1.0',
89
- ...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } : {}),
90
- ...headers
91
- }
92
- },
93
- (res) => {
94
- let raw = '';
95
- res.setEncoding('utf8');
96
- res.on('data', (chunk) => {
97
- raw += chunk;
98
- });
99
- res.on('end', () => {
100
- let data = {};
101
- try {
102
- data = raw ? JSON.parse(raw) : {};
103
- } catch (_error) {
104
- data = { raw };
105
- }
106
-
107
- const statusCode = res.statusCode || 0;
108
- if (statusCode < 200 || statusCode >= 300) {
109
- reject(httpErrorFromResponse(method, urlString, statusCode, data));
110
- return;
111
- }
112
-
113
- resolve(data);
114
- });
115
- }
116
- );
117
-
118
- req.on('error', reject);
119
- if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
120
- req.setTimeout(timeoutMs, () => {
121
- req.destroy(new Error(`request timeout after ${timeoutMs}ms`));
122
- });
123
- }
124
- if (body) req.write(body);
125
- req.end();
126
- });
127
- }
128
-
129
- function httpForm(urlString, method, params, headers = {}) {
130
- return new Promise((resolve, reject) => {
131
- const parsed = new URL(urlString);
132
- const lib = parsed.protocol === 'https:' ? https : http;
133
- const body = new URLSearchParams(params).toString();
134
-
135
- const req = lib.request(
136
- parsed,
137
- {
138
- method,
139
- headers: {
140
- Accept: 'application/json',
141
- // Identifies the client so the server can tell a CLI write from a portal write
142
- // (services/memoryAuthorship.ts). Writes also send source: 'cli' in the body,
143
- // which is authoritative; this covers every other endpoint.
144
- 'User-Agent': 'empirical-cli/1.0',
145
- 'Content-Type': 'application/x-www-form-urlencoded',
146
- 'Content-Length': Buffer.byteLength(body),
147
- ...headers
148
- }
149
- },
150
- (res) => {
151
- let raw = '';
152
- res.setEncoding('utf8');
153
- res.on('data', (chunk) => {
154
- raw += chunk;
155
- });
156
- res.on('end', () => {
157
- let data = {};
158
- try {
159
- data = raw ? JSON.parse(raw) : {};
160
- } catch (_error) {
161
- data = { raw };
162
- }
163
-
164
- const statusCode = res.statusCode || 0;
165
- if (statusCode < 200 || statusCode >= 300) {
166
- reject(httpErrorFromResponse(method, urlString, statusCode, data));
167
- return;
168
- }
169
-
170
- resolve(data);
171
- });
172
- }
173
- );
174
-
175
- req.on('error', reject);
176
- req.write(body);
177
- req.end();
178
- });
179
- }
180
-
181
- module.exports = {
182
- httpGet,
183
- httpErrorFromResponse,
184
- printApiError,
185
- httpJson,
186
- httpForm
187
- };
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const { warn } = require('./ui');
6
+ const { renderCliAlertBanner } = require('./alertBanner');
7
+
8
+ function httpGet(urlString, timeoutMs = 8000) {
9
+ return new Promise((resolve, reject) => {
10
+ const parsed = new URL(urlString);
11
+ const lib = parsed.protocol === 'https:' ? https : http;
12
+ const req = lib.request(
13
+ parsed,
14
+ {
15
+ method: 'GET',
16
+ headers: {
17
+ 'User-Agent': 'empirical-cli/1.0'
18
+ }
19
+ },
20
+ (res) => {
21
+ let body = '';
22
+ res.setEncoding('utf8');
23
+ res.on('data', (chunk) => {
24
+ body += chunk;
25
+ });
26
+ res.on('end', () => {
27
+ resolve({
28
+ statusCode: res.statusCode || 0,
29
+ headers: res.headers || {},
30
+ body
31
+ });
32
+ });
33
+ }
34
+ );
35
+
36
+ req.setTimeout(timeoutMs, () => {
37
+ req.destroy(new Error('request timeout'));
38
+ });
39
+ req.on('error', reject);
40
+ req.end();
41
+ });
42
+ }
43
+
44
+ function httpErrorFromResponse(method, urlString, statusCode, data) {
45
+ const baseMsg = data.error_description || data.message || data.error || `HTTP ${statusCode}`;
46
+ const upgradeUrl = data.upgrade_url || data.details?.upgradeUrl || null;
47
+ const retryAfterSeconds = data.retry_after_seconds ?? null;
48
+
49
+ const parts = [`${method} ${urlString} failed: ${baseMsg}`];
50
+ if (retryAfterSeconds != null) parts.push(`Retry after ${retryAfterSeconds}s.`);
51
+ if (upgradeUrl) parts.push(`Upgrade: ${upgradeUrl}`);
52
+
53
+ const error = new Error(parts.join(' '));
54
+ error.statusCode = statusCode;
55
+ error.code = data.code || null;
56
+ error.upgradeUrl = upgradeUrl;
57
+ error.retryAfterSeconds = retryAfterSeconds;
58
+ return error;
59
+ }
60
+
61
+ // Prints a structured JSON error line to stderr so scripts/agents parsing CLI
62
+ // output can branch on `code` (e.g. RATE_LIMITED, PLAN_LIMIT_MEMORY_REACHED)
63
+ // and act on `upgradeUrl`/`retryAfterSeconds` without string-parsing `message`.
64
+ function printApiError(error) {
65
+ warn(JSON.stringify({
66
+ error: true,
67
+ code: error.code || 'api_error',
68
+ message: error.message,
69
+ ...(error.retryAfterSeconds != null && { retryAfterSeconds: error.retryAfterSeconds }),
70
+ ...(error.upgradeUrl && { upgradeUrl: error.upgradeUrl })
71
+ }));
72
+ }
73
+
74
+ function httpJson(urlString, method, payload, headers = {}, timeoutMs = 0) {
75
+ return new Promise((resolve, reject) => {
76
+ const parsed = new URL(urlString);
77
+ const lib = parsed.protocol === 'https:' ? https : http;
78
+ const body = payload ? JSON.stringify(payload) : null;
79
+
80
+ const req = lib.request(
81
+ parsed,
82
+ {
83
+ method,
84
+ headers: {
85
+ Accept: 'application/json',
86
+ // Identifies the client so the server can tell a CLI write from a portal write
87
+ // (services/memoryAuthorship.ts). Writes also send source: 'cli' in the body,
88
+ // which is authoritative; this covers every other endpoint.
89
+ 'User-Agent': 'empirical-cli/1.0',
90
+ ...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } : {}),
91
+ ...headers
92
+ }
93
+ },
94
+ (res) => {
95
+ let raw = '';
96
+ res.setEncoding('utf8');
97
+ res.on('data', (chunk) => {
98
+ raw += chunk;
99
+ });
100
+ res.on('end', () => {
101
+ let data = {};
102
+ try {
103
+ data = raw ? JSON.parse(raw) : {};
104
+ } catch (_error) {
105
+ data = { raw };
106
+ }
107
+
108
+ if (data && Array.isArray(data.alerts) && data.alerts.length) {
109
+ process.stderr.write(renderCliAlertBanner(data.alerts));
110
+ }
111
+
112
+ const statusCode = res.statusCode || 0;
113
+ if (statusCode < 200 || statusCode >= 300) {
114
+ reject(httpErrorFromResponse(method, urlString, statusCode, data));
115
+ return;
116
+ }
117
+
118
+ resolve(data);
119
+ });
120
+ }
121
+ );
122
+
123
+ req.on('error', reject);
124
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
125
+ req.setTimeout(timeoutMs, () => {
126
+ req.destroy(new Error(`request timeout after ${timeoutMs}ms`));
127
+ });
128
+ }
129
+ if (body) req.write(body);
130
+ req.end();
131
+ });
132
+ }
133
+
134
+ function httpForm(urlString, method, params, headers = {}) {
135
+ return new Promise((resolve, reject) => {
136
+ const parsed = new URL(urlString);
137
+ const lib = parsed.protocol === 'https:' ? https : http;
138
+ const body = new URLSearchParams(params).toString();
139
+
140
+ const req = lib.request(
141
+ parsed,
142
+ {
143
+ method,
144
+ headers: {
145
+ Accept: 'application/json',
146
+ // Identifies the client so the server can tell a CLI write from a portal write
147
+ // (services/memoryAuthorship.ts). Writes also send source: 'cli' in the body,
148
+ // which is authoritative; this covers every other endpoint.
149
+ 'User-Agent': 'empirical-cli/1.0',
150
+ 'Content-Type': 'application/x-www-form-urlencoded',
151
+ 'Content-Length': Buffer.byteLength(body),
152
+ ...headers
153
+ }
154
+ },
155
+ (res) => {
156
+ let raw = '';
157
+ res.setEncoding('utf8');
158
+ res.on('data', (chunk) => {
159
+ raw += chunk;
160
+ });
161
+ res.on('end', () => {
162
+ let data = {};
163
+ try {
164
+ data = raw ? JSON.parse(raw) : {};
165
+ } catch (_error) {
166
+ data = { raw };
167
+ }
168
+
169
+ const statusCode = res.statusCode || 0;
170
+ if (statusCode < 200 || statusCode >= 300) {
171
+ reject(httpErrorFromResponse(method, urlString, statusCode, data));
172
+ return;
173
+ }
174
+
175
+ resolve(data);
176
+ });
177
+ }
178
+ );
179
+
180
+ req.on('error', reject);
181
+ req.write(body);
182
+ req.end();
183
+ });
184
+ }
185
+
186
+ module.exports = {
187
+ httpGet,
188
+ httpErrorFromResponse,
189
+ printApiError,
190
+ httpJson,
191
+ httpForm
192
+ };