@empirical-gauzza/cli 1.0.36 → 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.
@@ -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>]');
@@ -52,6 +53,15 @@ function showHelp() {
52
53
  log(' empirical workspace delete <number|name|id> [--confirm]');
53
54
  log(' Soft-delete a team workspace (dry-run without --confirm)');
54
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('');
55
65
  log('Options:');
56
66
  log(' install Interactive installer for Codex/Copilot/Claude CLI integration');
57
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
- function printApiError(error) {
65
- warn(JSON.stringify({
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
@@ -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
@@ -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
- summary: truncateForTable(m.summary, 60),
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
@@ -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