@satori-sh/cli 0.0.2

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.
Files changed (69) hide show
  1. package/README.md +88 -0
  2. package/bun.lock +37 -0
  3. package/dist/add.d.ts +2 -0
  4. package/dist/add.d.ts.map +1 -0
  5. package/dist/add.js +27 -0
  6. package/dist/add.js.map +1 -0
  7. package/dist/config.d.ts +3 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +15 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +49 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/memory.d.ts +12 -0
  16. package/dist/memory.d.ts.map +1 -0
  17. package/dist/memory.js +42 -0
  18. package/dist/memory.js.map +1 -0
  19. package/dist/search.d.ts +2 -0
  20. package/dist/search.d.ts.map +1 -0
  21. package/dist/search.js +27 -0
  22. package/dist/search.js.map +1 -0
  23. package/dist/src/add.d.ts +23 -0
  24. package/dist/src/add.d.ts.map +1 -0
  25. package/dist/src/add.js +46 -0
  26. package/dist/src/add.js.map +1 -0
  27. package/dist/src/config.d.ts +39 -0
  28. package/dist/src/config.d.ts.map +1 -0
  29. package/dist/src/config.js +165 -0
  30. package/dist/src/config.js.map +1 -0
  31. package/dist/src/index.d.ts +14 -0
  32. package/dist/src/index.d.ts.map +1 -0
  33. package/dist/src/index.js +141 -0
  34. package/dist/src/index.js.map +1 -0
  35. package/dist/src/memory.d.ts +52 -0
  36. package/dist/src/memory.d.ts.map +1 -0
  37. package/dist/src/memory.js +98 -0
  38. package/dist/src/memory.js.map +1 -0
  39. package/dist/src/providers.d.ts +34 -0
  40. package/dist/src/providers.d.ts.map +1 -0
  41. package/dist/src/providers.js +107 -0
  42. package/dist/src/providers.js.map +1 -0
  43. package/dist/src/search.d.ts +14 -0
  44. package/dist/src/search.d.ts.map +1 -0
  45. package/dist/src/search.js +39 -0
  46. package/dist/src/search.js.map +1 -0
  47. package/dist/src/types.d.ts +51 -0
  48. package/dist/src/types.d.ts.map +1 -0
  49. package/dist/src/types.js +2 -0
  50. package/dist/src/types.js.map +1 -0
  51. package/dist/tests/index.test.d.ts +2 -0
  52. package/dist/tests/index.test.d.ts.map +1 -0
  53. package/dist/tests/index.test.js +257 -0
  54. package/dist/tests/index.test.js.map +1 -0
  55. package/dist/types.d.ts +19 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +2 -0
  58. package/dist/types.js.map +1 -0
  59. package/logo.txt +2 -0
  60. package/package.json +25 -0
  61. package/src/add.ts +49 -0
  62. package/src/config.ts +170 -0
  63. package/src/index.ts +163 -0
  64. package/src/memory.ts +118 -0
  65. package/src/providers.ts +133 -0
  66. package/src/search.ts +42 -0
  67. package/src/types.ts +45 -0
  68. package/tests/index.test.ts +322 -0
  69. package/tsconfig.json +33 -0
