@tryfridayai/cli 0.2.1 → 0.2.4

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.
@@ -54,6 +54,50 @@ function ask(rl, question) {
54
54
  });
55
55
  }
56
56
 
57
+ function askSecret(rl, prompt) {
58
+ return new Promise((resolve) => {
59
+ rl.pause();
60
+ rl.terminal = false;
61
+ process.stdout.write(prompt);
62
+ let input = '';
63
+ const wasRaw = process.stdin.isRaw;
64
+ if (process.stdin.isTTY) {
65
+ process.stdin.setRawMode(true);
66
+ }
67
+ process.stdin.resume();
68
+ const onData = (data) => {
69
+ const str = data.toString();
70
+ for (const char of str) {
71
+ if (char === '\r' || char === '\n') {
72
+ process.stdin.removeListener('data', onData);
73
+ if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw || false);
74
+ process.stdout.write('\n');
75
+ rl.terminal = true;
76
+ rl.resume();
77
+ resolve(input);
78
+ return;
79
+ }
80
+ if (char === '\x03') {
81
+ process.stdin.removeListener('data', onData);
82
+ if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw || false);
83
+ process.stdout.write('\n');
84
+ rl.terminal = true;
85
+ rl.resume();
86
+ resolve('');
87
+ return;
88
+ }
89
+ if (char === '\x7f' || char === '\b') {
90
+ if (input.length > 0) { input = input.slice(0, -1); process.stdout.write('\b \b'); }
91
+ continue;
92
+ }
93
+ input += char;
94
+ process.stdout.write('*');
95
+ }
96
+ };
97
+ process.stdin.on('data', onData);
98
+ });
99
+ }
100
+
57
101
  function maskKey(key) {
58
102
  if (!key || key.length < 8) return '****';
59
103
  return key.slice(0, 7) + '...' + key.slice(-4);
@@ -78,7 +122,7 @@ export default async function setup(args) {
78
122
  console.log(` Anthropic API key found: ${maskKey(existingKey)}`);
79
123
  const change = await ask(rl, ' Change it? (y/N): ');
80
124
  if (change.toLowerCase() === 'y') {
81
- const key = await ask(rl, ' Paste your Anthropic API key: ');
125
+ const key = await askSecret(rl, ' Paste your Anthropic API key: ');
82
126
  if (key) {
83
127
  envVars.ANTHROPIC_API_KEY = key;
84
128
  config.anthropicApiKey = key;
@@ -89,7 +133,7 @@ export default async function setup(args) {
89
133
  console.log(' Friday uses Claude by Anthropic as its AI engine.');
90
134
  console.log(' Get a key at: https://console.anthropic.com/settings/keys');
91
135
  console.log('');
92
- const key = await ask(rl, ' Paste your API key: ');
136
+ const key = await askSecret(rl, ' Paste your API key: ');
93
137
  if (!key) {
94
138
  console.log(' No key provided. You can set ANTHROPIC_API_KEY in your environment later.');
95
139
  } else {
@@ -148,19 +192,19 @@ export default async function setup(args) {
148
192
  console.log(' (Press Enter to skip any)');
149
193
  console.log('');
150
194
 
151
- const openaiKey = await ask(rl, ' OpenAI API key (for image/video gen): ');
195
+ const openaiKey = await askSecret(rl, ' OpenAI API key (for image/video gen): ');
152
196
  if (openaiKey) {
153
197
  envVars.OPENAI_API_KEY = openaiKey;
154
198
  config.openaiApiKey = openaiKey;
155
199
  }
156
200
 
157
- const googleKey = await ask(rl, ' Google AI API key (for Gemini/Imagen): ');
201
+ const googleKey = await askSecret(rl, ' Google AI API key (for Gemini/Imagen): ');
158
202
  if (googleKey) {
159
203
  envVars.GOOGLE_API_KEY = googleKey;
160
204
  config.googleApiKey = googleKey;
161
205
  }
162
206
 
163
- const elevenKey = await ask(rl, ' ElevenLabs API key (for voice): ');
207
+ const elevenKey = await askSecret(rl, ' ElevenLabs API key (for voice): ');
164
208
  if (elevenKey) {
165
209
  envVars.ELEVENLABS_API_KEY = elevenKey;
166
210
  config.elevenlabsApiKey = elevenKey;
@@ -3,17 +3,8 @@
3
3
  */
4
4
 
5
5
  import readline from 'readline';
6
- import { createRequire } from 'module';
7
6
  import path from 'path';
8
-
9
- const require = createRequire(import.meta.url);
10
- let runtimeDir;
11
- try {
12
- const runtimePkg = require.resolve('friday-runtime/package.json');
13
- runtimeDir = path.dirname(runtimePkg);
14
- } catch {
15
- runtimeDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'runtime');
16
- }
7
+ import { runtimeDir } from '../resolveRuntime.js';
17
8
 
18
9
  const DIM = '\x1b[2m';
19
10
  const RESET = '\x1b[0m';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * resolveRuntime.js — Locate the friday-runtime package directory.
3
+ *
4
+ * Tries three strategies in order:
5
+ * 1. import.meta.resolve('friday-runtime/stdio') — uses exports map
6
+ * 2. createRequire().resolve('friday-runtime/package.json') — CJS fallback
7
+ * 3. Monorepo relative path — for local development
8
+ */
9
+
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { createRequire } from 'module';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+
16
+ let _runtimeDir;
17
+
18
+ try {
19
+ const stdioUrl = import.meta.resolve('friday-runtime/stdio');
20
+ _runtimeDir = path.dirname(fileURLToPath(stdioUrl));
21
+ } catch {
22
+ try {
23
+ const require = createRequire(import.meta.url);
24
+ const runtimePkg = require.resolve('friday-runtime/package.json');
25
+ _runtimeDir = path.dirname(runtimePkg);
26
+ } catch {
27
+ _runtimeDir = path.resolve(__dirname, '..', '..', 'runtime');
28
+ }
29
+ }
30
+
31
+ export const runtimeDir = _runtimeDir;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * SecureKeyStore - Secure storage for API keys using system keychain
3
+ *
4
+ * Uses keytar to store API keys in macOS Keychain, Windows Credential Manager,
5
+ * or Linux libsecret. Keys are NEVER stored in plain text files.
6
+ *
7
+ * This ensures API keys are:
8
+ * 1. Encrypted at rest by the OS
9
+ * 2. Protected by user authentication
10
+ * 3. Never exposed in environment variables or config files
11
+ */
12
+
13
+ import os from 'os';
14
+ import path from 'path';
15
+ import fs from 'fs';
16
+
17
+ const KEYTAR_SERVICE = 'FridayAI-APIKeys';
18
+ const METADATA_FILE = '.api-keys-metadata.json';
19
+
20
+ // API key configuration
21
+ const API_KEYS = {
22
+ ANTHROPIC_API_KEY: {
23
+ label: 'Anthropic',
24
+ unlocks: 'Chat (Claude)',
25
+ envKey: 'ANTHROPIC_API_KEY',
26
+ },
27
+ OPENAI_API_KEY: {
28
+ label: 'OpenAI',
29
+ unlocks: 'Chat, Images, Voice, Video',
30
+ envKey: 'OPENAI_API_KEY',
31
+ },
32
+ GOOGLE_API_KEY: {
33
+ label: 'Google AI',
34
+ unlocks: 'Chat, Images, Voice, Video',
35
+ envKey: 'GOOGLE_API_KEY',
36
+ },
37
+ ELEVENLABS_API_KEY: {
38
+ label: 'ElevenLabs',
39
+ unlocks: 'Premium Voice',
40
+ envKey: 'ELEVENLABS_API_KEY',
41
+ },
42
+ };
43
+
44
+ let keytarModule = null;
45
+ let keytarInitialized = false;
46
+
47
+ async function getKeytar() {
48
+ if (keytarInitialized) return keytarModule;
49
+
50
+ try {
51
+ const module = await import('keytar');
52
+ keytarModule = module.default ?? module;
53
+ keytarInitialized = true;
54
+ return keytarModule;
55
+ } catch (error) {
56
+ console.error('[SecureKeyStore] keytar not available:', error.message);
57
+ keytarInitialized = true;
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function getMetadataPath() {
63
+ const configDir = process.env.FRIDAY_CONFIG_DIR || path.join(os.homedir(), '.friday');
64
+ if (!fs.existsSync(configDir)) {
65
+ fs.mkdirSync(configDir, { recursive: true });
66
+ }
67
+ return path.join(configDir, METADATA_FILE);
68
+ }
69
+
70
+ function loadMetadata() {
71
+ const metadataPath = getMetadataPath();
72
+ try {
73
+ if (fs.existsSync(metadataPath)) {
74
+ return JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
75
+ }
76
+ } catch {
77
+ // Ignore errors
78
+ }
79
+ return { keys: {} };
80
+ }
81
+
82
+ function saveMetadata(metadata) {
83
+ const metadataPath = getMetadataPath();
84
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8');
85
+ }
86
+
87
+ /**
88
+ * Store an API key securely in the system keychain
89
+ * @param {string} keyName - The key name (e.g., 'ANTHROPIC_API_KEY')
90
+ * @param {string} value - The API key value
91
+ * @returns {Promise<boolean>} - True if successful
92
+ */
93
+ export async function setApiKey(keyName, value) {
94
+ const keytar = await getKeytar();
95
+
96
+ if (!keytar) {
97
+ throw new Error('Secure storage not available. Please ensure keytar is installed.');
98
+ }
99
+
100
+ await keytar.setPassword(KEYTAR_SERVICE, keyName, value);
101
+
102
+ // Update metadata (without storing the actual value)
103
+ const metadata = loadMetadata();
104
+ metadata.keys[keyName] = {
105
+ configured: true,
106
+ updatedAt: new Date().toISOString(),
107
+ // Store masked preview for UI display
108
+ preview: '*'.repeat(Math.max(0, value.length - 4)) + value.slice(-4),
109
+ };
110
+ saveMetadata(metadata);
111
+
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * Get an API key from the system keychain
117
+ * @param {string} keyName - The key name (e.g., 'ANTHROPIC_API_KEY')
118
+ * @returns {Promise<string|null>} - The API key value or null if not found
119
+ */
120
+ export async function getApiKey(keyName) {
121
+ const keytar = await getKeytar();
122
+
123
+ if (!keytar) {
124
+ return null;
125
+ }
126
+
127
+ return await keytar.getPassword(KEYTAR_SERVICE, keyName);
128
+ }
129
+
130
+ /**
131
+ * Delete an API key from the system keychain
132
+ * @param {string} keyName - The key name (e.g., 'ANTHROPIC_API_KEY')
133
+ * @returns {Promise<boolean>} - True if deleted
134
+ */
135
+ export async function deleteApiKey(keyName) {
136
+ const keytar = await getKeytar();
137
+
138
+ if (!keytar) {
139
+ return false;
140
+ }
141
+
142
+ await keytar.deletePassword(KEYTAR_SERVICE, keyName);
143
+
144
+ // Update metadata
145
+ const metadata = loadMetadata();
146
+ delete metadata.keys[keyName];
147
+ saveMetadata(metadata);
148
+
149
+ return true;
150
+ }
151
+
152
+ /**
153
+ * Get all configured API keys (values loaded from keychain)
154
+ * @returns {Promise<Object>} - Object with key names as keys and values
155
+ */
156
+ export async function getAllApiKeys() {
157
+ const keytar = await getKeytar();
158
+ const keys = {};
159
+
160
+ if (!keytar) {
161
+ return keys;
162
+ }
163
+
164
+ for (const keyName of Object.keys(API_KEYS)) {
165
+ const value = await keytar.getPassword(KEYTAR_SERVICE, keyName);
166
+ if (value) {
167
+ keys[keyName] = value;
168
+ }
169
+ }
170
+
171
+ return keys;
172
+ }
173
+
174
+ /**
175
+ * Load API keys into process.env (for internal use by providers only)
176
+ * This should ONLY be called during server initialization,
177
+ * AFTER the sensitive env filtering is in place.
178
+ * @returns {Promise<void>}
179
+ */
180
+ export async function loadApiKeysToEnv() {
181
+ const keys = await getAllApiKeys();
182
+
183
+ for (const [keyName, value] of Object.entries(keys)) {
184
+ // Only set if not already set (env vars take precedence)
185
+ if (!process.env[keyName]) {
186
+ process.env[keyName] = value;
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Check which API keys are configured (without loading values)
193
+ * @returns {Object} - Object with key names and their configuration status
194
+ */
195
+ export function getConfiguredKeys() {
196
+ const metadata = loadMetadata();
197
+ const result = {};
198
+
199
+ for (const [keyName, config] of Object.entries(API_KEYS)) {
200
+ const keyMeta = metadata.keys[keyName];
201
+ result[keyName] = {
202
+ ...config,
203
+ configured: !!keyMeta?.configured,
204
+ preview: keyMeta?.preview || null,
205
+ };
206
+ }
207
+
208
+ return result;
209
+ }
210
+
211
+ /**
212
+ * Check if secure storage is available
213
+ * @returns {Promise<boolean>}
214
+ */
215
+ export async function isSecureStorageAvailable() {
216
+ const keytar = await getKeytar();
217
+ return !!keytar;
218
+ }
219
+
220
+ export { API_KEYS };