@empirical-gauzza/cli 1.0.21 → 1.0.22

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.
@@ -1,144 +1,144 @@
1
- 'use strict';
2
-
3
- const { log, warn } = require('./ui');
4
- const { httpJson, printApiError } = require('./http');
5
- const { parseOption } = require('./env');
6
- const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
7
- const { readPolicyCache, writePolicyCache } = require('./deviceConfig');
8
-
9
- const POLICY_CACHE_TTL_MS = 15 * 60 * 1000;
10
-
11
- function policyUrl(baseUrl) {
12
- return new URL('/api/v1/users/me/empirical-policy', baseUrl).toString();
13
- }
14
-
15
- function isPolicyCacheFresh(cache, now = Date.now()) {
16
- return Boolean(
17
- cache &&
18
- cache.policy &&
19
- cache.updatedAt &&
20
- now - new Date(cache.cachedAt || 0).getTime() < POLICY_CACHE_TTL_MS
21
- );
22
- }
23
-
24
- async function fetchPolicy({ force = false } = {}) {
25
- const cached = readPolicyCache();
26
- if (!force && isPolicyCacheFresh(cached)) return { ...cached, source: 'cache' };
27
-
28
- const accessToken = requireAccessToken();
29
- const response = await httpJson(
30
- policyUrl(resolveApiBaseUrl()),
31
- 'GET',
32
- null,
33
- buildApiAuthHeaders(accessToken)
34
- );
35
- const data = response?.data || response;
36
- const next = {
37
- policy: data.policy || {},
38
- policyVersion: data.policyVersion || 1,
39
- updatedAt: data.updatedAt || null,
40
- cachedAt: new Date().toISOString()
41
- };
42
- writePolicyCache(next);
43
- return { ...next, source: 'remote' };
44
- }
45
-
46
- function policyFromInstallerOptions(options = {}) {
47
- return {
48
- memoryFirstWorkflows: options.requireMemoryInWorkflows === false
49
- ? []
50
- : (options.requiredWorkflows || []),
51
- allowDiscretionaryUse: options.allowDiscretionaryMemoryUsage !== false,
52
- stopOnMemoryFailure: options.stopOnMemoryToolFailure !== false,
53
- rememberCodingStyle: options.rememberCodingStyle !== false,
54
- rememberArchitectureDecisions: options.rememberArchitectureDecisions !== false,
55
- recordWorkHistory: options.recordWorkHistory !== false,
56
- recordScarTissue: options.recordScarTissue !== false,
57
- recordTodos: options.recordTodos === true,
58
- storeInstructionArtifacts: options.storeInstructionArtifacts === true,
59
- checkMemoryBeforeAskingContext: options.checkMemoryBeforeAskingContext !== false,
60
- durableWritesOnly: options.durableWritesOnly !== false
61
- };
62
- }
63
-
64
- async function savePolicy(policy, accessTokenOverride = null) {
65
- const accessToken = accessTokenOverride || requireAccessToken();
66
- const response = await httpJson(
67
- policyUrl(resolveApiBaseUrl()),
68
- 'PATCH',
69
- { policy },
70
- buildApiAuthHeaders(accessToken)
71
- );
72
- const data = response?.data || response;
73
- writePolicyCache({
74
- policy: data.policy || {},
75
- policyVersion: data.policyVersion || 1,
76
- updatedAt: data.updatedAt || null,
77
- cachedAt: new Date().toISOString()
78
- });
79
- return data;
80
- }
81
-
82
- async function runPolicyCommand(args = []) {
83
- const subcommand = args[0] || 'show';
84
- if (subcommand === 'show') {
85
- try {
86
- const result = await fetchPolicy({ force: args.includes('--refresh') });
87
- log(JSON.stringify(result, null, 2));
88
- } catch (error) {
89
- printApiError(error);
90
- process.exitCode = 1;
91
- }
92
- return;
93
- }
94
-
95
- if (subcommand === 'set') {
96
- const raw = parseOption(args, '--json', '');
97
- if (!raw) {
98
- warn('Usage: empirical policy set --json \'{"allowDiscretionaryUse":false}\'');
99
- process.exitCode = 1;
100
- return;
101
- }
102
- let policy;
103
- try {
104
- policy = JSON.parse(raw);
105
- } catch (error) {
106
- warn(`Invalid policy JSON: ${error.message}`);
107
- process.exitCode = 1;
108
- return;
109
- }
110
- try {
111
- const accessToken = requireAccessToken();
112
- const response = await httpJson(
113
- policyUrl(resolveApiBaseUrl()),
114
- 'PATCH',
115
- { policy },
116
- buildApiAuthHeaders(accessToken)
117
- );
118
- const data = response?.data || response;
119
- writePolicyCache({
120
- policy: data.policy || {},
121
- policyVersion: data.policyVersion || 1,
122
- updatedAt: data.updatedAt || null,
123
- cachedAt: new Date().toISOString()
124
- });
125
- log(JSON.stringify(data, null, 2));
126
- } catch (error) {
127
- printApiError(error);
128
- process.exitCode = 1;
129
- }
130
- return;
131
- }
132
-
133
- warn('Usage: empirical policy <show [--refresh]|set --json <policy>>');
134
- process.exitCode = 1;
135
- }
136
-
137
- module.exports = {
138
- POLICY_CACHE_TTL_MS,
139
- isPolicyCacheFresh,
140
- policyFromInstallerOptions,
141
- fetchPolicy,
142
- savePolicy,
143
- runPolicyCommand
144
- };
1
+ 'use strict';
2
+
3
+ const { log, warn } = require('./ui');
4
+ const { httpJson, printApiError } = require('./http');
5
+ const { parseOption } = require('./env');
6
+ const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
7
+ const { readPolicyCache, writePolicyCache } = require('./deviceConfig');
8
+
9
+ const POLICY_CACHE_TTL_MS = 15 * 60 * 1000;
10
+
11
+ function policyUrl(baseUrl) {
12
+ return new URL('/api/v1/users/me/empirical-policy', baseUrl).toString();
13
+ }
14
+
15
+ function isPolicyCacheFresh(cache, now = Date.now()) {
16
+ return Boolean(
17
+ cache &&
18
+ cache.policy &&
19
+ cache.updatedAt &&
20
+ now - new Date(cache.cachedAt || 0).getTime() < POLICY_CACHE_TTL_MS
21
+ );
22
+ }
23
+
24
+ async function fetchPolicy({ force = false } = {}) {
25
+ const cached = readPolicyCache();
26
+ if (!force && isPolicyCacheFresh(cached)) return { ...cached, source: 'cache' };
27
+
28
+ const accessToken = requireAccessToken();
29
+ const response = await httpJson(
30
+ policyUrl(resolveApiBaseUrl()),
31
+ 'GET',
32
+ null,
33
+ buildApiAuthHeaders(accessToken)
34
+ );
35
+ const data = response?.data || response;
36
+ const next = {
37
+ policy: data.policy || {},
38
+ policyVersion: data.policyVersion || 1,
39
+ updatedAt: data.updatedAt || null,
40
+ cachedAt: new Date().toISOString()
41
+ };
42
+ writePolicyCache(next);
43
+ return { ...next, source: 'remote' };
44
+ }
45
+
46
+ function policyFromInstallerOptions(options = {}) {
47
+ return {
48
+ memoryFirstWorkflows: options.requireMemoryInWorkflows === false
49
+ ? []
50
+ : (options.requiredWorkflows || []),
51
+ allowDiscretionaryUse: options.allowDiscretionaryMemoryUsage !== false,
52
+ stopOnMemoryFailure: options.stopOnMemoryToolFailure !== false,
53
+ rememberCodingStyle: options.rememberCodingStyle !== false,
54
+ rememberArchitectureDecisions: options.rememberArchitectureDecisions !== false,
55
+ recordWorkHistory: options.recordWorkHistory !== false,
56
+ recordScarTissue: options.recordScarTissue !== false,
57
+ recordTodos: options.recordTodos === true,
58
+ storeInstructionArtifacts: options.storeInstructionArtifacts === true,
59
+ checkMemoryBeforeAskingContext: options.checkMemoryBeforeAskingContext !== false,
60
+ durableWritesOnly: options.durableWritesOnly !== false
61
+ };
62
+ }
63
+
64
+ async function savePolicy(policy, accessTokenOverride = null) {
65
+ const accessToken = accessTokenOverride || requireAccessToken();
66
+ const response = await httpJson(
67
+ policyUrl(resolveApiBaseUrl()),
68
+ 'PATCH',
69
+ { policy },
70
+ buildApiAuthHeaders(accessToken)
71
+ );
72
+ const data = response?.data || response;
73
+ writePolicyCache({
74
+ policy: data.policy || {},
75
+ policyVersion: data.policyVersion || 1,
76
+ updatedAt: data.updatedAt || null,
77
+ cachedAt: new Date().toISOString()
78
+ });
79
+ return data;
80
+ }
81
+
82
+ async function runPolicyCommand(args = []) {
83
+ const subcommand = args[0] || 'show';
84
+ if (subcommand === 'show') {
85
+ try {
86
+ const result = await fetchPolicy({ force: args.includes('--refresh') });
87
+ log(JSON.stringify(result, null, 2));
88
+ } catch (error) {
89
+ printApiError(error);
90
+ process.exitCode = 1;
91
+ }
92
+ return;
93
+ }
94
+
95
+ if (subcommand === 'set') {
96
+ const raw = parseOption(args, '--json', '');
97
+ if (!raw) {
98
+ warn('Usage: empirical policy set --json \'{"allowDiscretionaryUse":false}\'');
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+ let policy;
103
+ try {
104
+ policy = JSON.parse(raw);
105
+ } catch (error) {
106
+ warn(`Invalid policy JSON: ${error.message}`);
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+ try {
111
+ const accessToken = requireAccessToken();
112
+ const response = await httpJson(
113
+ policyUrl(resolveApiBaseUrl()),
114
+ 'PATCH',
115
+ { policy },
116
+ buildApiAuthHeaders(accessToken)
117
+ );
118
+ const data = response?.data || response;
119
+ writePolicyCache({
120
+ policy: data.policy || {},
121
+ policyVersion: data.policyVersion || 1,
122
+ updatedAt: data.updatedAt || null,
123
+ cachedAt: new Date().toISOString()
124
+ });
125
+ log(JSON.stringify(data, null, 2));
126
+ } catch (error) {
127
+ printApiError(error);
128
+ process.exitCode = 1;
129
+ }
130
+ return;
131
+ }
132
+
133
+ warn('Usage: empirical policy <show [--refresh]|set --json <policy>>');
134
+ process.exitCode = 1;
135
+ }
136
+
137
+ module.exports = {
138
+ POLICY_CACHE_TTL_MS,
139
+ isPolicyCacheFresh,
140
+ policyFromInstallerOptions,
141
+ fetchPolicy,
142
+ savePolicy,
143
+ runPolicyCommand
144
+ };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ // Task 9: `empirical setup list/run/answer/finish/resume` — the CLI surface for the guided-
4
+ // interview blueprint flow (Task 5's guidedSetupSession.service, exposed over HTTP by Task 9's
5
+ // packages/server/src/routes/guidedSetups.ts). `run --answers <file.json>` drives the whole
6
+ // interview non-interactively from a {questionKey: value} map with no TTY required, which per
7
+ // spec is the primary end-to-end test harness for the feature; `run` with no `--answers` falls
8
+ // back to the shared interactive prompter (createPrompter from ./ui) for a human at a terminal.
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { httpJson, printApiError } = require('./http');
12
+ const { parseOption } = require('./env');
13
+ const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
14
+ const { warn, log, createPrompter } = require('./ui');
15
+
16
+ function apiUrl(pathSuffix) {
17
+ return new URL(`/api/v1/guided-setups${pathSuffix}`, resolveApiBaseUrl()).toString();
18
+ }
19
+
20
+ async function callApi(method, pathSuffix, body) {
21
+ const accessToken = requireAccessToken();
22
+ try {
23
+ const response = await httpJson(apiUrl(pathSuffix), method, body, buildApiAuthHeaders(accessToken));
24
+ return response && response.data ? response.data : response;
25
+ } catch (error) {
26
+ printApiError(error);
27
+ process.exit(1);
28
+ }
29
+ }
30
+
31
+ async function runList() {
32
+ const { setups } = await callApi('GET', '', undefined);
33
+ log(JSON.stringify(setups, null, 2));
34
+ }
35
+
36
+ async function runNonInteractive(setupId, answersFilePath) {
37
+ const raw = fs.readFileSync(path.resolve(answersFilePath), 'utf8');
38
+ const answersByQuestionKey = JSON.parse(raw);
39
+
40
+ let current = await callApi('POST', '/start', { setupId });
41
+
42
+ while (current.status !== 'completed' && current.status !== 'finished_early') {
43
+ const questionKey = current.question.questionKey;
44
+ if (!Object.prototype.hasOwnProperty.call(answersByQuestionKey, questionKey)) {
45
+ warn(`Answers file is missing an answer for unknown question "${questionKey}"`);
46
+ process.exit(1);
47
+ }
48
+ current = await callApi('POST', `/${current.sessionId}/continue`, { answer: answersByQuestionKey[questionKey] });
49
+ }
50
+
51
+ log(JSON.stringify(current, null, 2));
52
+ }
53
+
54
+ async function runInteractive(setupId) {
55
+ const prompter = createPrompter(process.stderr);
56
+ try {
57
+ let current = await callApi('POST', '/start', { setupId });
58
+ while (current.question && current.status !== 'completed' && current.status !== 'finished_early') {
59
+ const answer = await prompter.askText(current.question.prompt);
60
+ current = await callApi('POST', `/${current.sessionId}/continue`, { answer });
61
+ }
62
+ log(JSON.stringify(current, null, 2));
63
+ } finally {
64
+ prompter.close();
65
+ }
66
+ }
67
+
68
+ async function runRun(args) {
69
+ const setupId = args[0] && !args[0].startsWith('--') ? args[0] : '';
70
+ if (!setupId) {
71
+ warn('Usage: empirical setup run <setupId> [--answers <file.json>]');
72
+ process.exit(1);
73
+ }
74
+ const answersFile = parseOption(args, '--answers', '');
75
+ if (answersFile) {
76
+ await runNonInteractive(setupId, answersFile);
77
+ } else {
78
+ await runInteractive(setupId);
79
+ }
80
+ }
81
+
82
+ async function runAnswer(args) {
83
+ const [sessionId, questionKey, ...valueParts] = args;
84
+ if (!sessionId || !questionKey || valueParts.length === 0) {
85
+ warn('Usage: empirical setup answer <sessionId> <questionKey> <value>');
86
+ process.exit(1);
87
+ }
88
+ const result = await callApi('POST', `/${sessionId}/continue`, { answer: valueParts.join(' '), questionKey });
89
+ log(JSON.stringify(result, null, 2));
90
+ }
91
+
92
+ async function runFinish(args) {
93
+ const [sessionId] = args;
94
+ if (!sessionId) {
95
+ warn('Usage: empirical setup finish <sessionId>');
96
+ process.exit(1);
97
+ }
98
+ const result = await callApi('POST', `/${sessionId}/finish`, {});
99
+ log(JSON.stringify(result, null, 2));
100
+ }
101
+
102
+ async function runResume(args) {
103
+ const [sessionId] = args;
104
+ if (!sessionId) {
105
+ warn('Usage: empirical setup resume <sessionId>');
106
+ process.exit(1);
107
+ }
108
+ const result = await callApi('POST', `/${sessionId}/continue`, {});
109
+ log(JSON.stringify(result, null, 2));
110
+ }
111
+
112
+ async function runSetupCommand(args) {
113
+ const [sub, ...rest] = args;
114
+ if (sub === 'list') return runList();
115
+ if (sub === 'run') return runRun(rest);
116
+ if (sub === 'answer') return runAnswer(rest);
117
+ if (sub === 'finish') return runFinish(rest);
118
+ if (sub === 'resume') return runResume(rest);
119
+ warn('Usage: empirical setup <list|run|answer|finish|resume> ...');
120
+ process.exit(1);
121
+ }
122
+
123
+ module.exports = { runSetupCommand };
package/lib/ui.js CHANGED
@@ -174,6 +174,12 @@ function createPrompter(output = process.stdout) {
174
174
  return defaultYes;
175
175
  }
176
176
 
177
+ async function promptText(question) {
178
+ output.write(uiQuestion(`${question} `) + '\n');
179
+ const answer = await readLine();
180
+ return answer.trim();
181
+ }
182
+
177
183
  function close() {
178
184
  process.stdin.removeListener('data', onData);
179
185
  process.stdin.removeListener('end', onEnd);
@@ -184,7 +190,7 @@ function createPrompter(output = process.stdout) {
184
190
  process.stdin.pause();
185
191
  }
186
192
 
187
- return { askYesNo: promptYesNo, close };
193
+ return { askYesNo: promptYesNo, askText: promptText, close };
188
194
  }
189
195
 
190
196
  function parseSelectionList(answer, max) {
@@ -1,44 +1,44 @@
1
- 'use strict';
2
-
3
- const os = require('os');
4
- const path = require('path');
5
- const { httpJson } = require('./http');
6
- const { getEnv } = require('./env');
7
- const { resolveAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
8
- const { readStoredOAuthClient } = require('./oauthClient');
9
- const { readJsonIfExists, writeFile, ensureDir } = require('./fsUtils');
10
-
11
- const WORKSPACE_CONTEXT_TIMEOUT_MS = 2000;
12
- const SELECTED_WORKSPACE_PATH = path.join(os.homedir(), '.empirical', 'workspace-selection.json');
13
-
14
- function readSelectedWorkspace() {
15
- const value = readJsonIfExists(SELECTED_WORKSPACE_PATH);
16
- return value && typeof value.workspaceId === 'string' ? value : null;
17
- }
18
-
19
- function writeSelectedWorkspace(workspace) {
20
- ensureDir(path.dirname(SELECTED_WORKSPACE_PATH));
21
- writeFile(SELECTED_WORKSPACE_PATH, `${JSON.stringify(workspace, null, 2)}\n`);
22
- }
23
-
24
- function clearSelectedWorkspace() {
25
- const fs = require('fs');
26
- if (fs.existsSync(SELECTED_WORKSPACE_PATH)) fs.unlinkSync(SELECTED_WORKSPACE_PATH);
27
- }
28
-
29
- async function withSelectedWorkspaceContext(fn) {
30
- const stored = readSelectedWorkspace();
31
- if (stored?.workspaceId) {
32
- const previousWorkspaceId = globalThis.__empiricalCliWorkspaceId || null;
33
- globalThis.__empiricalCliWorkspaceId = stored.workspaceId;
34
- try {
35
- return await fn();
36
- } finally {
37
- globalThis.__empiricalCliWorkspaceId = previousWorkspaceId;
38
- }
39
- }
40
-
41
- const accessToken = resolveAccessToken();
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { httpJson } = require('./http');
6
+ const { getEnv } = require('./env');
7
+ const { resolveAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
8
+ const { readStoredOAuthClient } = require('./oauthClient');
9
+ const { readJsonIfExists, writeFile, ensureDir } = require('./fsUtils');
10
+
11
+ const WORKSPACE_CONTEXT_TIMEOUT_MS = 2000;
12
+ const SELECTED_WORKSPACE_PATH = path.join(os.homedir(), '.empirical', 'workspace-selection.json');
13
+
14
+ function readSelectedWorkspace() {
15
+ const value = readJsonIfExists(SELECTED_WORKSPACE_PATH);
16
+ return value && typeof value.workspaceId === 'string' ? value : null;
17
+ }
18
+
19
+ function writeSelectedWorkspace(workspace) {
20
+ ensureDir(path.dirname(SELECTED_WORKSPACE_PATH));
21
+ writeFile(SELECTED_WORKSPACE_PATH, `${JSON.stringify(workspace, null, 2)}\n`);
22
+ }
23
+
24
+ function clearSelectedWorkspace() {
25
+ const fs = require('fs');
26
+ if (fs.existsSync(SELECTED_WORKSPACE_PATH)) fs.unlinkSync(SELECTED_WORKSPACE_PATH);
27
+ }
28
+
29
+ async function withSelectedWorkspaceContext(fn) {
30
+ const stored = readSelectedWorkspace();
31
+ if (stored?.workspaceId) {
32
+ const previousWorkspaceId = globalThis.__empiricalCliWorkspaceId || null;
33
+ globalThis.__empiricalCliWorkspaceId = stored.workspaceId;
34
+ try {
35
+ return await fn();
36
+ } finally {
37
+ globalThis.__empiricalCliWorkspaceId = previousWorkspaceId;
38
+ }
39
+ }
40
+
41
+ const accessToken = resolveAccessToken();
42
42
  const clientId = getEnv('EMPIRICAL_CLIENT_ID') || readStoredOAuthClient()?.clientId;
43
43
  if (!accessToken || !clientId) return fn();
44
44
 
@@ -68,9 +68,9 @@ async function withSelectedWorkspaceContext(fn) {
68
68
  }
69
69
  }
70
70
 
71
- module.exports = {
72
- withSelectedWorkspaceContext,
73
- readSelectedWorkspace,
74
- writeSelectedWorkspace,
75
- clearSelectedWorkspace
76
- };
71
+ module.exports = {
72
+ withSelectedWorkspaceContext,
73
+ readSelectedWorkspace,
74
+ writeSelectedWorkspace,
75
+ clearSelectedWorkspace
76
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empirical-gauzza/cli",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
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
5
  "keywords": [
6
6
  "memory",
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ // Coverage for createPrompter()'s free-text askText method, alongside its existing askYesNo.
4
+ // Follows the same fake-stdin-stream approach as the rest of this prompter's design: feed data
5
+ // through process.stdin directly rather than mocking readline, since createPrompter reads
6
+ // process.stdin itself (see the block comment above createPrompter in ../lib/ui.js).
7
+
8
+ const { PassThrough } = require('stream');
9
+ const { createPrompter } = require('../lib/ui');
10
+
11
+ describe('createPrompter().askText', () => {
12
+ let originalStdin;
13
+ let fakeStdin;
14
+
15
+ beforeEach(() => {
16
+ originalStdin = Object.getOwnPropertyDescriptor(process, 'stdin');
17
+ fakeStdin = new PassThrough();
18
+ fakeStdin.setEncoding = PassThrough.prototype.setEncoding.bind(fakeStdin);
19
+ Object.defineProperty(process, 'stdin', { value: fakeStdin, configurable: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ Object.defineProperty(process, 'stdin', originalStdin);
24
+ });
25
+
26
+ test('resolves with the trimmed line typed on stdin', async () => {
27
+ const output = new PassThrough();
28
+ let written = '';
29
+ output.on('data', (chunk) => { written += String(chunk); });
30
+
31
+ const prompter = createPrompter(output);
32
+ const answerPromise = prompter.askText('What is your favorite color?');
33
+
34
+ fakeStdin.write(' blue \n');
35
+
36
+ const answer = await answerPromise;
37
+ expect(answer).toBe('blue');
38
+ expect(written).toMatch(/What is your favorite color\?/);
39
+
40
+ prompter.close();
41
+ });
42
+ });