@yanzc/claude-pro 0.4.5 → 0.5.0
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 +36 -4
- package/package.json +8 -2
- package/src/agent.js +38 -1
- package/src/api.js +12 -0
- package/src/checkpoints.js +20 -1
- package/src/commands.js +148 -35
- package/src/config.js +3 -0
- package/src/main.js +2 -0
- package/src/peers.js +40 -7
- package/src/prompts.js +17 -0
- package/src/qr.js +206 -0
- package/src/remote.js +540 -0
- package/src/session.js +4 -1
- package/src/tools/files.js +13 -4
- package/src/tools/peers.js +26 -19
- package/src/transfer.js +260 -0
- package/src/ui/dialogs.js +3 -2
- package/src/ui/editor.js +72 -41
- package/src/ui/repl.js +637 -72
- package/src/ui/screen.js +4 -1
- package/src/utils/downloads.js +83 -0
- package/src/utils/images.js +175 -0
package/src/ui/screen.js
CHANGED
|
@@ -52,6 +52,7 @@ export class Screen {
|
|
|
52
52
|
this.resizeTimer = null;
|
|
53
53
|
this.resizeSince = 0;
|
|
54
54
|
this.closed = false;
|
|
55
|
+
this.onLog = null;
|
|
55
56
|
this.titleProvider = null; // () => the terminal title (tab label) to show now
|
|
56
57
|
this.title = null; // what was last sent
|
|
57
58
|
this.titleInherited = false; // the process before this one in the session already saved the shell's title
|
|
@@ -86,8 +87,10 @@ export class Screen {
|
|
|
86
87
|
|
|
87
88
|
setLive(fn) { this.renderLive = fn; this.schedule(); }
|
|
88
89
|
|
|
89
|
-
// Append lines (strings, may contain \n) to the static log.
|
|
90
|
+
// Append lines (strings, may contain \n) to the static log. `onLog(items)` is told about each call (the remote
|
|
91
|
+
// control mirrors what the terminal prints).
|
|
90
92
|
log(...items) {
|
|
93
|
+
if (this.onLog) { try { this.onLog(items); } catch { /* the listener's problem */ } }
|
|
91
94
|
for (const item of items) {
|
|
92
95
|
if (Array.isArray(item)) this.staticQueue.push(...item);
|
|
93
96
|
else this.staticQueue.push(String(item));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Files that other sessions send this one (/sendfile, MessagePeer's files) land in the user's Downloads folder:
|
|
2
|
+
// on Windows the folder the shell knows as Downloads (it can have been moved), else ~/Downloads (or what
|
|
3
|
+
// xdg-user-dir says). CLAUDE_PRO_DOWNLOAD_DIR replaces it.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
export const MAX_SEND_BYTES = 5 * 1024 ** 3; // a file; the gateway takes no more
|
|
11
|
+
export const MAX_SEND_FILES = 20; // a message
|
|
12
|
+
|
|
13
|
+
const DOWNLOADS_GUID = '{374DE290-123F-4565-9164-39C4925E467B}';
|
|
14
|
+
const PS_DOWNLOADS = `
|
|
15
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
16
|
+
$p = (Get-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders' -Name '${DOWNLOADS_GUID}' -ErrorAction Stop).'${DOWNLOADS_GUID}'
|
|
17
|
+
[Console]::Out.Write($p)
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
function output(cmd, args) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
let out = '';
|
|
23
|
+
let child;
|
|
24
|
+
try {
|
|
25
|
+
child = spawn(cmd, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
26
|
+
} catch {
|
|
27
|
+
resolve('');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const timer = setTimeout(() => child.kill(), 10_000);
|
|
31
|
+
child.stdout.setEncoding('utf8');
|
|
32
|
+
child.stdout.on('data', (d) => { out += d; });
|
|
33
|
+
child.on('error', () => { clearTimeout(timer); resolve(''); });
|
|
34
|
+
child.on('close', (code) => { clearTimeout(timer); resolve(code === 0 ? out.trim() : ''); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let cached = null;
|
|
39
|
+
|
|
40
|
+
export function downloadsDir(env = process.env) {
|
|
41
|
+
if (env.CLAUDE_PRO_DOWNLOAD_DIR) return Promise.resolve(path.resolve(env.CLAUDE_PRO_DOWNLOAD_DIR));
|
|
42
|
+
cached ??= (async () => {
|
|
43
|
+
const home = os.homedir();
|
|
44
|
+
let dir = '';
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
dir = await output('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', Buffer.from(PS_DOWNLOADS, 'utf16le').toString('base64')]);
|
|
47
|
+
} else if (process.platform !== 'darwin') {
|
|
48
|
+
dir = await output('xdg-user-dir', ['DOWNLOAD']);
|
|
49
|
+
if (path.resolve(dir || home) === path.resolve(home)) dir = ''; // xdg's answer when it has none
|
|
50
|
+
}
|
|
51
|
+
return dir && path.isAbsolute(dir) ? dir : path.join(home, 'Downloads');
|
|
52
|
+
})();
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const RESERVED = /^(con|prn|aux|nul|com\d|lpt\d)$/i;
|
|
57
|
+
|
|
58
|
+
// A name a file can be saved under, on any system, from the name another computer gave it.
|
|
59
|
+
export function safeFileName(name) {
|
|
60
|
+
let base = String(name || '').split(/[\\/]/).pop();
|
|
61
|
+
base = base.replace(/[\u0000-\u001f\u007f<>:"|?*]/g, '_').replace(/[. ]+$/g, '').trim();
|
|
62
|
+
if (!base || base === '.' || base === '..') base = 'file';
|
|
63
|
+
const ext = path.extname(base);
|
|
64
|
+
if (RESERVED.test(base.slice(0, base.length - ext.length))) base = `_${base}`;
|
|
65
|
+
if (base.length > 200) base = base.slice(0, 200 - ext.length) + ext;
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The files to send, checked: [{path, name, size}], or throws with what is wrong with which.
|
|
70
|
+
export function filesToSend(paths) {
|
|
71
|
+
if (!paths.length) throw new Error('No file to send.');
|
|
72
|
+
if (paths.length > MAX_SEND_FILES) throw new Error(`At most ${MAX_SEND_FILES} files at a time (${paths.length} given).`);
|
|
73
|
+
return paths.map((p) => {
|
|
74
|
+
let st;
|
|
75
|
+
try { st = fs.statSync(p); } catch { throw new Error(`File not found: ${p}`); }
|
|
76
|
+
if (st.isDirectory()) throw new Error(`${p} is a folder: send the files in it (or zip it first).`);
|
|
77
|
+
if (!st.isFile()) throw new Error(`Not a file: ${p}`);
|
|
78
|
+
if (st.size > MAX_SEND_BYTES) throw new Error(`${path.basename(p)} is ${formatBytes(st.size)}: at most ${formatBytes(MAX_SEND_BYTES)} a file.`);
|
|
79
|
+
return { path: p, name: path.basename(p), size: st.size };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const formatBytes = (n) => (n >= 1024 ** 3 ? `${(n / 1024 ** 3).toFixed(1)} GB` : n >= 1048576 ? `${(n / 1048576).toFixed(1)} MB` : n >= 1024 ? `${Math.round(n / 1024)} KB` : `${n} B`);
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Images for the model. The API refuses an image wider or taller than 8000 px, and in a request that carries many
|
|
2
|
+
// images (a long session that looked at screenshots) one beyond 2000 px. It scales anything whose long edge is over
|
|
3
|
+
// 1568 px down to that anyway, so images are scaled here to fit 1568 px before they go into the conversation:
|
|
4
|
+
// the model loses nothing it would have seen, and the conversation never carries an image that a later request
|
|
5
|
+
// is refused for. There is no image library: Windows scales with System.Drawing (PowerShell), macOS with sips,
|
|
6
|
+
// elsewhere ImageMagick when it is installed.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
export const MAX_IMAGE_EDGE = 1568;
|
|
14
|
+
const API_MAX_EDGE = 2000; // what a request with many images accepts: sent as it is when it cannot be scaled
|
|
15
|
+
export const MAX_IMAGE_BYTES = 3_750_000; // ~5MB once base64 encoded
|
|
16
|
+
|
|
17
|
+
// {width, height} from the header of a PNG, JPEG, GIF or WebP; null for anything else.
|
|
18
|
+
export function imageSize(buf) {
|
|
19
|
+
if (!buf || buf.length < 10) return null;
|
|
20
|
+
if (buf.length >= 24 && buf.readUInt32BE(0) === 0x89504e47) return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
21
|
+
if (buf.toString('latin1', 0, 3) === 'GIF') return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
22
|
+
if (buf.length >= 30 && buf.toString('latin1', 0, 4) === 'RIFF' && buf.toString('latin1', 8, 12) === 'WEBP') {
|
|
23
|
+
const kind = buf.toString('latin1', 12, 16);
|
|
24
|
+
if (kind === 'VP8 ') return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
|
|
25
|
+
if (kind === 'VP8L') { const b = buf.readUInt32LE(21); return { width: (b & 0x3fff) + 1, height: ((b >>> 14) & 0x3fff) + 1 }; }
|
|
26
|
+
if (kind === 'VP8X') return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (buf[0] === 0xff && buf[1] === 0xd8) {
|
|
30
|
+
let i = 2;
|
|
31
|
+
while (i + 9 < buf.length) {
|
|
32
|
+
if (buf[i] !== 0xff) { i++; continue; }
|
|
33
|
+
const marker = buf[i + 1];
|
|
34
|
+
if (marker === 0xff) { i++; continue; }
|
|
35
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd8)) { i += 2; continue; }
|
|
36
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
37
|
+
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
38
|
+
}
|
|
39
|
+
i += 2 + buf.readUInt16BE(i + 2);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function run(cmd, args, { env, timeout = 30_000 } = {}) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
let out = '';
|
|
48
|
+
let child;
|
|
49
|
+
try {
|
|
50
|
+
child = spawn(cmd, args, { env: { ...process.env, ...env }, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
51
|
+
} catch {
|
|
52
|
+
resolve({ code: -1, out: '' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const timer = setTimeout(() => child.kill(), timeout);
|
|
56
|
+
child.stdout.on('data', (d) => { out += d; });
|
|
57
|
+
child.stderr.on('data', () => {});
|
|
58
|
+
child.on('error', () => { clearTimeout(timer); resolve({ code: -1, out }); });
|
|
59
|
+
child.on('close', (code) => { clearTimeout(timer); resolve({ code, out: out.trim() }); });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const PS_SCALE = `
|
|
64
|
+
$ErrorActionPreference = 'Stop'
|
|
65
|
+
Add-Type -AssemblyName System.Drawing
|
|
66
|
+
$img = [System.Drawing.Image]::FromFile($env:CLAUDE_PRO_IMG_IN)
|
|
67
|
+
$max = [int]$env:CLAUDE_PRO_IMG_MAX
|
|
68
|
+
$scale = [Math]::Min(1.0, $max / [Math]::Max($img.Width, $img.Height))
|
|
69
|
+
$w = [Math]::Max(1, [int][Math]::Round($img.Width * $scale)); $h = [Math]::Max(1, [int][Math]::Round($img.Height * $scale))
|
|
70
|
+
$bmp = New-Object System.Drawing.Bitmap $w, $h
|
|
71
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
72
|
+
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
73
|
+
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
|
|
74
|
+
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
|
75
|
+
$g.DrawImage($img, 0, 0, $w, $h)
|
|
76
|
+
$g.Dispose(); $img.Dispose()
|
|
77
|
+
$out = $env:CLAUDE_PRO_IMG_OUT
|
|
78
|
+
$format = 'png'
|
|
79
|
+
if ($env:CLAUDE_PRO_IMG_JPEG -ne '1') { $bmp.Save($out, [System.Drawing.Imaging.ImageFormat]::Png) }
|
|
80
|
+
if ($env:CLAUDE_PRO_IMG_JPEG -eq '1' -or (Get-Item $out).Length -gt [int]$env:CLAUDE_PRO_IMG_BYTES) {
|
|
81
|
+
$codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }
|
|
82
|
+
$params = New-Object System.Drawing.Imaging.EncoderParameters 1
|
|
83
|
+
$params.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter ([System.Drawing.Imaging.Encoder]::Quality), 85L
|
|
84
|
+
$bmp.Save($out, $codec, $params)
|
|
85
|
+
$format = 'jpeg'
|
|
86
|
+
}
|
|
87
|
+
$bmp.Dispose()
|
|
88
|
+
Write-Output "IMG $format"
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
// The image file `input` scaled to fit `max` px into a new file: {buffer, media_type}, or null when nothing here
|
|
92
|
+
// can scale images.
|
|
93
|
+
async function scaleFile(input, { max, jpeg }) {
|
|
94
|
+
const out = path.join(os.tmpdir(), `claude-pro-img-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
95
|
+
try {
|
|
96
|
+
if (process.platform === 'win32') {
|
|
97
|
+
const r = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', Buffer.from(PS_SCALE, 'utf16le').toString('base64')], {
|
|
98
|
+
env: { CLAUDE_PRO_IMG_IN: input, CLAUDE_PRO_IMG_OUT: out, CLAUDE_PRO_IMG_MAX: String(max), CLAUDE_PRO_IMG_JPEG: jpeg ? '1' : '', CLAUDE_PRO_IMG_BYTES: String(MAX_IMAGE_BYTES) },
|
|
99
|
+
});
|
|
100
|
+
const m = /IMG (png|jpeg)/.exec(r.out);
|
|
101
|
+
if (r.code !== 0 || !m) return null;
|
|
102
|
+
return { buffer: fs.readFileSync(out), media_type: `image/${m[1]}` };
|
|
103
|
+
}
|
|
104
|
+
const target = `${out}.${jpeg ? 'jpg' : 'png'}`;
|
|
105
|
+
const tries = process.platform === 'darwin'
|
|
106
|
+
? [['sips', ['-Z', String(max), '-s', 'format', jpeg ? 'jpeg' : 'png', input, '--out', target]]]
|
|
107
|
+
: [['magick', [input, '-resize', `${max}x${max}>`, target]], ['convert', [input, '-resize', `${max}x${max}>`, target]]];
|
|
108
|
+
for (const [cmd, args] of tries) {
|
|
109
|
+
const r = await run(cmd, args);
|
|
110
|
+
if (r.code === 0 && fs.existsSync(target)) {
|
|
111
|
+
try { return { buffer: fs.readFileSync(target), media_type: jpeg ? 'image/jpeg' : 'image/png' }; } finally { fs.rm(target, { force: true }, () => {}); }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
} finally {
|
|
118
|
+
fs.rm(out, { force: true }, () => {});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The image as the model should get it. `data` is base64. Returns {data, media_type, width, height, from} where
|
|
123
|
+
// `from` is the size it had when it was scaled (null when it is sent as it is), or {error} when it is too large
|
|
124
|
+
// and cannot be scaled here.
|
|
125
|
+
export async function fitImage({ data, media_type }) {
|
|
126
|
+
const buf = Buffer.from(data, 'base64');
|
|
127
|
+
const size = imageSize(buf);
|
|
128
|
+
const keep = { data, media_type, width: size?.width ?? null, height: size?.height ?? null, from: null };
|
|
129
|
+
if (!size) return keep; // not a format it knows: the API will say what it thinks of it
|
|
130
|
+
const edge = Math.max(size.width, size.height);
|
|
131
|
+
if (edge <= MAX_IMAGE_EDGE && buf.length <= MAX_IMAGE_BYTES) return keep;
|
|
132
|
+
const ext = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp' }[media_type] || '.img';
|
|
133
|
+
const input = path.join(os.tmpdir(), `claude-pro-img-in-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`);
|
|
134
|
+
let scaled = null;
|
|
135
|
+
try {
|
|
136
|
+
fs.writeFileSync(input, buf);
|
|
137
|
+
scaled = await scaleFile(input, { max: Math.min(edge, MAX_IMAGE_EDGE), jpeg: media_type === 'image/jpeg' });
|
|
138
|
+
} catch { /* scaled stays null */ } finally {
|
|
139
|
+
fs.rm(input, { force: true }, () => {});
|
|
140
|
+
}
|
|
141
|
+
const got = scaled && imageSize(scaled.buffer);
|
|
142
|
+
if (got && Math.max(got.width, got.height) <= API_MAX_EDGE && scaled.buffer.length <= MAX_IMAGE_BYTES) {
|
|
143
|
+
return { data: scaled.buffer.toString('base64'), media_type: scaled.media_type, width: got.width, height: got.height, from: size };
|
|
144
|
+
}
|
|
145
|
+
if (edge <= API_MAX_EDGE && buf.length <= MAX_IMAGE_BYTES) return keep;
|
|
146
|
+
return { error: `the image is ${size.width}×${size.height} px${buf.length > MAX_IMAGE_BYTES ? ` and ${(buf.length / 1e6).toFixed(1)}MB` : ''}, more than the API accepts (${API_MAX_EDGE} px a side when a conversation has many images), and it could not be scaled down here` };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Every base64 image block in `blocks` (content blocks, tool_result blocks with content inside, or messages)
|
|
150
|
+
// fitted in place, so that the conversation that holds them changes too. Returns how many were changed; an
|
|
151
|
+
// image that cannot be fitted becomes a text block that says so.
|
|
152
|
+
export async function fitImagesIn(blocks) {
|
|
153
|
+
let changed = 0;
|
|
154
|
+
const visit = async (list) => {
|
|
155
|
+
if (!Array.isArray(list)) return;
|
|
156
|
+
for (const b of list) {
|
|
157
|
+
if (!b || typeof b !== 'object') continue;
|
|
158
|
+
if (b.role && Array.isArray(b.content)) { await visit(b.content); continue; }
|
|
159
|
+
if (b.type === 'tool_result' && Array.isArray(b.content)) { await visit(b.content); continue; }
|
|
160
|
+
if (b.type !== 'image' || b.source?.type !== 'base64' || typeof b.source.data !== 'string') continue;
|
|
161
|
+
const fit = await fitImage(b.source);
|
|
162
|
+
if (fit.error) {
|
|
163
|
+
const mark = b.cache_control;
|
|
164
|
+
for (const k of Object.keys(b)) delete b[k];
|
|
165
|
+
Object.assign(b, { type: 'text', text: `[image left out: ${fit.error}]` }, mark ? { cache_control: mark } : {});
|
|
166
|
+
changed++;
|
|
167
|
+
} else if (fit.from) {
|
|
168
|
+
b.source = { type: 'base64', media_type: fit.media_type, data: fit.data };
|
|
169
|
+
changed++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
await visit(blocks);
|
|
174
|
+
return changed;
|
|
175
|
+
}
|