@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 ADDED
@@ -0,0 +1,136 @@
1
+ # Friday CLI
2
+
3
+ Autonomous AI agent for your terminal. Chat, generate images, create videos, produce voice — all from the command line.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @tryfridayai/cli
9
+ ```
10
+
11
+ Requires Node.js 18+.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Start chatting
17
+ friday chat
18
+
19
+ # Add API keys (stored in system keychain)
20
+ friday chat
21
+ /keys
22
+ ```
23
+
24
+ You need at least one API key to get started:
25
+
26
+ | Key | Enables |
27
+ |-----|---------|
28
+ | `ANTHROPIC_API_KEY` | Chat (Claude) |
29
+ | `OPENAI_API_KEY` | Chat, Images, Video, Voice |
30
+ | `GOOGLE_API_KEY` | Chat, Images, Video, Voice |
31
+ | `ELEVENLABS_API_KEY` | Voice |
32
+
33
+ ## Features
34
+
35
+ ### Chat
36
+
37
+ Conversational AI powered by Claude, GPT, and Gemini. Sessions auto-resume — close and reopen without losing context.
38
+
39
+ ```
40
+ > explain this codebase
41
+ > build a landing page for my app
42
+ > find and fix the bug in auth.js
43
+ ```
44
+
45
+ ### Image Generation
46
+
47
+ Generate images with DALL-E, GPT Image, and Google Imagen.
48
+
49
+ ```
50
+ > generate an image of a mountain sunset
51
+ > create a logo for my startup
52
+ ```
53
+
54
+ ### Video Generation
55
+
56
+ Create videos with OpenAI Sora and Google Veo.
57
+
58
+ ```
59
+ > generate a 10-second video of ocean waves
60
+ ```
61
+
62
+ ### Voice
63
+
64
+ Text-to-speech with OpenAI TTS, Google Cloud TTS, and ElevenLabs.
65
+
66
+ ```
67
+ > read this paragraph aloud
68
+ > generate speech in a warm friendly tone
69
+ ```
70
+
71
+ ## Commands
72
+
73
+ Type these in the chat:
74
+
75
+ | Command | Description |
76
+ |---------|-------------|
77
+ | `/help` | Show all commands |
78
+ | `/keys` | Add or manage API keys |
79
+ | `/model` | Enable/disable models, see pricing |
80
+ | `/plugins` | Install plugins (GitHub, Figma, email, etc.) |
81
+ | `/config` | View and edit settings |
82
+ | `/clear` | Clear chat history |
83
+ | `/quit` | Exit |
84
+
85
+ ## Models
86
+
87
+ ### Chat Models
88
+ - Claude 4 Sonnet / Opus (Anthropic)
89
+ - GPT-5.2 / GPT-4o (OpenAI)
90
+ - Gemini 3 Pro / Flash (Google)
91
+
92
+ ### Image Models
93
+ - GPT Image 1.5 (OpenAI)
94
+ - Imagen 4 Ultra / Standard / Fast (Google)
95
+
96
+ ### Video Models
97
+ - Sora 2 / Sora 2 Pro (OpenAI)
98
+ - Veo 3.1 / Veo 3.1 Fast (Google)
99
+
100
+ ### Voice Models
101
+ - GPT-4o Mini TTS (OpenAI)
102
+ - Google Cloud TTS (WaveNet, Neural2, Standard)
103
+ - ElevenLabs Eleven v3, Flash v2.5, Turbo v2.5
104
+
105
+ ## Plugins
106
+
107
+ Extend Friday with MCP-based plugins:
108
+
109
+ ```
110
+ /plugins
111
+ ```
112
+
113
+ Available plugins: GitHub, Figma, Firecrawl, Resend (email), Discord, Reddit, Twitter, Gmail, Google Drive, Supabase, and more.
114
+
115
+ ## Architecture
116
+
117
+ The CLI (`@tryfridayai/cli`) is a thin interface over the runtime (`friday-runtime`). The runtime handles agent orchestration, MCP servers, permissions, sessions, and provider management.
118
+
119
+ ```
120
+ friday chat
121
+ └── CLI (this package)
122
+ └── friday-runtime
123
+ ├── Claude Agent SDK
124
+ ├── MCP Servers (filesystem, terminal, media, plugins)
125
+ └── AI Providers (OpenAI, Google, ElevenLabs)
126
+ ```
127
+
128
+ ## Links
129
+
130
+ - Website: [tryfriday.ai](https://tryfriday.ai)
131
+ - Documentation: [docs.tryfriday.ai](https://docs.tryfriday.ai)
132
+ - GitHub: [github.com/tryfridayai/friday_cli](https://github.com/tryfridayai/friday_cli)
133
+
134
+ ## License
135
+
136
+ MIT
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.1",
6
+ "version": "0.2.4",
7
7
  "description": "Friday AI — autonomous agent for your terminal. Chat, build apps, research, automate tasks.",
8
8
  "type": "module",
9
9
  "bin": {
@@ -38,7 +38,8 @@
38
38
  ],
39
39
  "author": "Friday AI <hello@tryfriday.ai>",
40
40
  "dependencies": {
41
- "friday-runtime": "^0.2.0"
41
+ "friday-runtime": "^0.2.0",
42
+ "keytar": "^7.9.0"
42
43
  },
43
44
  "engines": {
44
45
  "node": ">=18.0.0"
@@ -0,0 +1,510 @@
1
+ /**
2
+ * chat/inputLine.js — Bottom-pinned input bar for Friday CLI chat
3
+ *
4
+ * Replaces Node's readline with a custom input line pinned to the bottom
5
+ * of the terminal. Output scrolls above a separator line; the user always
6
+ * types on the last row.
7
+ *
8
+ * Fixes:
9
+ * - Multi-line paste: pasted text (one data event with newlines) is joined
10
+ * into a single message instead of discarding all but the first line.
11
+ * - Input area: output and input no longer intermix — the prompt is always
12
+ * visible at the bottom of the terminal.
13
+ *
14
+ * How it works:
15
+ * - ANSI scroll region confines output to rows 1..N-2
16
+ * - Row N-1: dim separator line
17
+ * - Row N: input prompt with editable text
18
+ * - stdout is patched: writes are redirected into the scroll region using
19
+ * SCO cursor save/restore (\x1b[s / \x1b[u) to track the output position,
20
+ * then cursor jumps back to the input line.
21
+ * - The scroll region handles line wrapping and scrolling natively —
22
+ * no explicit \n is injected per write.
23
+ */
24
+
25
+ import { PURPLE, RESET, BOLD, DIM } from './ui.js';
26
+
27
+ const HISTORY_MAX = 50;
28
+
29
+ export default class InputLine {
30
+ constructor() {
31
+ this._buf = ''; // current input buffer
32
+ this._cursor = 0; // cursor position within _buf
33
+ this._history = []; // command history ring
34
+ this._historyIdx = -1; // -1 = current input, 0..N-1 = history
35
+ this._savedBuf = ''; // buffer saved when browsing history
36
+ this._submitCb = null; // onSubmit callback
37
+ this._active = false; // true when input line is live
38
+ this._paused = false; // true when paused for selectOption/askSecret
39
+ this._rows = 0; // terminal rows
40
+ this._cols = 0; // terminal cols
41
+ this._onData = null; // stdin data handler ref
42
+ this._onResize = null; // SIGWINCH handler ref
43
+ this._originalWrite = null; // original process.stdout.write
44
+ this._promptStr = `${BOLD}>${RESET} `; // visible prompt
45
+ this._promptLen = 2; // visible character length of prompt ("> ")
46
+ }
47
+
48
+ // ── Public API ──────────────────────────────────────────────────────
49
+
50
+ init() {
51
+ if (this._active) return;
52
+ this._active = true;
53
+
54
+ this._rows = process.stdout.rows || 24;
55
+ this._cols = process.stdout.columns || 80;
56
+
57
+ // Set scroll region (rows 1..N-2) — output is confined here
58
+ this._setScrollRegion();
59
+
60
+ // Draw separator and clear input row
61
+ this._drawChrome();
62
+
63
+ // Position output cursor at bottom of scroll region and save it
64
+ // using SCO save (\x1b[s). The patched stdout will restore/save
65
+ // this position on every write so the output cursor tracks correctly.
66
+ const scrollEnd = Math.max(1, this._rows - 2);
67
+ this._writeRaw(`\x1b[${scrollEnd};1H`);
68
+ this._writeRaw('\x1b[s'); // SCO save — output cursor position
69
+
70
+ // Patch stdout.write to redirect output into the scroll region
71
+ this._patchStdout();
72
+
73
+ // Listen for terminal resize
74
+ this._onResize = () => {
75
+ this._rows = process.stdout.rows || 24;
76
+ this._cols = process.stdout.columns || 80;
77
+ this._setScrollRegion();
78
+ this._drawChrome();
79
+ // Re-save output cursor at bottom of new scroll region
80
+ const end = Math.max(1, this._rows - 2);
81
+ this._writeRaw(`\x1b[${end};1H`);
82
+ this._writeRaw('\x1b[s');
83
+ this._renderInput();
84
+ };
85
+ process.stdout.on('resize', this._onResize);
86
+
87
+ // Start raw-mode keystroke handling
88
+ this._startRawInput();
89
+ }
90
+
91
+ destroy() {
92
+ if (!this._active) return;
93
+ this._active = false;
94
+
95
+ this._stopRawInput();
96
+
97
+ if (this._onResize) {
98
+ process.stdout.removeListener('resize', this._onResize);
99
+ this._onResize = null;
100
+ }
101
+
102
+ this._unpatchStdout();
103
+
104
+ // Reset scroll region to full terminal and move cursor to bottom
105
+ this._writeRaw('\x1b[r');
106
+ this._writeRaw(`\x1b[${this._rows};1H\n`);
107
+ }
108
+
109
+ onSubmit(cb) {
110
+ this._submitCb = cb;
111
+ }
112
+
113
+ prompt() {
114
+ if (!this._active || this._paused) return;
115
+ this._renderInput();
116
+ }
117
+
118
+ pause() {
119
+ if (this._paused) return;
120
+ this._paused = true;
121
+ this._stopRawInput();
122
+ // Reset scroll region so selectOption / askSecret can render anywhere
123
+ this._writeRaw('\x1b[r');
124
+ // Clear separator and input line, move cursor there so
125
+ // selectOption / askSecret renders in visible space
126
+ this._writeRaw(`\x1b[${this._rows - 1};1H\x1b[J`);
127
+ }
128
+
129
+ resume() {
130
+ if (!this._paused) return;
131
+ this._paused = false;
132
+
133
+ this._rows = process.stdout.rows || 24;
134
+ this._cols = process.stdout.columns || 80;
135
+
136
+ this._setScrollRegion();
137
+ this._drawChrome();
138
+
139
+ // Re-save output cursor at bottom of scroll region
140
+ const scrollEnd = Math.max(1, this._rows - 2);
141
+ this._writeRaw(`\x1b[${scrollEnd};1H`);
142
+ this._writeRaw('\x1b[s');
143
+
144
+ this._startRawInput();
145
+ this._renderInput();
146
+ }
147
+
148
+ getLine() {
149
+ return this._buf;
150
+ }
151
+
152
+ close() {
153
+ this.destroy();
154
+ process.exit(0);
155
+ }
156
+
157
+ // ── Scroll region / chrome ─────────────────────────────────────────
158
+
159
+ _setScrollRegion() {
160
+ const scrollEnd = Math.max(1, this._rows - 2);
161
+ this._writeRaw(`\x1b[1;${scrollEnd}r`);
162
+ }
163
+
164
+ /**
165
+ * Draw the separator line on row N-1 and clear the input row N.
166
+ * These rows are OUTSIDE the scroll region, so scrolling never touches them.
167
+ * Only needs to be called on init, resize, and resume — not on every write.
168
+ */
169
+ _drawChrome() {
170
+ const sepRow = this._rows - 1;
171
+ const inputRow = this._rows;
172
+
173
+ this._writeRaw(`\x1b[${sepRow};1H\x1b[2K`);
174
+ this._writeRaw(`${DIM}${'─'.repeat(this._cols)}${RESET}`);
175
+ this._writeRaw(`\x1b[${inputRow};1H\x1b[2K`);
176
+ }
177
+
178
+ /**
179
+ * Render the input prompt and buffer on row N, then place the cursor there.
180
+ * Uses absolute positioning only — does not disturb the SCO-saved output cursor.
181
+ */
182
+ _renderInput() {
183
+ if (!this._active || this._paused) return;
184
+ const inputRow = this._rows;
185
+
186
+ // Clear input row and draw prompt + buffer
187
+ this._writeRaw(`\x1b[${inputRow};1H\x1b[2K`);
188
+
189
+ const maxBufLen = this._cols - this._promptLen - 1;
190
+ let displayBuf = this._buf;
191
+ let displayCursor = this._cursor;
192
+
193
+ if (displayBuf.length > maxBufLen) {
194
+ const start = Math.max(0, this._cursor - Math.floor(maxBufLen / 2));
195
+ displayBuf = displayBuf.slice(start, start + maxBufLen);
196
+ displayCursor = this._cursor - start;
197
+ }
198
+
199
+ this._writeRaw(this._promptStr + displayBuf);
200
+
201
+ // Place visible cursor on input line
202
+ const cursorCol = this._promptLen + displayCursor + 1;
203
+ this._writeRaw(`\x1b[${inputRow};${cursorCol}H`);
204
+ }
205
+
206
+ // ── stdout interception ────────────────────────────────────────────
207
+
208
+ /**
209
+ * Patch process.stdout.write so that ALL output is redirected into the
210
+ * scroll region while the visible cursor stays on the input line.
211
+ *
212
+ * Uses SCO save/restore (\x1b[s / \x1b[u) to track the output cursor
213
+ * position inside the scroll region across writes. This allows:
214
+ * - Spinner: writes \r to overwrite in place → cursor stays on same row
215
+ * - Streaming: partial writes accumulate on same row
216
+ * - console.log: trailing \n causes the scroll region to scroll naturally
217
+ */
218
+ _patchStdout() {
219
+ if (this._originalWrite) return;
220
+ this._originalWrite = process.stdout.write.bind(process.stdout);
221
+
222
+ const self = this;
223
+ process.stdout.write = function (data, encoding, callback) {
224
+ if (!self._active || self._paused) {
225
+ return self._originalWrite(data, encoding, callback);
226
+ }
227
+
228
+ // Restore the output cursor in the scroll region (SCO restore)
229
+ self._originalWrite('\x1b[u');
230
+
231
+ // Write the data at the output cursor position
232
+ const result = self._originalWrite(data, encoding, callback);
233
+
234
+ // Save the new output cursor position (SCO save)
235
+ self._originalWrite('\x1b[s');
236
+
237
+ // Move visible cursor back to the input line
238
+ const cursorCol = self._promptLen + self._cursor + 1;
239
+ self._originalWrite(`\x1b[${self._rows};${cursorCol}H`);
240
+
241
+ return result;
242
+ };
243
+ }
244
+
245
+ _unpatchStdout() {
246
+ if (this._originalWrite) {
247
+ process.stdout.write = this._originalWrite;
248
+ this._originalWrite = null;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Write directly to the terminal, bypassing the stdout patch.
254
+ * Used for chrome drawing and cursor positioning.
255
+ */
256
+ _writeRaw(data) {
257
+ const writer = this._originalWrite || process.stdout.write.bind(process.stdout);
258
+ writer(data);
259
+ }
260
+
261
+ // ── Raw keystroke handling ─────────────────────────────────────────
262
+
263
+ _startRawInput() {
264
+ if (this._onData) return;
265
+
266
+ if (process.stdin.isTTY) {
267
+ process.stdin.setRawMode(true);
268
+ }
269
+ process.stdin.resume();
270
+
271
+ this._onData = (data) => this._handleData(data);
272
+ process.stdin.on('data', this._onData);
273
+ }
274
+
275
+ _stopRawInput() {
276
+ if (this._onData) {
277
+ process.stdin.removeListener('data', this._onData);
278
+ this._onData = null;
279
+ }
280
+ if (process.stdin.isTTY) {
281
+ process.stdin.setRawMode(false);
282
+ }
283
+ }
284
+
285
+ _handleData(data) {
286
+ const str = data.toString('utf8');
287
+
288
+ // Multi-line paste detection: pasted text arrives as one data event
289
+ // with embedded newline characters.
290
+ if (str.includes('\n') || str.includes('\r')) {
291
+ // Single Enter keypress
292
+ if (str === '\r' || str === '\n' || str === '\r\n') {
293
+ this._submit();
294
+ return;
295
+ }
296
+
297
+ // Multi-line paste: join all lines into one message
298
+ const joined = str
299
+ .replace(/\r\n/g, '\n')
300
+ .replace(/\r/g, '\n')
301
+ .split('\n')
302
+ .map(l => l.trimEnd())
303
+ .filter(l => l.length > 0)
304
+ .join(' ');
305
+
306
+ if (joined.length > 0) {
307
+ this._buf = joined;
308
+ this._cursor = joined.length;
309
+ this._renderInput();
310
+ this._submit();
311
+ }
312
+ return;
313
+ }
314
+
315
+ // Process individual keystrokes
316
+ let i = 0;
317
+ while (i < str.length) {
318
+ const ch = str[i];
319
+ const code = ch.charCodeAt(0);
320
+
321
+ // Ctrl+C
322
+ if (code === 3) {
323
+ if (this._buf.length > 0) {
324
+ this._buf = '';
325
+ this._cursor = 0;
326
+ this._historyIdx = -1;
327
+ this._renderInput();
328
+ } else {
329
+ this.destroy();
330
+ process.exit(0);
331
+ }
332
+ i++;
333
+ continue;
334
+ }
335
+
336
+ // Ctrl+A — home
337
+ if (code === 1) {
338
+ this._cursor = 0;
339
+ this._renderInput();
340
+ i++;
341
+ continue;
342
+ }
343
+
344
+ // Ctrl+E — end
345
+ if (code === 5) {
346
+ this._cursor = this._buf.length;
347
+ this._renderInput();
348
+ i++;
349
+ continue;
350
+ }
351
+
352
+ // Ctrl+U — clear line
353
+ if (code === 21) {
354
+ this._buf = '';
355
+ this._cursor = 0;
356
+ this._renderInput();
357
+ i++;
358
+ continue;
359
+ }
360
+
361
+ // Ctrl+K — kill to end of line
362
+ if (code === 11) {
363
+ this._buf = this._buf.slice(0, this._cursor);
364
+ this._renderInput();
365
+ i++;
366
+ continue;
367
+ }
368
+
369
+ // Ctrl+W — delete word backwards
370
+ if (code === 23) {
371
+ const before = this._buf.slice(0, this._cursor);
372
+ const trimmed = before.replace(/\s+$/, '');
373
+ const lastSpace = trimmed.lastIndexOf(' ');
374
+ const newEnd = lastSpace >= 0 ? lastSpace + 1 : 0;
375
+ this._buf = this._buf.slice(0, newEnd) + this._buf.slice(this._cursor);
376
+ this._cursor = newEnd;
377
+ this._renderInput();
378
+ i++;
379
+ continue;
380
+ }
381
+
382
+ // Backspace
383
+ if (code === 127 || code === 8) {
384
+ if (this._cursor > 0) {
385
+ this._buf = this._buf.slice(0, this._cursor - 1) + this._buf.slice(this._cursor);
386
+ this._cursor--;
387
+ this._renderInput();
388
+ }
389
+ i++;
390
+ continue;
391
+ }
392
+
393
+ // Escape sequences
394
+ if (code === 0x1b) {
395
+ i++;
396
+ if (i < str.length && str[i] === '[') {
397
+ i++;
398
+ let param = '';
399
+ while (i < str.length && str.charCodeAt(i) >= 0x30 && str.charCodeAt(i) <= 0x3f) {
400
+ param += str[i];
401
+ i++;
402
+ }
403
+ if (i < str.length) {
404
+ const final = str[i];
405
+ i++;
406
+
407
+ switch (final) {
408
+ case 'A': this._historyUp(); break;
409
+ case 'B': this._historyDown(); break;
410
+ case 'C':
411
+ if (this._cursor < this._buf.length) {
412
+ this._cursor++;
413
+ this._renderInput();
414
+ }
415
+ break;
416
+ case 'D':
417
+ if (this._cursor > 0) {
418
+ this._cursor--;
419
+ this._renderInput();
420
+ }
421
+ break;
422
+ case 'H':
423
+ this._cursor = 0;
424
+ this._renderInput();
425
+ break;
426
+ case 'F':
427
+ this._cursor = this._buf.length;
428
+ this._renderInput();
429
+ break;
430
+ case '~':
431
+ if (param === '3' && this._cursor < this._buf.length) {
432
+ this._buf = this._buf.slice(0, this._cursor) + this._buf.slice(this._cursor + 1);
433
+ this._renderInput();
434
+ }
435
+ break;
436
+ default: break;
437
+ }
438
+ }
439
+ } else if (i < str.length) {
440
+ i++; // skip Alt+key / other ESC sequences
441
+ }
442
+ continue;
443
+ }
444
+
445
+ // Skip other control characters
446
+ if (code < 0x20) {
447
+ i++;
448
+ continue;
449
+ }
450
+
451
+ // Printable character — insert at cursor
452
+ this._buf = this._buf.slice(0, this._cursor) + ch + this._buf.slice(this._cursor);
453
+ this._cursor++;
454
+ this._renderInput();
455
+ i++;
456
+ }
457
+ }
458
+
459
+ // ── History ────────────────────────────────────────────────────────
460
+
461
+ _historyUp() {
462
+ if (this._history.length === 0) return;
463
+ if (this._historyIdx === -1) {
464
+ this._savedBuf = this._buf;
465
+ }
466
+ if (this._historyIdx < this._history.length - 1) {
467
+ this._historyIdx++;
468
+ this._buf = this._history[this._historyIdx];
469
+ this._cursor = this._buf.length;
470
+ this._renderInput();
471
+ }
472
+ }
473
+
474
+ _historyDown() {
475
+ if (this._historyIdx <= -1) return;
476
+ this._historyIdx--;
477
+ if (this._historyIdx === -1) {
478
+ this._buf = this._savedBuf;
479
+ } else {
480
+ this._buf = this._history[this._historyIdx];
481
+ }
482
+ this._cursor = this._buf.length;
483
+ this._renderInput();
484
+ }
485
+
486
+ // ── Submit ─────────────────────────────────────────────────────────
487
+
488
+ _submit() {
489
+ const line = this._buf.trim();
490
+ this._buf = '';
491
+ this._cursor = 0;
492
+ this._historyIdx = -1;
493
+ this._savedBuf = '';
494
+
495
+ if (line.length > 0) {
496
+ if (this._history[0] !== line) {
497
+ this._history.unshift(line);
498
+ if (this._history.length > HISTORY_MAX) {
499
+ this._history.pop();
500
+ }
501
+ }
502
+ }
503
+
504
+ this._renderInput();
505
+
506
+ if (this._submitCb) {
507
+ this._submitCb(line);
508
+ }
509
+ }
510
+ }