@@ -0,0 +1,257 @@
1
+ import { test, expect, mock, beforeEach, afterEach } from 'bun:test';
2
+ import { main, searchMemories, addMemories, enhanceMessagesWithMemory } from '../src/index';
3
+ import { getConfig, checkWriteAccess, saveApiKey } from '../src/config.js';
4
+ let originalFetch;
5
+ let originalEnv;
6
+ let originalArgv;
7
+ let originalPlatform;
8
+ beforeEach(() => {
9
+ originalFetch = global.fetch;
10
+ originalEnv = { ...process.env };
11
+ originalArgv = [...process.argv];
12
+ originalPlatform = process.platform;
13
+ });
14
+ afterEach(() => {
15
+ global.fetch = originalFetch;
16
+ process.env = originalEnv;
17
+ process.argv = originalArgv;
18
+ process.platform = originalPlatform;
19
+ });
20
+ test('error handling for invalid base URL', async () => {
21
+ process.env.SATORI_BASE_URL = 'invalid-url';
22
+ process.argv = ['node', 'index.js', 'query'];
23
+ const errorSpy = mock(() => { });
24
+ console.error = errorSpy;
25
+ const exitSpy = mock(() => { throw new Error('exit'); });
26
+ process.exit = exitSpy;
27
+ try {
28
+ await main();
29
+ expect(true).toBe(false); // should not reach
30
+ }
31
+ catch (e) {
32
+ expect(e.message).toBe('exit');
33
+ }
34
+ });
35
+ test('error handling for invalid base URL', async () => {
36
+ process.env.SATORI_API_KEY = 'test-key';
37
+ process.env.SATORI_BASE_URL = 'invalid-url';
38
+ process.argv = ['node', 'index.js', 'query'];
39
+ const errorSpy = mock(() => { });
40
+ console.error = errorSpy;
41
+ const exitSpy = mock(() => { throw new Error('exit'); });
42
+ process.exit = exitSpy;
43
+ try {
44
+ await main();
45
+ expect(true).toBe(false); // should not reach
46
+ }
47
+ catch (e) {
48
+ expect(e.message).toBe('exit');
49
+ }
50
+ });
51
+ test('search with empty query', async () => {
52
+ process.env.SATORI_API_KEY = 'satori-erQCxG3GYryoXt4JydgEsbRmg5LuJf6p';
53
+ const errorSpy = mock(() => { });
54
+ console.error = errorSpy;
55
+ await searchMemories('');
56
+ expect(errorSpy).toHaveBeenCalledWith('Query cannot be empty');
57
+ });
58
+ test('add with empty text', async () => {
59
+ process.env.SATORI_API_KEY = 'satori-erQCxG3GYryoXt4JydgEsbRmg5LuJf6p';
60
+ const errorSpy = mock(() => { });
61
+ console.error = errorSpy;
62
+ await addMemories('');
63
+ expect(errorSpy).toHaveBeenCalledWith('Text cannot be empty');
64
+ });
65
+ test('HTTP 401 error', async () => {
66
+ process.env.SATORI_API_KEY = 'test-key';
67
+ const fetchMock = Object.assign(mock(() => Promise.resolve(new Response('Unauthorized', { status: 401, statusText: 'Unauthorized' }))), { preconnect: mock(() => { }) });
68
+ globalThis.fetch = fetchMock;
69
+ const errorSpy = mock(() => { });
70
+ console.error = errorSpy;
71
+ await searchMemories('test');
72
+ expect(errorSpy).toHaveBeenCalledWith('HTTP error: 401 Unauthorized');
73
+ });
74
+ test('HTTP 500 error', async () => {
75
+ process.env.SATORI_API_KEY = 'test-key';
76
+ const fetchMock = Object.assign(mock(() => Promise.resolve(new Response('Internal Server Error', { status: 500, statusText: 'Internal Server Error' }))), { preconnect: mock(() => { }) });
77
+ globalThis.fetch = fetchMock;
78
+ const errorSpy = mock(() => { });
79
+ console.error = errorSpy;
80
+ await searchMemories('test');
81
+ expect(errorSpy).toHaveBeenCalledWith('HTTP error: 500 Internal Server Error');
82
+ });
83
+ test('network error', async () => {
84
+ process.env.SATORI_API_KEY = 'test-key';
85
+ const fetchMock = Object.assign(mock(() => { throw new Error('network error'); }), { preconnect: mock(() => { }) });
86
+ globalThis.fetch = fetchMock;
87
+ const errorSpy = mock(() => { });
88
+ console.error = errorSpy;
89
+ await searchMemories('test');
90
+ expect(errorSpy).toHaveBeenCalledWith('Error searching memories: network error');
91
+ });
92
+ test('malformed JSON response', async () => {
93
+ process.env.SATORI_API_KEY = 'test-key';
94
+ const fetchMock = Object.assign(mock(() => Promise.resolve(new Response('invalid json', { status: 200 }))), { preconnect: mock(() => { }) });
95
+ globalThis.fetch = fetchMock;
96
+ const errorSpy = mock(() => { });
97
+ console.error = errorSpy;
98
+ await searchMemories('test');
99
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error searching memories:'));
100
+ });
101
+ test('enhanceMessagesWithMemory adds context', () => {
102
+ const messages = [{ role: 'user', content: 'hello' }];
103
+ const memoryContext = { results: [{ id: '1', memory: 'test memory' }] };
104
+ const enhanced = enhanceMessagesWithMemory(messages, memoryContext);
105
+ expect(enhanced).toHaveLength(2);
106
+ expect(enhanced[0].role).toBe('system');
107
+ expect(enhanced[0].content).toContain('Relevant context from memory:\ntest memory');
108
+ expect(enhanced[1]).toEqual(messages[0]);
109
+ });
110
+ test('enhanceMessagesWithMemory handles empty memories', () => {
111
+ const messages = [{ role: 'user', content: 'hello' }];
112
+ const memoryContext = { results: [] };
113
+ const enhanced = enhanceMessagesWithMemory(messages, memoryContext);
114
+ expect(enhanced).toEqual(messages);
115
+ });
116
+ test('getConfig throws on invalid base URL', async () => {
117
+ process.env.SATORI_BASE_URL = 'invalid-url';
118
+ try {
119
+ await getConfig();
120
+ expect(true).toBe(false); // should throw
121
+ }
122
+ catch (error) {
123
+ expect(error.message).toBe('Invalid SATORI_BASE_URL format');
124
+ }
125
+ finally {
126
+ delete process.env.SATORI_BASE_URL;
127
+ }
128
+ });
129
+ test('getConfig throws on non-darwin platform', async () => {
130
+ process.platform = 'win32';
131
+ try {
132
+ await getConfig();
133
+ expect(true).toBe(false); // should throw
134
+ }
135
+ catch (error) {
136
+ expect(error.message).toBe('We do not currently support Windows yet, email support@satori.sh to request Windows support');
137
+ }
138
+ });
139
+ test('getConfig generates apiKey on successful fetch', async () => {
140
+ delete process.env.SATORI_API_KEY;
141
+ const fetchMock = Object.assign(mock(() => Promise.resolve(new Response(JSON.stringify({ api_key: 'generated-key' }), { status: 200 }))), { preconnect: mock(() => { }) });
142
+ globalThis.fetch = fetchMock;
143
+ const config = await getConfig();
144
+ expect(config.apiKey).toBe('generated-key');
145
+ globalThis.fetch = originalFetch;
146
+ });
147
+ test('getConfig throws on network error during generation', async () => {
148
+ delete process.env.SATORI_API_KEY;
149
+ mock.module('node:fs', () => ({
150
+ promises: {
151
+ mkdir: mock(() => Promise.resolve()),
152
+ writeFile: mock(() => Promise.resolve()),
153
+ unlink: mock(() => Promise.resolve())
154
+ }
155
+ }));
156
+ const mockFile = {
157
+ exists: mock(() => Promise.resolve(false))
158
+ };
159
+ const originalBun = globalThis.Bun;
160
+ globalThis.Bun = { file: mock(() => mockFile) };
161
+ const fetchMock = Object.assign(mock(() => Promise.reject(new Error('network error'))), { preconnect: mock(() => { }) });
162
+ globalThis.fetch = fetchMock;
163
+ try {
164
+ await getConfig();
165
+ expect(true).toBe(false); // should throw
166
+ }
167
+ catch (error) {
168
+ expect(error.message).toBe('Failed to generate API key: network error');
169
+ }
170
+ globalThis.fetch = originalFetch;
171
+ globalThis.Bun = originalBun;
172
+ });
173
+ test('getConfig throws on invalid response during generation', async () => {
174
+ delete process.env.SATORI_API_KEY;
175
+ mock.module('node:fs', () => ({
176
+ promises: {
177
+ mkdir: mock(() => Promise.resolve()),
178
+ writeFile: mock(() => Promise.resolve()),
179
+ unlink: mock(() => Promise.resolve())
180
+ }
181
+ }));
182
+ const mockFile = {
183
+ exists: mock(() => Promise.resolve(false))
184
+ };
185
+ const originalBun = globalThis.Bun;
186
+ globalThis.Bun = { file: mock(() => mockFile) };
187
+ const fetchMock = Object.assign(mock(() => Promise.resolve(new Response(JSON.stringify({}), { status: 200 }))), { preconnect: mock(() => { }) });
188
+ globalThis.fetch = fetchMock;
189
+ try {
190
+ await getConfig();
191
+ expect(true).toBe(false); // should throw
192
+ }
193
+ catch (error) {
194
+ expect(error.message).toBe('Failed to generate API key: Invalid response: missing api_key');
195
+ }
196
+ globalThis.fetch = originalFetch;
197
+ globalThis.Bun = originalBun;
198
+ });
199
+ test('checkWriteAccess succeeds on successful write', async () => {
200
+ mock.module('node:fs', () => ({
201
+ promises: {
202
+ mkdir: mock(() => Promise.resolve()),
203
+ writeFile: mock(() => Promise.resolve()),
204
+ unlink: mock(() => Promise.resolve())
205
+ }
206
+ }));
207
+ await checkWriteAccess();
208
+ // No throw means success
209
+ });
210
+ test('checkWriteAccess throws on permission denied', async () => {
211
+ mock.module('node:fs', () => ({
212
+ promises: {
213
+ mkdir: mock(() => Promise.resolve()),
214
+ writeFile: mock(() => { throw new Error('EACCES: permission denied'); }),
215
+ unlink: mock(() => Promise.resolve())
216
+ }
217
+ }));
218
+ try {
219
+ await checkWriteAccess();
220
+ expect(true).toBe(false); // should throw
221
+ }
222
+ catch (error) {
223
+ expect(error.message).toBe('Cannot write to config directory: EACCES: permission denied');
224
+ }
225
+ });
226
+ test('saveApiKey saves successfully', async () => {
227
+ const mockWriteFile = mock(() => Promise.resolve());
228
+ mock.module('node:fs', () => ({
229
+ promises: {
230
+ mkdir: mock(() => Promise.resolve()),
231
+ writeFile: mockWriteFile,
232
+ unlink: mock(() => Promise.resolve())
233
+ }
234
+ }));
235
+ await saveApiKey('test-key');
236
+ expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining('satori.json'), JSON.stringify({ api_key: 'test-key' }, null, 2));
237
+ });
238
+ test('saveApiKey throws on write error', async () => {
239
+ const mockWriteFile = mock(() => { throw new Error('write error'); });
240
+ mock.module('node:fs', () => ({
241
+ promises: {
242
+ mkdir: mock(() => Promise.resolve()),
243
+ writeFile: mockWriteFile,
244
+ unlink: mock(() => Promise.resolve())
245
+ }
246
+ }));
247
+ try {
248
+ await saveApiKey('test-key');
249
+ expect(true).toBe(false); // should throw
250
+ }
251
+ catch (error) {
252
+ expect(error.message).toBe('Cannot write to config directory: write error');
253
+ }
254
+ });
255
+ // Note: Additional unit tests for file reading scenarios would require mocking Bun.file,
256
+ // which is readonly. The existing test covers the missing file case.
257
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../tests/index.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export interface Config {
2
+ apiKey: string;
3
+ baseUrl: string;
4
+ }
5
+ export interface SearchResponse {
6
+ results: Array<{
7
+ id: string;
8
+ memory: string;
9
+ score?: number;
10
+ }>;
11
+ }
12
+ export interface AddResponse {
13
+ results: Array<{
14
+ id: string;
15
+ memory: string;
16
+ event: string;
17
+ }>;
18
+ }
19
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/D"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/logo.txt ADDED
@@ -0,0 +1,2 @@
1
+ █▀ ▄▀█ ▀█▀ █▀█ █▀█ █
2
+ ▄█ █▀█ █ █▄█ █▀▄ █
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@satori-sh/cli",
3
+ "version": "0.0.2",
4
+ "description": "CLI tool for Satori memory server",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "satori": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "bun test",
13
+ "lint": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "chalk": "^5.6.2",
17
+ "commander": "^12.1.0",
18
+ "random-words": "^2.0.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/bun": "^1.3.3",
22
+ "@types/node": "^24.10.1",
23
+ "typescript": "^5.9.3"
24
+ }
25
+ }
package/src/add.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { getConfig } from './config.js';
2
+ import { AddResponse } from './types.js';
3
+
4
+ /**
5
+ * Adds a new memory to the Satori server.
6
+ *
7
+ * @param {string} text - The text content to add as a memory
8
+ * @param {object} [options] - Additional options for the memory
9
+ * @param {string} [options.memoryId] - Optional memory ID for scoping
10
+ * @returns {Promise<void>} Logs success or error messages to console
11
+ * @throws {Error} If the text is empty
12
+ *
13
+ * @example
14
+ * ```bash
15
+ * satori add "I like pizza"
16
+ * ```
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * await addMemories("User prefers dark mode", { memoryId: "session-123" });
21
+ * ```
22
+ */
23
+ export async function addMemories(text: string, options: { memoryId?: string } = {}): Promise<void> {
24
+ if (!text || !text.trim()) {
25
+ console.error('Text cannot be empty');
26
+ return;
27
+ }
28
+
29
+ try {
30
+ const config = await getConfig();
31
+ const response = await fetch(`${config.baseUrl}/memories`, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'Authorization': `Bearer ${config.apiKey}`
36
+ },
37
+ body: JSON.stringify({ messages: [{ role: "user", content: text }], ...(options.memoryId && { memory_id: options.memoryId }) })
38
+ });
39
+
40
+ if (!response.ok) {
41
+ console.error(`HTTP error: ${response.status} ${response.statusText}`);
42
+ return;
43
+ }
44
+
45
+ await response.json() as AddResponse;
46
+ } catch (error: unknown) {
47
+ console.error(`Error adding memory: ${error instanceof Error ? error.message : error}`);
48
+ }
49
+ }
package/src/config.ts ADDED
@@ -0,0 +1,170 @@
1
+ import { Config } from './types.js';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+
6
+ /**
7
+ * Checks write access to the config directory by attempting to create, write, and delete a temporary file.
8
+ *
9
+ * @throws {Error} If write access is denied or any operation fails
10
+ */
11
+ export async function checkWriteAccess(): Promise<void> {
12
+ // Dynamic import used instead of static import at top-level to enable mocking with Bun's mock.module() in tests.
13
+ // Static imports cannot be overridden by module mocks in Bun, so this allows fs operations to be properly mocked.
14
+ const { promises: fs } = await import('node:fs');
15
+ const dir = join(homedir(), '.config', 'satori');
16
+ try {
17
+ await fs.mkdir(dir, { recursive: true });
18
+ const tempFile = join(dir, `temp-${randomUUID()}.txt`);
19
+ await fs.writeFile(tempFile, 'test');
20
+ await fs.unlink(tempFile);
21
+ } catch (error) {
22
+ throw new Error(`Cannot write to config directory: ${error instanceof Error ? error.message : error}`);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Saves the API key to the config file.
28
+ *
29
+ * @param {string} apiKey - The API key to save
30
+ * @throws {Error} If write access fails or saving fails
31
+ */
32
+ export async function saveApiKey(apiKey: string): Promise<void> {
33
+ // Dynamic import used instead of static import at top-level to enable mocking with Bun's mock.module() in tests.
34
+ // Static imports cannot be overridden by module mocks in Bun, so this allows fs operations to be properly mocked.
35
+ const { promises: fs } = await import('node:fs');
36
+ await checkWriteAccess();
37
+ const configPath = join(homedir(), '.config', 'satori', 'satori.json');
38
+ try {
39
+ const existing = await loadConfigFile();
40
+ const config = { ...existing, api_key: apiKey };
41
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
42
+ } catch (error) {
43
+ throw new Error(`Failed to save API key: ${error instanceof Error ? error.message : error}`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Saves the memory ID to the config file.
49
+ *
50
+ * @param {string} memoryId - The memory ID to save
51
+ * @throws {Error} If write access fails or saving fails
52
+ */
53
+ export async function saveMemoryId(memoryId: string): Promise<void> {
54
+ // Dynamic import used instead of static import at top-level to enable mocking with Bun's mock.module() in tests.
55
+ // Static imports cannot be overridden by module mocks in Bun, so this allows fs operations to be properly mocked.
56
+ const { promises: fs } = await import('node:fs');
57
+ await checkWriteAccess();
58
+ const configPath = join(homedir(), '.config', 'satori', 'satori.json');
59
+ try {
60
+ const existing = await loadConfigFile();
61
+ const config = { ...existing, memory_id: memoryId };
62
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
63
+ } catch (error) {
64
+ throw new Error(`Failed to save memory ID: ${error instanceof Error ? error.message : error}`);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Loads the config file data.
70
+ *
71
+ * @returns {Promise<Record<string, any>>} The config data
72
+ */
73
+ async function loadConfigFile(): Promise<Record<string, any>> {
74
+ try {
75
+ const configPath = join(homedir(), '.config', 'satori', 'satori.json');
76
+ const configFile = Bun.file(configPath);
77
+ if (await configFile.exists()) {
78
+ return await configFile.json();
79
+ }
80
+ } catch {
81
+ // Ignore errors
82
+ }
83
+ return {};
84
+ }
85
+
86
+ /**
87
+ * Retrieves and validates configuration from file and environment variables.
88
+ * Fetches the memory_id and api_key from the config file in ~/.config/satori/satori.json.
89
+ *
90
+ * If the API key isn't in the config, it looks in SATORI_API_KEY.
91
+ * If there is no API key in either place it will call the API to get a new key.
92
+ *
93
+ * @returns {Promise<Config>} The validated configuration object
94
+ * @throws {Error} If required environment variables are missing or invalid
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const config = await getConfig();
99
+ * console.log(config.apiKey); // API key from ~/.config/satori/satori.json or SATORI_API_KEY env var, or null
100
+ * ```
101
+ */
102
+ export async function getConfig(): Promise<Config> {
103
+ if (process.platform !== 'darwin') {
104
+ throw new Error('We do not currently support Windows yet, email support@satori.sh to request Windows support');
105
+ }
106
+
107
+ let apiKey: string | null = null;
108
+ let memoryId: string | undefined = undefined;
109
+ try {
110
+ const configPath = join(homedir(), '.config', 'satori', 'satori.json');
111
+ const configFile = Bun.file(configPath);
112
+ if (await configFile.exists()) {
113
+ const data = await configFile.json();
114
+ if (data && typeof data.api_key === 'string') {
115
+ apiKey = data.api_key;
116
+ }
117
+ if (data && typeof data.memory_id === 'string') {
118
+ memoryId = data.memory_id;
119
+ }
120
+ }
121
+ } catch {
122
+ // If file reading or parsing fails, apiKey and memoryId remain default
123
+ }
124
+
125
+ // Fallback to environment variable if not set from file
126
+ if (!apiKey) {
127
+ apiKey = process.env.SATORI_API_KEY || null;
128
+ }
129
+
130
+ const baseUrl = process.env.SATORI_BASE_URL || 'https://api.satori.sh';
131
+
132
+ try {
133
+ new URL(baseUrl);
134
+ } catch {
135
+ throw new Error('Invalid SATORI_BASE_URL format');
136
+ }
137
+
138
+ // Generate new API key if still null
139
+ if (!apiKey) {
140
+ try {
141
+ const response = await fetch(`${baseUrl}/orgs`, {
142
+ method: 'POST',
143
+ });
144
+ if (!response.ok) {
145
+ throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
146
+ }
147
+ const data = await response.json() as { api_key?: string };
148
+ if (data && typeof data.api_key === 'string') {
149
+ apiKey = data.api_key;
150
+ await saveApiKey(apiKey);
151
+ } else {
152
+ throw new Error('Invalid response: missing api_key');
153
+ }
154
+ } catch (error) {
155
+ throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : error}`);
156
+ }
157
+ }
158
+
159
+ const provider = process.env.SATORI_PROVIDER || 'openai';
160
+ const model = process.env.SATORI_MODEL || 'gpt-4o';
161
+ const openaiKey = process.env.OPENAI_API_KEY;
162
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
163
+
164
+ // Fallback to config file memoryId if not set via env
165
+ if (!memoryId) {
166
+ memoryId = process.env.SATORI_MEMORY_ID;
167
+ }
168
+
169
+ return { apiKey, baseUrl, provider, model, openaiKey, anthropicKey, memoryId };
170
+ }