@tryfridayai/cli 0.2.0 → 0.2.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.
- package/package.json +3 -2
- package/src/commands/chat/inputLine.js +510 -0
- package/src/commands/chat/slashCommands.js +417 -133
- package/src/commands/chat/smartAffordances.js +5 -0
- package/src/commands/chat/welcomeScreen.js +56 -88
- package/src/commands/chat.js +250 -114
- package/src/commands/install.js +46 -16
- package/src/commands/plugins.js +1 -10
- package/src/commands/schedule.js +1 -10
- package/src/commands/setup.js +49 -5
- package/src/commands/uninstall.js +1 -10
- package/src/resolveRuntime.js +31 -0
- package/src/secureKeyStore.js +220 -0
package/src/commands/setup.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 };
|