blyuecode 0.8.0 → 0.8.3

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/dist/core/deps.js CHANGED
@@ -1,4 +1,4 @@
1
- import { exec } from 'child_process';
1
+ import { exec, spawn } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import os from 'os';
4
4
  const execAsync = promisify(exec);
@@ -77,18 +77,23 @@ export async function installMediaDeps(onOutput) {
77
77
  return { ok: false, output: 'no install command for this platform' };
78
78
  }
79
79
  return new Promise((resolve) => {
80
- const proc = exec(cmd, { maxBuffer: 10 * 1024 * 1024 });
81
- let buf = '';
82
- proc.stdout?.on('data', (c) => {
83
- const s = c.toString();
84
- buf += s;
85
- onOutput?.(s);
86
- });
87
- proc.stderr?.on('data', (c) => {
88
- const s = c.toString();
89
- buf += s;
90
- onOutput?.(s);
80
+ const proc = spawn('sh', ['-c', cmd], {
81
+ stdio: ['ignore', 'pipe', 'pipe'],
91
82
  });
83
+ let buf = '';
84
+ const forward = (chunk) => {
85
+ const text = chunk.toString('utf8');
86
+ buf += text;
87
+ if (onOutput) {
88
+ for (const line of text.split(/[\r\n]+/)) {
89
+ const t = line.trim();
90
+ if (t)
91
+ onOutput(t);
92
+ }
93
+ }
94
+ };
95
+ proc.stdout?.on('data', forward);
96
+ proc.stderr?.on('data', forward);
92
97
  proc.on('exit', (code) => {
93
98
  resolve({ ok: code === 0, output: buf.slice(-2000) });
94
99
  });
@@ -1,9 +1,9 @@
1
1
  import { exec } from 'child_process';
2
- import { runYtdlp } from './ytdlp.js';
3
2
  import { promisify } from 'util';
4
3
  import fs from 'fs';
5
4
  import path from 'path';
6
5
  import os from 'os';
6
+ import { runYtdlpStream } from './ytdlp.js';
7
7
  const execAsync = promisify(exec);
8
8
  const MUSIC_DIR = path.join(os.homedir(), '.blyue', 'music');
9
9
  const VIDEO_DIR = path.join(os.homedir(), '.blyue', 'video');
@@ -35,16 +35,15 @@ export async function checkDeps() {
35
35
  return { ytdlp, ffmpeg, mpv, ok: missing.length === 0, missing };
36
36
  }
37
37
  export async function searchYouTubeN(query, n = 5) {
38
- const r = await runYtdlp([
38
+ const r = await runYtdlpStream([
39
39
  'ytsearch' + n + ':' + query,
40
40
  '--dump-json',
41
41
  '--no-warnings',
42
42
  '--skip-download',
43
43
  '--default-search', 'ytsearch' + n,
44
44
  ]);
45
- if (!r.ok) {
45
+ if (!r.ok)
46
46
  throw new Error(r.error?.userMessage || 'yt-dlp search failed');
47
- }
48
47
  const lines = r.stdout.trim().split('\n').filter(Boolean);
49
48
  const out = [];
50
49
  for (const line of lines) {
@@ -65,52 +64,37 @@ export async function searchYouTubeN(query, n = 5) {
65
64
  return out;
66
65
  }
67
66
  export async function searchYouTube(query) {
68
- const r = await runYtdlp([
69
- 'ytsearch1:' + query,
70
- '--dump-json',
71
- '--no-warnings',
72
- '--skip-download',
73
- '--default-search', 'ytsearch1',
74
- ]);
75
- if (!r.ok) {
76
- throw new Error(r.error?.userMessage || 'yt-dlp search failed');
77
- }
78
- const first = r.stdout.trim().split('\n')[0];
79
- if (!first)
67
+ const results = await searchYouTubeN(query, 1);
68
+ if (results.length === 0)
80
69
  throw new Error('no results');
81
- let info;
82
- try {
83
- info = JSON.parse(first);
84
- }
85
- catch (err) {
86
- throw new Error('could not parse yt-dlp output: ' + err.message);
87
- }
88
- return {
89
- id: String(info.id || ''),
90
- title: String(info.title || 'unknown'),
91
- duration: Number(info.duration) || 0,
92
- uploader: String(info.uploader || info.channel || ''),
93
- kind: 'audio',
94
- };
70
+ return results[0];
95
71
  }
96
72
  export async function downloadAudio(track, onProgress) {
97
73
  fs.mkdirSync(MUSIC_DIR, { recursive: true });
98
74
  const outTemplate = path.join(MUSIC_DIR, '%(id)s.%(ext)s');
99
75
  onProgress?.({ phase: 'downloading' });
100
- const r = await runYtdlp([
76
+ const onLine = (line) => {
77
+ if (line.includes('[download]') || line.includes('ETA')) {
78
+ onProgress?.({ phase: 'downloading', line });
79
+ }
80
+ };
81
+ const r = await runYtdlpStream([
101
82
  '-f', 'bestaudio[ext=m4a]/bestaudio[ext=webm]/bestaudio/best',
102
83
  '--no-part',
103
84
  '--no-mtime',
104
85
  '--no-cache-dir',
105
- '--socket-timeout', '10',
106
- '--concurrent-fragments', '16',
86
+ '--socket-timeout', '15',
87
+ '--concurrent-fragments', '8',
107
88
  '--buffer-size', '16K',
108
- '--http-chunk-size', '10M',
89
+ '--http-chunk-size', '5M',
90
+ '--retries', '5',
91
+ '--fragment-retries', '5',
92
+ '--extractor-args', 'youtube:player_client=android,web_safari',
109
93
  '-o', outTemplate,
110
94
  '--no-playlist',
111
95
  '--no-warnings',
112
96
  'https://www.youtube.com/watch?v=' + track.id,
113
- ]);
97
+ ], onLine);
114
98
  if (!r.ok)
115
99
  throw new Error(r.error?.userMessage || 'yt-dlp download failed');
116
100
  const candidates = fs
@@ -122,18 +106,6 @@ export async function downloadAudio(track, onProgress) {
122
106
  return { ...track, file: candidates[0], kind: 'audio' };
123
107
  }
124
108
  export const VIDEO_QUALITIES = ['360p', '480p', '720p', '1080p', 'best'];
125
- export function estimateSizeMb(durationSec, quality) {
126
- if (!durationSec || durationSec <= 0)
127
- return 0;
128
- const bitrateMbps = {
129
- '360p': 0.6,
130
- '480p': 1.1,
131
- '720p': 2.5,
132
- '1080p': 5.0,
133
- best: 8.0,
134
- };
135
- return Math.round((bitrateMbps[quality] * durationSec) / 8);
136
- }
137
109
  export function parseQuality(s) {
138
110
  if (!s)
139
111
  return '720p';
@@ -150,6 +122,18 @@ export function parseQuality(s) {
150
122
  return '360p';
151
123
  return '720p';
152
124
  }
125
+ export function estimateSizeMb(durationSec, quality) {
126
+ if (!durationSec || durationSec <= 0)
127
+ return 0;
128
+ const bitrateMbps = {
129
+ '360p': 0.6,
130
+ '480p': 1.1,
131
+ '720p': 2.5,
132
+ '1080p': 5.0,
133
+ best: 8.0,
134
+ };
135
+ return Math.round((bitrateMbps[quality] * durationSec) / 8);
136
+ }
153
137
  function formatSelector(q) {
154
138
  if (q === 'best')
155
139
  return 'bestvideo+bestaudio/best';
@@ -160,32 +144,22 @@ export async function downloadVideo(track, quality, onProgress) {
160
144
  fs.mkdirSync(VIDEO_DIR, { recursive: true });
161
145
  const outTemplate = path.join(VIDEO_DIR, '%(id)s.%(ext)s');
162
146
  onProgress?.({ phase: 'downloading' });
147
+ const onLine = (line) => {
148
+ if (line.includes('[download]') || line.includes('ETA')) {
149
+ onProgress?.({ phase: 'downloading', line });
150
+ }
151
+ };
163
152
  const formatSel = quality === '360p'
164
153
  ? '18/best[height<=360][ext=mp4]/best[height<=360]'
165
154
  : quality === '480p'
166
155
  ? '22/best[height<=480][ext=mp4]/best[height<=480]'
167
156
  : formatSelector(quality);
168
- const args = [
169
- '-f', formatSel,
170
- '--extractor-args', 'youtube:player_client=android,web_safari',
171
- '--no-part',
172
- '--no-mtime',
173
- '--no-cache-dir',
174
- '--socket-timeout', '15',
175
- '--concurrent-fragments', '8',
176
- '--buffer-size', '16K',
177
- '--http-chunk-size', '5M',
178
- '--retries', '5',
179
- '--fragment-retries', '5',
180
- '-o', outTemplate,
181
- '--no-playlist',
182
- '--no-warnings',
183
- 'https://www.youtube.com/watch?v=' + track.id,
184
- ];
157
+ const args = ['-f', formatSel];
185
158
  if (quality !== '360p' && quality !== '480p') {
186
- args.splice(2, 0, '--merge-output-format', 'mp4');
159
+ args.push('--merge-output-format', 'mp4');
187
160
  }
188
- const r = await runYtdlp(args);
161
+ args.push('--extractor-args', 'youtube:player_client=android,web_safari', '--no-part', '--no-mtime', '--no-cache-dir', '--socket-timeout', '15', '--concurrent-fragments', '8', '--buffer-size', '16K', '--http-chunk-size', '5M', '--retries', '5', '--fragment-retries', '5', '-o', outTemplate, '--no-playlist', '--no-warnings', 'https://www.youtube.com/watch?v=' + track.id);
162
+ const r = await runYtdlpStream(args, onLine);
189
163
  if (!r.ok)
190
164
  throw new Error(r.error?.userMessage || 'yt-dlp download failed');
191
165
  const candidates = fs.readdirSync(VIDEO_DIR)
@@ -1,4 +1,4 @@
1
- import { exec } from 'child_process';
1
+ import { exec, spawn } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
@@ -110,6 +110,64 @@ export async function runYtdlp(args, opts = {}) {
110
110
  return { ok: false, stdout: err.stdout || '', stderr, error: classified };
111
111
  }
112
112
  }
113
+ export async function runYtdlpStream(args, onProgress, opts = {}) {
114
+ const fullArgs = [...args, '--newline', '--progress'];
115
+ const cmd = 'yt-dlp ' + fullArgs.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(' ');
116
+ const started = Date.now();
117
+ return new Promise((resolve) => {
118
+ const proc = spawn('yt-dlp', fullArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
119
+ let stdout = '';
120
+ let stderr = '';
121
+ const onData = (chunk, toStdout) => {
122
+ const text = chunk.toString('utf8');
123
+ if (toStdout)
124
+ stdout += text;
125
+ else
126
+ stderr += text;
127
+ if (onProgress) {
128
+ for (const line of text.split(/[\r\n]+/)) {
129
+ const t = line.trim();
130
+ if (t)
131
+ onProgress(t);
132
+ }
133
+ }
134
+ };
135
+ proc.stdout?.on('data', (c) => onData(c, true));
136
+ proc.stderr?.on('data', (c) => onData(c, false));
137
+ const timeout = opts.timeoutMs ?? 30 * 60 * 1000;
138
+ const timer = setTimeout(() => {
139
+ try {
140
+ proc.kill('SIGTERM');
141
+ }
142
+ catch { }
143
+ log(`TIMEOUT ${Date.now() - started}ms ${cmd.slice(0, 200)}`);
144
+ resolve({
145
+ ok: false, stdout, stderr,
146
+ error: { kind: 'unknown', userMessage: 'download timed out', detail: '', autoFixable: false },
147
+ });
148
+ }, timeout);
149
+ proc.on('exit', (code) => {
150
+ clearTimeout(timer);
151
+ if (code === 0) {
152
+ log(`OK ${Date.now() - started}ms ${cmd.slice(0, 200)}`);
153
+ resolve({ ok: true, stdout, stderr });
154
+ }
155
+ else {
156
+ const combined = stderr || stdout;
157
+ const classified = classify(combined, code ?? 1);
158
+ log(`ERR ${Date.now() - started}ms kind=${classified.kind} code=${code} detail=${classified.detail}`);
159
+ resolve({ ok: false, stdout, stderr, error: classified });
160
+ }
161
+ });
162
+ proc.on('error', (err) => {
163
+ clearTimeout(timer);
164
+ resolve({
165
+ ok: false, stdout, stderr,
166
+ error: { kind: 'unknown', userMessage: err.message, detail: String(err), autoFixable: false },
167
+ });
168
+ });
169
+ });
170
+ }
113
171
  export function updateCmd() {
114
172
  const isTermux = !!process.env.TERMUX_VERSION || !!process.env.PREFIX?.includes('com.termux');
115
173
  if (isTermux)
package/dist/ui/Chat.js CHANGED
@@ -40,6 +40,48 @@ const fmtTime = (s) => {
40
40
  const sec = Math.floor(s % 60);
41
41
  return h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0') : m + ':' + String(sec).padStart(2, '0');
42
42
  };
43
+ const parseYtdlpProgress = (line) => {
44
+ const m = line.match(/\[download\]\s+([\d.]+)%\s+of\s+~?\s*([\d.]+\s*\w+)(?:\s+at\s+([\d.]+\s*\w+\/s))?(?:\s+ETA\s+([\d:]+))?/);
45
+ if (!m)
46
+ return null;
47
+ const [, pct, size, speed, eta] = m;
48
+ const pctNum = Math.min(100, Math.max(0, parseFloat(pct)));
49
+ const bars = 20;
50
+ const filled = Math.round((pctNum / 100) * bars);
51
+ const bar = '━'.repeat(filled) + '─'.repeat(bars - filled);
52
+ const parts = [pct + '%', size.trim()];
53
+ if (speed)
54
+ parts.push(speed.trim());
55
+ if (eta)
56
+ parts.push('ETA ' + eta);
57
+ return bar + ' ' + parts.join(' · ');
58
+ };
59
+ const parsePkgProgress = (line) => {
60
+ let m = line.match(/(\d+)%\s*\[/);
61
+ if (m) {
62
+ const pct = Math.min(100, Math.max(0, parseInt(m[1], 10)));
63
+ const bars = 20;
64
+ const filled = Math.round((pct / 100) * bars);
65
+ return '━'.repeat(filled) + '─'.repeat(bars - filled) + ' ' + pct + '%';
66
+ }
67
+ m = line.match(/^Get:\d+\s+\S+\s+\[([\d.,]+\s*\w+)\]/);
68
+ if (m)
69
+ return '· fetching ' + m[1];
70
+ m = line.match(/^Setting up ([^ ]+)/);
71
+ if (m)
72
+ return '· unpacking ' + m[1];
73
+ m = line.match(/^Unpacking ([^ ]+)/);
74
+ if (m)
75
+ return '· unpacking ' + m[1];
76
+ m = line.match(/^(Selecting|Preparing) ([^ ]+)/);
77
+ if (m)
78
+ return '· ' + m[2];
79
+ if (line.startsWith('Reading package') || line.startsWith('Building dependency'))
80
+ return '· ' + line.slice(0, 60);
81
+ if (line.includes('Setting up') || line.includes('unpack'))
82
+ return '· ' + line.slice(0, 60);
83
+ return null;
84
+ };
43
85
  const HistoryLine = ({ item }) => {
44
86
  if (item.kind === 'user') {
45
87
  return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: pastelAt(0.08), bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }));
@@ -216,7 +258,14 @@ export const Chat = ({ resumedCount = 0 }) => {
216
258
  if (pm.kind === 'audio') {
217
259
  setMediaStatus('downloading: ' + pm.track.title + '…');
218
260
  try {
219
- const d = await downloadAudio(pm.track);
261
+ const d = await downloadAudio(pm.track, (p) => {
262
+ const line = p.line;
263
+ if (line) {
264
+ const pretty = parseYtdlpProgress(line);
265
+ if (pretty)
266
+ setMediaStatus('♪ ' + pretty);
267
+ }
268
+ });
220
269
  setMediaStatus('loading into player…');
221
270
  await player.play(d);
222
271
  setMediaStatus(null);
@@ -240,7 +289,14 @@ export const Chat = ({ resumedCount = 0 }) => {
240
289
  else {
241
290
  setMediaStatus('downloading ' + pm.quality + ': ' + pm.track.title + '…');
242
291
  try {
243
- const v = await downloadVideo(pm.track, pm.quality);
292
+ const v = await downloadVideo(pm.track, pm.quality, (p) => {
293
+ const line = p.line;
294
+ if (line) {
295
+ const pretty = parseYtdlpProgress(line);
296
+ if (pretty)
297
+ setMediaStatus('♪ ' + pretty);
298
+ }
299
+ });
244
300
  setMediaStatus('opening player…');
245
301
  const { cleanup, notice } = await playVideo(v);
246
302
  setMediaStatus(null);
@@ -433,9 +489,27 @@ export const Chat = ({ resumedCount = 0 }) => {
433
489
  ...h,
434
490
  mkItem('tool', 'will run: ' + plat.installCmd),
435
491
  mkItem('tool', '~' + plat.approximateSizeMb + ' MB via ' + plat.updateHint),
436
- mkItem('tool', 'proceed? [y] yes · [n] cancel'),
492
+ mkItem('tool', 'starting install…'),
437
493
  ]);
438
- setPendingInstall(plat.installCmd);
494
+ setLoading(true);
495
+ let lastLine = '';
496
+ const r = await installMediaDeps((line) => {
497
+ const pretty = parsePkgProgress(line);
498
+ if (pretty && pretty !== lastLine) {
499
+ lastLine = pretty;
500
+ setHistory((h) => [...h, mkItem('tool', ' ' + pretty)]);
501
+ }
502
+ else if (!pretty && line.length < 100) {
503
+ setHistory((h) => [...h, mkItem('tool', ' ' + line.slice(0, 100))]);
504
+ }
505
+ });
506
+ setLoading(false);
507
+ if (r.ok) {
508
+ setHistory((h) => [...h, mkItem('tool', '✓ media tools installed')]);
509
+ }
510
+ else {
511
+ setHistory((h) => [...h, mkItem('error', 'install failed: ' + r.output.slice(-400))]);
512
+ }
439
513
  return;
440
514
  }
441
515
  if (trimmed.startsWith('/image ')) {
package/dist/ui/splash.js CHANGED
@@ -106,7 +106,7 @@ function sleep(ms) {
106
106
  }
107
107
  export async function playSplash(version) {
108
108
  const cols = process.stdout.columns || 80;
109
- process.stdout.write(HIDE_CURSOR);
109
+ process.stdout.write('\x1b[?1049h' + HIDE_CURSOR);
110
110
  try {
111
111
  for (let frame = 0; frame <= T_HOLD_END; frame++) {
112
112
  process.stdout.write(HOME_CLEAR + buildFrame(frame, cols, version));
@@ -116,6 +116,6 @@ export async function playSplash(version) {
116
116
  process.stdout.write(HOME_CLEAR);
117
117
  }
118
118
  finally {
119
- process.stdout.write(SHOW_CURSOR);
119
+ process.stdout.write(SHOW_CURSOR + '\x1b[?1049l');
120
120
  }
121
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blyuecode",
3
- "version": "0.8.0",
3
+ "version": "0.8.3",
4
4
  "description": "BLYUE Code — a coding agent by Lumen",
5
5
  "type": "module",
6
6
  "license": "MIT",