@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.
- package/README.md +136 -0
- 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 +249 -113
- 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/chat.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* friday chat — Interactive conversation with the agent runtime
|
|
3
3
|
*
|
|
4
4
|
* Spawns the runtime's stdio transport (friday-server.js) as a child process
|
|
5
|
-
* and provides a
|
|
5
|
+
* and provides a bottom-pinned input bar with clean, user-friendly output.
|
|
6
6
|
*
|
|
7
7
|
* Use --verbose to see raw debug output (session IDs, tool inputs, etc.)
|
|
8
8
|
*/
|
|
@@ -11,9 +11,7 @@ import { spawn } from 'child_process';
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import os from 'os';
|
|
13
13
|
import path from 'path';
|
|
14
|
-
import readline from 'readline';
|
|
15
14
|
import { fileURLToPath } from 'url';
|
|
16
|
-
import { createRequire } from 'module';
|
|
17
15
|
|
|
18
16
|
// ── Chat modules ─────────────────────────────────────────────────────────
|
|
19
17
|
|
|
@@ -26,20 +24,9 @@ import {
|
|
|
26
24
|
routeSlashCommand, handleColonCommand, checkPendingResponse,
|
|
27
25
|
} from './chat/slashCommands.js';
|
|
28
26
|
import { checkPreQueryHint, checkPostResponseHint } from './chat/smartAffordances.js';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
-
const __dirname = path.dirname(__filename);
|
|
34
|
-
|
|
35
|
-
const require = createRequire(import.meta.url);
|
|
36
|
-
let runtimeDir;
|
|
37
|
-
try {
|
|
38
|
-
const runtimePkg = require.resolve('friday-runtime/package.json');
|
|
39
|
-
runtimeDir = path.dirname(runtimePkg);
|
|
40
|
-
} catch {
|
|
41
|
-
runtimeDir = path.resolve(__dirname, '..', '..', '..', 'runtime');
|
|
42
|
-
}
|
|
27
|
+
import { runtimeDir } from '../resolveRuntime.js';
|
|
28
|
+
import { loadApiKeysToEnv } from '../secureKeyStore.js';
|
|
29
|
+
import InputLine from './chat/inputLine.js';
|
|
43
30
|
const serverScript = path.join(runtimeDir, 'friday-server.js');
|
|
44
31
|
|
|
45
32
|
// ── Tool humanization ────────────────────────────────────────────────────
|
|
@@ -99,12 +86,59 @@ function humanizeToolUse(toolName, toolInput) {
|
|
|
99
86
|
}
|
|
100
87
|
|
|
101
88
|
const humanized = action.replace(/_/g, ' ').replace(/\b\w/g, (c) => c);
|
|
102
|
-
if (server) {
|
|
103
|
-
return `${humanized} (${server})`;
|
|
104
|
-
}
|
|
105
89
|
return humanized;
|
|
106
90
|
}
|
|
107
91
|
|
|
92
|
+
/** Humanize a tool name for permission prompts — hide MCP internals */
|
|
93
|
+
function humanizePermission(toolName, description) {
|
|
94
|
+
// Map tool actions to friendly descriptions
|
|
95
|
+
const friendlyNames = {
|
|
96
|
+
'generate_image': 'Generate Image',
|
|
97
|
+
'generate image': 'Generate Image',
|
|
98
|
+
'generate_video': 'Generate Video',
|
|
99
|
+
'generate video': 'Generate Video',
|
|
100
|
+
'text_to_speech': 'Text to Speech',
|
|
101
|
+
'text to speech': 'Text to Speech',
|
|
102
|
+
'speech_to_text': 'Speech to Text',
|
|
103
|
+
'speech to text': 'Speech to Text',
|
|
104
|
+
'query_model': 'Query AI Model',
|
|
105
|
+
'list_voices': 'List Voices',
|
|
106
|
+
'clone_voice': 'Clone Voice',
|
|
107
|
+
'execute_command': 'Run Terminal Command',
|
|
108
|
+
'execute command': 'Run Terminal Command',
|
|
109
|
+
'start_preview': 'Start Preview Server',
|
|
110
|
+
'stop_preview': 'Stop Preview Server',
|
|
111
|
+
'WebSearch': 'Search the Web',
|
|
112
|
+
'WebFetch': 'Fetch Web Page',
|
|
113
|
+
'scrape': 'Scrape Web Page',
|
|
114
|
+
'crawl': 'Crawl Website',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (description) {
|
|
118
|
+
// Strip MCP server prefixes: "mcp__server__" or "mcp server-name "
|
|
119
|
+
let cleaned = description
|
|
120
|
+
.replace(/mcp__[\w-]+__/gi, '') // mcp__server__tool
|
|
121
|
+
.replace(/mcp\s+[\w-]+\s+/gi, '') // mcp server-name tool
|
|
122
|
+
.replace(/_/g, ' ')
|
|
123
|
+
.trim();
|
|
124
|
+
|
|
125
|
+
// Check if cleaned text matches a friendly name
|
|
126
|
+
const lowerCleaned = cleaned.toLowerCase();
|
|
127
|
+
for (const [key, value] of Object.entries(friendlyNames)) {
|
|
128
|
+
if (lowerCleaned === key.toLowerCase() || lowerCleaned.startsWith(key.toLowerCase())) {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return cleaned;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!toolName) return 'use a tool';
|
|
136
|
+
const parts = toolName.split('__');
|
|
137
|
+
const action = parts[parts.length - 1];
|
|
138
|
+
|
|
139
|
+
return friendlyNames[action] || `Allow Friday to use ${action.replace(/_/g, ' ')}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
108
142
|
/**
|
|
109
143
|
* Make absolute file paths clickable in terminal output using OSC 8 hyperlinks.
|
|
110
144
|
*/
|
|
@@ -125,7 +159,7 @@ const SPINNER_FRAMES = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u283
|
|
|
125
159
|
function createSpinner() {
|
|
126
160
|
let interval = null;
|
|
127
161
|
let frameIndex = 0;
|
|
128
|
-
let currentText = '
|
|
162
|
+
let currentText = 'Friday is thinking';
|
|
129
163
|
let lineLength = 0;
|
|
130
164
|
|
|
131
165
|
function clear() {
|
|
@@ -144,7 +178,7 @@ function createSpinner() {
|
|
|
144
178
|
}
|
|
145
179
|
|
|
146
180
|
return {
|
|
147
|
-
start(text = '
|
|
181
|
+
start(text = 'Friday is thinking') {
|
|
148
182
|
currentText = text;
|
|
149
183
|
frameIndex = 0;
|
|
150
184
|
if (interval) clearInterval(interval);
|
|
@@ -254,6 +288,36 @@ export default async function chat(args) {
|
|
|
254
288
|
);
|
|
255
289
|
fs.mkdirSync(workspacePath, { recursive: true });
|
|
256
290
|
|
|
291
|
+
// Load API keys from secure storage (system keychain)
|
|
292
|
+
// Keys are loaded into process.env for the runtime to access,
|
|
293
|
+
// but are filtered out before being passed to the agent (see AgentRuntime.js)
|
|
294
|
+
try {
|
|
295
|
+
await loadApiKeysToEnv();
|
|
296
|
+
} catch (err) {
|
|
297
|
+
if (verbose) {
|
|
298
|
+
console.log(`${DIM}Note: Could not load from secure storage: ${err.message}${RESET}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fallback: Also check ~/.friday/.env for legacy keys (will be migrated to keychain)
|
|
303
|
+
const fridayEnvPath = path.join(os.homedir(), '.friday', '.env');
|
|
304
|
+
try {
|
|
305
|
+
if (fs.existsSync(fridayEnvPath)) {
|
|
306
|
+
const content = fs.readFileSync(fridayEnvPath, 'utf8');
|
|
307
|
+
for (const line of content.split('\n')) {
|
|
308
|
+
const trimmed = line.trim();
|
|
309
|
+
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
310
|
+
const eqIdx = trimmed.indexOf('=');
|
|
311
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
312
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
313
|
+
// Only use .env values if not already set from secure storage
|
|
314
|
+
if (key && val && !process.env[key]) {
|
|
315
|
+
process.env[key] = val;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch { /* ignore */ }
|
|
320
|
+
|
|
257
321
|
const env = { ...process.env, FRIDAY_WORKSPACE: workspacePath };
|
|
258
322
|
|
|
259
323
|
if (verbose) {
|
|
@@ -281,11 +345,17 @@ export default async function chat(args) {
|
|
|
281
345
|
process.exit(code ?? 0);
|
|
282
346
|
});
|
|
283
347
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
348
|
+
// ── InputLine (replaces readline) ──────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
const inputLine = new InputLine();
|
|
351
|
+
|
|
352
|
+
// Compatibility adapter for slashCommands.js (expects ctx.rl)
|
|
353
|
+
const rlCompat = {
|
|
354
|
+
pause() { inputLine.pause(); },
|
|
355
|
+
resume() { inputLine.resume(); },
|
|
356
|
+
prompt() { inputLine.prompt(); },
|
|
357
|
+
close() { inputLine.destroy(); },
|
|
358
|
+
};
|
|
289
359
|
|
|
290
360
|
let sessionId = null;
|
|
291
361
|
let pendingPermission = null;
|
|
@@ -294,6 +364,14 @@ export default async function chat(args) {
|
|
|
294
364
|
let isStreaming = false;
|
|
295
365
|
let accumulatedResponse = ''; // Track response text for post-response hints
|
|
296
366
|
|
|
367
|
+
// ── Permission queue (fixes issues b & c) ──────────────────────────────
|
|
368
|
+
// Multiple permission_request messages can arrive simultaneously.
|
|
369
|
+
// We queue them and show only one selectOption at a time, preventing
|
|
370
|
+
// stacked prompts and the `tool_use ids must be unique` API error.
|
|
371
|
+
|
|
372
|
+
const permissionQueue = [];
|
|
373
|
+
let showingPermission = false;
|
|
374
|
+
|
|
297
375
|
const spinner = createSpinner();
|
|
298
376
|
|
|
299
377
|
function writeMessage(payload) {
|
|
@@ -319,6 +397,73 @@ export default async function chat(args) {
|
|
|
319
397
|
}
|
|
320
398
|
}
|
|
321
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Display the next queued permission prompt. Only one is shown at a time.
|
|
402
|
+
* When the user responds, processNextPermission() is called again to
|
|
403
|
+
* dequeue the next one (if any).
|
|
404
|
+
*/
|
|
405
|
+
function processNextPermission() {
|
|
406
|
+
if (showingPermission) return; // one at a time
|
|
407
|
+
if (permissionQueue.length === 0) return;
|
|
408
|
+
|
|
409
|
+
showingPermission = true;
|
|
410
|
+
const msg = permissionQueue.shift();
|
|
411
|
+
pendingPermission = msg;
|
|
412
|
+
|
|
413
|
+
if (spinner.active) {
|
|
414
|
+
spinner.stop();
|
|
415
|
+
}
|
|
416
|
+
if (isStreaming) {
|
|
417
|
+
process.stdout.write('\n');
|
|
418
|
+
isStreaming = false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log('');
|
|
422
|
+
console.log(`${YELLOW}${BOLD}Permission needed:${RESET} ${humanizePermission(msg.tool_name, msg.description)}`);
|
|
423
|
+
if (msg.tool_input) {
|
|
424
|
+
const entries = Object.entries(msg.tool_input);
|
|
425
|
+
const preview = entries.slice(0, 3).map(([k, v]) => {
|
|
426
|
+
const val = typeof v === 'string' ? v : JSON.stringify(v);
|
|
427
|
+
const short = val.length > 70 ? val.slice(0, 67) + '...' : val;
|
|
428
|
+
return ` ${DIM}${k}: ${short}${RESET}`;
|
|
429
|
+
});
|
|
430
|
+
preview.forEach((line) => console.log(line));
|
|
431
|
+
if (entries.length > 3) {
|
|
432
|
+
console.log(` ${DIM}...and ${entries.length - 3} more${RESET}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
console.log('');
|
|
436
|
+
selectOption(
|
|
437
|
+
[
|
|
438
|
+
{ label: 'Allow', value: 'allow' },
|
|
439
|
+
{ label: 'Allow for this session', value: 'session' },
|
|
440
|
+
{ label: 'Deny', value: 'deny' },
|
|
441
|
+
],
|
|
442
|
+
{ rl: rlCompat }
|
|
443
|
+
).then((choice) => {
|
|
444
|
+
showingPermission = false;
|
|
445
|
+
|
|
446
|
+
if (!pendingPermission) {
|
|
447
|
+
// Permission was cancelled while user was choosing
|
|
448
|
+
inputLine.prompt();
|
|
449
|
+
processNextPermission();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (choice.value === 'deny') {
|
|
453
|
+
sendPermissionResponse(false, {});
|
|
454
|
+
} else {
|
|
455
|
+
sendPermissionResponse(true, {});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Process next queued permission (if any)
|
|
459
|
+
if (permissionQueue.length > 0) {
|
|
460
|
+
processNextPermission();
|
|
461
|
+
} else {
|
|
462
|
+
inputLine.prompt();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
322
467
|
function showRulePrompt() {
|
|
323
468
|
if (pendingRulePrompt || rulePromptQueue.length === 0) return;
|
|
324
469
|
pendingRulePrompt = rulePromptQueue.shift();
|
|
@@ -367,7 +512,7 @@ export default async function chat(args) {
|
|
|
367
512
|
// ── Slash command context ──────────────────────────────────────────────
|
|
368
513
|
|
|
369
514
|
const slashCtx = {
|
|
370
|
-
get rl() { return
|
|
515
|
+
get rl() { return rlCompat; },
|
|
371
516
|
get sessionId() { return sessionId; },
|
|
372
517
|
get workspacePath() { return workspacePath; },
|
|
373
518
|
get verbose() { return verbose; },
|
|
@@ -412,7 +557,8 @@ export default async function chat(args) {
|
|
|
412
557
|
case 'ready':
|
|
413
558
|
console.log(renderWelcome());
|
|
414
559
|
console.log('');
|
|
415
|
-
|
|
560
|
+
inputLine.init();
|
|
561
|
+
inputLine.prompt();
|
|
416
562
|
break;
|
|
417
563
|
|
|
418
564
|
case 'session':
|
|
@@ -421,7 +567,11 @@ export default async function chat(args) {
|
|
|
421
567
|
|
|
422
568
|
case 'thinking':
|
|
423
569
|
if (!spinner.active) {
|
|
424
|
-
|
|
570
|
+
if (isStreaming) {
|
|
571
|
+
process.stdout.write('\n');
|
|
572
|
+
isStreaming = false;
|
|
573
|
+
}
|
|
574
|
+
spinner.start('Friday is thinking');
|
|
425
575
|
}
|
|
426
576
|
break;
|
|
427
577
|
|
|
@@ -434,6 +584,8 @@ export default async function chat(args) {
|
|
|
434
584
|
}
|
|
435
585
|
if (!isStreaming) {
|
|
436
586
|
isStreaming = true;
|
|
587
|
+
// Start agent output on a fresh line
|
|
588
|
+
process.stdout.write('\n');
|
|
437
589
|
}
|
|
438
590
|
{
|
|
439
591
|
const text = msg.text || msg.content || '';
|
|
@@ -462,59 +614,20 @@ export default async function chat(args) {
|
|
|
462
614
|
break;
|
|
463
615
|
|
|
464
616
|
case 'permission_request':
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
if (isStreaming) {
|
|
470
|
-
process.stdout.write('\n');
|
|
471
|
-
isStreaming = false;
|
|
472
|
-
}
|
|
473
|
-
console.log('');
|
|
474
|
-
console.log(`${YELLOW}${BOLD}Permission needed:${RESET} ${msg.description || msg.tool_name}`);
|
|
475
|
-
if (msg.tool_input) {
|
|
476
|
-
const entries = Object.entries(msg.tool_input);
|
|
477
|
-
const preview = entries.slice(0, 3).map(([k, v]) => {
|
|
478
|
-
const val = typeof v === 'string' ? v : JSON.stringify(v);
|
|
479
|
-
const short = val.length > 70 ? val.slice(0, 67) + '...' : val;
|
|
480
|
-
return ` ${DIM}${k}: ${short}${RESET}`;
|
|
481
|
-
});
|
|
482
|
-
preview.forEach((line) => console.log(line));
|
|
483
|
-
if (entries.length > 3) {
|
|
484
|
-
console.log(` ${DIM}...and ${entries.length - 3} more${RESET}`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
console.log('');
|
|
488
|
-
selectOption(
|
|
489
|
-
[
|
|
490
|
-
{ label: 'Allow', value: 'allow' },
|
|
491
|
-
{ label: 'Allow for this session', value: 'session' },
|
|
492
|
-
{ label: 'Deny', value: 'deny' },
|
|
493
|
-
],
|
|
494
|
-
{ rl }
|
|
495
|
-
).then((choice) => {
|
|
496
|
-
if (choice.value === 'deny') {
|
|
497
|
-
sendPermissionResponse(false, {});
|
|
498
|
-
} else {
|
|
499
|
-
const permissionLevel = choice.value === 'session' ? 'session' : 'once';
|
|
500
|
-
const response = {
|
|
501
|
-
type: 'permission_response',
|
|
502
|
-
permission_id: pendingPermission.permission_id,
|
|
503
|
-
approved: true,
|
|
504
|
-
permission_level: permissionLevel,
|
|
505
|
-
};
|
|
506
|
-
writeMessage(response);
|
|
507
|
-
pendingPermission = null;
|
|
508
|
-
spinner.start('Working');
|
|
509
|
-
}
|
|
510
|
-
rl.prompt();
|
|
511
|
-
});
|
|
617
|
+
// Queue the permission and process one at a time (fixes issues b & c)
|
|
618
|
+
permissionQueue.push(msg);
|
|
619
|
+
processNextPermission();
|
|
512
620
|
break;
|
|
513
621
|
|
|
514
622
|
case 'permission_cancelled':
|
|
515
623
|
if (pendingPermission && pendingPermission.permission_id === msg.permission_id) {
|
|
516
624
|
pendingPermission = null;
|
|
517
625
|
}
|
|
626
|
+
// Also remove from queue if still pending
|
|
627
|
+
const cancelIdx = permissionQueue.findIndex(p => p.permission_id === msg.permission_id);
|
|
628
|
+
if (cancelIdx !== -1) {
|
|
629
|
+
permissionQueue.splice(cancelIdx, 1);
|
|
630
|
+
}
|
|
518
631
|
break;
|
|
519
632
|
|
|
520
633
|
case 'rule_prompt':
|
|
@@ -530,7 +643,7 @@ export default async function chat(args) {
|
|
|
530
643
|
spinner.stop();
|
|
531
644
|
}
|
|
532
645
|
console.log(`\n${RED}Error: ${msg.message}${RESET}`);
|
|
533
|
-
|
|
646
|
+
inputLine.prompt();
|
|
534
647
|
break;
|
|
535
648
|
|
|
536
649
|
case 'complete':
|
|
@@ -540,12 +653,15 @@ export default async function chat(args) {
|
|
|
540
653
|
if (isStreaming) {
|
|
541
654
|
isStreaming = false;
|
|
542
655
|
}
|
|
543
|
-
// Show
|
|
544
|
-
if (msg.cost && msg.cost.
|
|
545
|
-
const cost = msg.cost.estimated;
|
|
546
|
-
const costStr = cost < 0.01 ? `${(cost * 100).toFixed(2)}c` : `$${cost.toFixed(4)}`;
|
|
656
|
+
// Show chat token usage if available
|
|
657
|
+
if (msg.cost && msg.cost.tokens) {
|
|
547
658
|
const tokens = msg.cost.tokens;
|
|
548
|
-
|
|
659
|
+
const total = tokens.input + tokens.output;
|
|
660
|
+
if (total > 0) {
|
|
661
|
+
console.log(`\n${DIM}chat: ${total.toLocaleString()} tokens${RESET}`);
|
|
662
|
+
} else {
|
|
663
|
+
console.log('');
|
|
664
|
+
}
|
|
549
665
|
} else {
|
|
550
666
|
console.log('');
|
|
551
667
|
}
|
|
@@ -557,7 +673,7 @@ export default async function chat(args) {
|
|
|
557
673
|
}
|
|
558
674
|
accumulatedResponse = '';
|
|
559
675
|
}
|
|
560
|
-
|
|
676
|
+
inputLine.prompt();
|
|
561
677
|
break;
|
|
562
678
|
|
|
563
679
|
default:
|
|
@@ -576,7 +692,8 @@ export default async function chat(args) {
|
|
|
576
692
|
switch (msg.type) {
|
|
577
693
|
case 'ready':
|
|
578
694
|
console.log('Friday is ready. Type your prompt to begin.');
|
|
579
|
-
|
|
695
|
+
inputLine.init();
|
|
696
|
+
inputLine.prompt();
|
|
580
697
|
break;
|
|
581
698
|
case 'session':
|
|
582
699
|
sessionId = msg.session_id;
|
|
@@ -592,21 +709,9 @@ export default async function chat(args) {
|
|
|
592
709
|
process.stdout.write(msg.text || msg.content || '');
|
|
593
710
|
break;
|
|
594
711
|
case 'permission_request':
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
console.log(`Desc : ${msg.description}`);
|
|
599
|
-
if (msg.tool_input) {
|
|
600
|
-
const keys = Object.keys(msg.tool_input);
|
|
601
|
-
console.log('Input:');
|
|
602
|
-
keys.slice(0, 5).forEach((key) => {
|
|
603
|
-
console.log(` ${key}: ${JSON.stringify(msg.tool_input[key])}`);
|
|
604
|
-
});
|
|
605
|
-
if (keys.length > 5) console.log(` ...and ${keys.length - 5} more`);
|
|
606
|
-
}
|
|
607
|
-
console.log('Type :allow or :deny to respond.');
|
|
608
|
-
console.log('===============================================\n');
|
|
609
|
-
rl.prompt();
|
|
712
|
+
// Queue the permission in verbose mode too
|
|
713
|
+
permissionQueue.push(msg);
|
|
714
|
+
processNextPermission();
|
|
610
715
|
break;
|
|
611
716
|
case 'permission_cancelled':
|
|
612
717
|
if (pendingPermission && pendingPermission.permission_id === msg.permission_id) {
|
|
@@ -623,7 +728,7 @@ export default async function chat(args) {
|
|
|
623
728
|
break;
|
|
624
729
|
case 'complete':
|
|
625
730
|
console.log('');
|
|
626
|
-
|
|
731
|
+
inputLine.prompt();
|
|
627
732
|
break;
|
|
628
733
|
default:
|
|
629
734
|
if (msg.type === 'tool_use') {
|
|
@@ -650,16 +755,51 @@ export default async function chat(args) {
|
|
|
650
755
|
|
|
651
756
|
// ── Input handler ──────────────────────────────────────────────────────
|
|
652
757
|
|
|
653
|
-
|
|
758
|
+
// Flag to prevent leaked input from askSecret from being sent as query
|
|
759
|
+
let processingSlashCommand = false;
|
|
760
|
+
|
|
761
|
+
// Safety patterns to detect accidentally leaked API keys
|
|
762
|
+
const API_KEY_PATTERNS = [
|
|
763
|
+
/^sk-[a-zA-Z0-9-_]{20,}$/, // OpenAI/Anthropic style
|
|
764
|
+
/^sk-ant-[a-zA-Z0-9-_]{20,}$/, // Anthropic
|
|
765
|
+
/^sk-proj-[a-zA-Z0-9-_]{20,}$/, // OpenAI project keys
|
|
766
|
+
/^AIza[a-zA-Z0-9-_]{30,}$/, // Google API keys
|
|
767
|
+
/^[a-f0-9]{32}$/, // Generic 32-char hex keys
|
|
768
|
+
];
|
|
769
|
+
|
|
770
|
+
function looksLikeApiKey(text) {
|
|
771
|
+
return API_KEY_PATTERNS.some(pattern => pattern.test(text));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
inputLine.onSubmit(async (input) => {
|
|
654
775
|
const line = input.trim();
|
|
655
776
|
if (!line) {
|
|
656
|
-
|
|
777
|
+
inputLine.prompt();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// SECURITY: Silently block any input that looks like an API key
|
|
782
|
+
// This is a fallback in case askSecret leaks input to readline buffer
|
|
783
|
+
if (looksLikeApiKey(line)) {
|
|
784
|
+
// Silently ignore - don't alarm the user, just don't send to agent
|
|
785
|
+
inputLine.prompt();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Ignore input while processing a slash command (e.g., leaked from askSecret)
|
|
790
|
+
if (processingSlashCommand) {
|
|
791
|
+
inputLine.prompt();
|
|
657
792
|
return;
|
|
658
793
|
}
|
|
659
794
|
|
|
660
795
|
// ── Slash commands (/help, /plugins, etc.) ──────────────────────────
|
|
661
796
|
if (line.startsWith('/')) {
|
|
662
|
-
|
|
797
|
+
processingSlashCommand = true;
|
|
798
|
+
try {
|
|
799
|
+
await routeSlashCommand(line, slashCtx);
|
|
800
|
+
} finally {
|
|
801
|
+
processingSlashCommand = false;
|
|
802
|
+
}
|
|
663
803
|
return;
|
|
664
804
|
}
|
|
665
805
|
|
|
@@ -679,7 +819,7 @@ export default async function chat(args) {
|
|
|
679
819
|
console.log(`${ORANGE}Hint:${RESET} ${DIM}Commands now use / prefix. Try ${BOLD}/quit${RESET}`);
|
|
680
820
|
spinner.stop();
|
|
681
821
|
backend.kill();
|
|
682
|
-
|
|
822
|
+
inputLine.destroy();
|
|
683
823
|
return;
|
|
684
824
|
case 'new':
|
|
685
825
|
console.log(`${ORANGE}Hint:${RESET} ${DIM}Commands now use / prefix. Try ${BOLD}/new${RESET}`);
|
|
@@ -739,7 +879,7 @@ export default async function chat(args) {
|
|
|
739
879
|
return;
|
|
740
880
|
}
|
|
741
881
|
}
|
|
742
|
-
|
|
882
|
+
inputLine.prompt();
|
|
743
883
|
return;
|
|
744
884
|
}
|
|
745
885
|
|
|
@@ -750,14 +890,10 @@ export default async function chat(args) {
|
|
|
750
890
|
}
|
|
751
891
|
|
|
752
892
|
// ── Send user query ─────────────────────────────────────────────────
|
|
893
|
+
// Echo user input so it's visible in the scroll region
|
|
894
|
+
console.log(`\n${PURPLE}▸${RESET} ${BOLD}${line}${RESET}`);
|
|
753
895
|
accumulatedResponse = '';
|
|
754
|
-
spinner.start('
|
|
896
|
+
spinner.start('Friday is thinking');
|
|
755
897
|
writeMessage({ type: 'query', message: line, session_id: sessionId });
|
|
756
898
|
});
|
|
757
|
-
|
|
758
|
-
rl.on('close', () => {
|
|
759
|
-
spinner.stop();
|
|
760
|
-
backend.kill('SIGINT');
|
|
761
|
-
process.exit(0);
|
|
762
|
-
});
|
|
763
899
|
}
|
package/src/commands/install.js
CHANGED
|
@@ -6,17 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import readline from 'readline';
|
|
9
|
-
import { createRequire } from 'module';
|
|
10
9
|
import path from 'path';
|
|
11
|
-
|
|
12
|
-
const require = createRequire(import.meta.url);
|
|
13
|
-
let runtimeDir;
|
|
14
|
-
try {
|
|
15
|
-
const runtimePkg = require.resolve('friday-runtime/package.json');
|
|
16
|
-
runtimeDir = path.dirname(runtimePkg);
|
|
17
|
-
} catch {
|
|
18
|
-
runtimeDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'runtime');
|
|
19
|
-
}
|
|
10
|
+
import { runtimeDir } from '../resolveRuntime.js';
|
|
20
11
|
|
|
21
12
|
const DIM = '\x1b[2m';
|
|
22
13
|
const RESET = '\x1b[0m';
|
|
@@ -31,6 +22,48 @@ function ask(rl, question) {
|
|
|
31
22
|
});
|
|
32
23
|
}
|
|
33
24
|
|
|
25
|
+
function askSecret(rl, prompt) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
rl.pause();
|
|
28
|
+
rl.terminal = false;
|
|
29
|
+
process.stdout.write(prompt);
|
|
30
|
+
let input = '';
|
|
31
|
+
const wasRaw = process.stdin.isRaw;
|
|
32
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
33
|
+
process.stdin.resume();
|
|
34
|
+
const onData = (data) => {
|
|
35
|
+
const str = data.toString();
|
|
36
|
+
for (const char of str) {
|
|
37
|
+
if (char === '\r' || char === '\n') {
|
|
38
|
+
process.stdin.removeListener('data', onData);
|
|
39
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw || false);
|
|
40
|
+
process.stdout.write('\n');
|
|
41
|
+
rl.terminal = true;
|
|
42
|
+
rl.resume();
|
|
43
|
+
resolve(input);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (char === '\x03') {
|
|
47
|
+
process.stdin.removeListener('data', onData);
|
|
48
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw || false);
|
|
49
|
+
process.stdout.write('\n');
|
|
50
|
+
rl.terminal = true;
|
|
51
|
+
rl.resume();
|
|
52
|
+
resolve('');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (char === '\x7f' || char === '\b') {
|
|
56
|
+
if (input.length > 0) { input = input.slice(0, -1); process.stdout.write('\b \b'); }
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
input += char;
|
|
60
|
+
process.stdout.write('*');
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
process.stdin.on('data', onData);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
34
67
|
function maskValue(value) {
|
|
35
68
|
if (!value || value.length < 8) return '****';
|
|
36
69
|
return value.slice(0, 4) + '...' + value.slice(-4);
|
|
@@ -127,15 +160,12 @@ export default async function install(args) {
|
|
|
127
160
|
|
|
128
161
|
const requiredTag = field.required ? '' : ` ${DIM}(optional)${RESET}`;
|
|
129
162
|
const prompt = ` ${field.label}${requiredTag}: `;
|
|
130
|
-
const value =
|
|
163
|
+
const value = field.type === 'secret'
|
|
164
|
+
? await askSecret(rl, prompt)
|
|
165
|
+
: await ask(rl, prompt);
|
|
131
166
|
|
|
132
167
|
if (value) {
|
|
133
168
|
credentials[field.key] = value;
|
|
134
|
-
if (field.type === 'secret') {
|
|
135
|
-
// Clear the line and reprint masked
|
|
136
|
-
process.stdout.write(`\x1b[1A\x1b[2K`);
|
|
137
|
-
console.log(` ${field.label}${requiredTag}: ${DIM}${maskValue(value)}${RESET}`);
|
|
138
|
-
}
|
|
139
169
|
} else if (field.required) {
|
|
140
170
|
console.log(` ${YELLOW}Skipped (required). Plugin may not work without this.${RESET}`);
|
|
141
171
|
}
|
package/src/commands/plugins.js
CHANGED
|
@@ -2,17 +2,8 @@
|
|
|
2
2
|
* friday plugins — List installed and available plugins
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { createRequire } from 'module';
|
|
6
5
|
import path from 'path';
|
|
7
|
-
|
|
8
|
-
const require = createRequire(import.meta.url);
|
|
9
|
-
let runtimeDir;
|
|
10
|
-
try {
|
|
11
|
-
const runtimePkg = require.resolve('friday-runtime/package.json');
|
|
12
|
-
runtimeDir = path.dirname(runtimePkg);
|
|
13
|
-
} catch {
|
|
14
|
-
runtimeDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'runtime');
|
|
15
|
-
}
|
|
6
|
+
import { runtimeDir } from '../resolveRuntime.js';
|
|
16
7
|
|
|
17
8
|
const DIM = '\x1b[2m';
|
|
18
9
|
const RESET = '\x1b[0m';
|
package/src/commands/schedule.js
CHANGED
|
@@ -8,17 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import readline from 'readline';
|
|
11
|
-
import { createRequire } from 'module';
|
|
12
11
|
import path from 'path';
|
|
13
|
-
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
|
-
let runtimeDir;
|
|
16
|
-
try {
|
|
17
|
-
const runtimePkg = require.resolve('friday-runtime/package.json');
|
|
18
|
-
runtimeDir = path.dirname(runtimePkg);
|
|
19
|
-
} catch {
|
|
20
|
-
runtimeDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'runtime');
|
|
21
|
-
}
|
|
12
|
+
import { runtimeDir } from '../resolveRuntime.js';
|
|
22
13
|
|
|
23
14
|
const DIM = '\x1b[2m';
|
|
24
15
|
const RESET = '\x1b[0m';
|