@ziamana/bruine 0.1.2 → 0.1.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 +8 -5
- package/dist/bin.js +10 -5
- package/dist/plugins/mcp.js +2 -1
- package/dist/plugins/modes.js +145 -9
- package/dist/plugins/render.js +123 -20
- package/dist/plugins/repl.js +174 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
[](https://nodejs.org)
|
|
14
|
-
 
|
|
15
|
+
[](CHANGELOG.md)
|
|
16
16
|
|
|
17
17
|
<a href="docs/media/bruine-film.mp4"><img src="docs/media/hero.webp" alt="The bruine film: the word bruine becomes the logo in the rain, then the effort climbs from low to max and the rain turns into a storm" width="100%"></a>
|
|
18
18
|
|
|
@@ -144,7 +144,7 @@ training), and the offer can end without notice, so keep another model in reach.
|
|
|
144
144
|
| `/model` and `/provider` | Switch model mid-session from the server's own live catalogue |
|
|
145
145
|
| Skills | Reuses the skills you already wrote for Claude Code, OpenCode, pi or `~/.agents/skills` |
|
|
146
146
|
| Context and speed | A footer that shows context used, tok/s, prefill and cache hit rate |
|
|
147
|
-
| Works on |
|
|
147
|
+
| Works on | Linux (Konsole, GNOME Terminal), tested every day. Written for macOS Terminal, iTerm2, Windows Terminal and PowerShell too, not yet checked by hand there |
|
|
148
148
|
| herdr | Works with herdr: shows up as `bruine` in `herdr agent list`. |
|
|
149
149
|
|
|
150
150
|
## The weather
|
|
@@ -215,8 +215,11 @@ turn it on in the setup. Network discovery of model servers is opt-in and only t
|
|
|
215
215
|
compatible server) with a context of 32k or more. Small models can chat but tend to lose the thread in
|
|
216
216
|
long agent loops. The setup detects what the server's chat template supports (thinking, effort levels).
|
|
217
217
|
|
|
218
|
-
**Does it work on Windows?**
|
|
219
|
-
|
|
218
|
+
**Does it work on Windows or macOS?** It is written for them (on Windows, commands run in PowerShell) and
|
|
219
|
+
the unit tests run on both in CI, but nobody has used it by hand on a real Windows or macOS machine yet,
|
|
220
|
+
and the terminal tests on Windows still fail. Linux is the one platform used every day. If it breaks on
|
|
221
|
+
yours, [open an issue](https://github.com/ziamana/bruine/issues). [docs/PLATFORMS.md](docs/PLATFORMS.md)
|
|
222
|
+
lists what was checked and what still has limits.
|
|
220
223
|
|
|
221
224
|
**How do I make it ask less?** `/auto` lets a fast model judge the routine actions (risky ones still
|
|
222
225
|
ask), the approval's `a` (Always) remembers one exact command for the session and says which, and
|
package/dist/bin.js
CHANGED
|
@@ -688,8 +688,8 @@ async function writeEnvVar(envPath, key, value) {
|
|
|
688
688
|
text = "";
|
|
689
689
|
}
|
|
690
690
|
const line = `${key}=${value}`;
|
|
691
|
-
const re = new RegExp(`^${key}=.*$`, "m");
|
|
692
|
-
const next = re.test(text) ? text.replace(re, line) : text === "" || text.endsWith("\n") ? `${text}${line}
|
|
691
|
+
const re = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=.*$`, "m");
|
|
692
|
+
const next = re.test(text) ? text.replace(re, () => line) : text === "" || text.endsWith("\n") ? `${text}${line}
|
|
693
693
|
` : `${text}
|
|
694
694
|
${line}
|
|
695
695
|
`;
|
|
@@ -4655,12 +4655,17 @@ function readMcpServers(home, projectDir, env = process.env) {
|
|
|
4655
4655
|
return { servers, problems };
|
|
4656
4656
|
}
|
|
4657
4657
|
function fingerprint(server) {
|
|
4658
|
-
const
|
|
4658
|
+
const may = { alwaysAllow: [...server.alwaysAllow].sort(), readOnly: server.readOnly };
|
|
4659
|
+
const what = server.transport === "stdio" ? { t: "stdio", command: server.command, args: server.args ?? [], env: server.env ?? {}, cwd: server.cwd ?? "", may } : { t: "http", url: server.url, headers: server.headers ?? {}, may };
|
|
4659
4660
|
return createHash("sha256").update(JSON.stringify(what)).digest("hex").slice(0, 16);
|
|
4660
4661
|
}
|
|
4661
4662
|
function describeServer(server) {
|
|
4662
|
-
|
|
4663
|
-
|
|
4663
|
+
const where = server.transport === "stdio" ? [server.command, ...server.args ?? []].join(" ") : server.url ?? "";
|
|
4664
|
+
const may = [
|
|
4665
|
+
...server.readOnly ? ["marked read-only: usable in Plan mode, never asks"] : [],
|
|
4666
|
+
...server.alwaysAllow.length > 0 ? [`runs without asking: ${server.alwaysAllow.join(", ")}`] : []
|
|
4667
|
+
];
|
|
4668
|
+
return may.length === 0 ? where : `${where} (${may.join("; ")})`;
|
|
4664
4669
|
}
|
|
4665
4670
|
var PROJECT_MCP_FILE, isRecord;
|
|
4666
4671
|
var init_config = __esm({
|
package/dist/plugins/mcp.js
CHANGED
|
@@ -151,7 +151,8 @@ function readMcpServers(home, projectDir, env = process.env) {
|
|
|
151
151
|
return { servers, problems };
|
|
152
152
|
}
|
|
153
153
|
function fingerprint(server) {
|
|
154
|
-
const
|
|
154
|
+
const may = { alwaysAllow: [...server.alwaysAllow].sort(), readOnly: server.readOnly };
|
|
155
|
+
const what = server.transport === "stdio" ? { t: "stdio", command: server.command, args: server.args ?? [], env: server.env ?? {}, cwd: server.cwd ?? "", may } : { t: "http", url: server.url, headers: server.headers ?? {}, may };
|
|
155
156
|
return createHash("sha256").update(JSON.stringify(what)).digest("hex").slice(0, 16);
|
|
156
157
|
}
|
|
157
158
|
function toDshConfig(server) {
|
package/dist/plugins/modes.js
CHANGED
|
@@ -26,7 +26,8 @@ function configReadPath(home) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// src/plugins/modes.ts
|
|
29
|
-
import { existsSync as
|
|
29
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
30
|
+
import { join as join2 } from "path";
|
|
30
31
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
31
32
|
import { Text } from "@earendil-works/pi-tui";
|
|
32
33
|
|
|
@@ -190,6 +191,7 @@ var READONLY_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
190
191
|
"alias",
|
|
191
192
|
"ps"
|
|
192
193
|
]);
|
|
194
|
+
var PS_ENVIRONMENT = /^ps\s(?:[^\n]*\s)?(?!-)[A-Za-z]*e[A-Za-z]*(?:\s|$)|^ps\b[^\n]*--environment/;
|
|
193
195
|
var NOT_SIMPLE = /[;&|<>`]|\$\(|\n/;
|
|
194
196
|
var EXECUTABLE_OPTION = /--(?:pre|ext-diff|textconv)(?=$|[=\s'"\\])/;
|
|
195
197
|
var FIND_MUTATING = /\s-(exec|ok|fprint|fls|delete)/;
|
|
@@ -295,7 +297,13 @@ var SENSITIVE_PATTERNS = [
|
|
|
295
297
|
/(^|[/\\])\.bashrc\b|(^|[/\\])\.bash_profile\b/,
|
|
296
298
|
/(^|[/\\])\.profile\b/,
|
|
297
299
|
/(^|[/\\])\.zprofile\b/,
|
|
298
|
-
/(^|[/\\])\.config[/\\]fish[/\\]
|
|
300
|
+
/(^|[/\\])\.config[/\\]fish[/\\]/,
|
|
301
|
+
// Cloud, cluster and registry credentials, and the tool's own config (MCP tokens may sit in it).
|
|
302
|
+
/(^|[/\\])\.(aws|gnupg|kube|azure)([/\\]|$)/,
|
|
303
|
+
/(^|[/\\])\.config[/\\](gh|gcloud|glab-cli)([/\\]|$)/,
|
|
304
|
+
/(^|[/\\])\.docker[/\\]config\.json\b/,
|
|
305
|
+
/(^|[/\\])\.(netrc|git-credentials)\b|(^|[/\\])_netrc\b/,
|
|
306
|
+
/(^|[/\\])(bruine|kumo)\.json\b/
|
|
299
307
|
];
|
|
300
308
|
function parseArgs(raw) {
|
|
301
309
|
if (raw !== null && typeof raw === "object") return Array.isArray(raw) ? {} : raw;
|
|
@@ -382,6 +390,7 @@ function isReadonlyBash(command) {
|
|
|
382
390
|
const words = command.trim().split(/\s+/);
|
|
383
391
|
const first = words[0] ?? "";
|
|
384
392
|
if (first === "find") return !FIND_MUTATING.test(command);
|
|
393
|
+
if (first === "ps") return !PS_ENVIRONMENT.test(command.trim());
|
|
385
394
|
if (first === "git") return gitReadonly(words, command);
|
|
386
395
|
return READONLY_COMMANDS.has(first);
|
|
387
396
|
}
|
|
@@ -463,6 +472,43 @@ function isPathInside(target, projectDir) {
|
|
|
463
472
|
const rel = mod.relative(root, abs);
|
|
464
473
|
return rel !== "" && !rel.startsWith("..") && !mod.isAbsolute(rel) ? true : rel === "";
|
|
465
474
|
}
|
|
475
|
+
var FILE_LOOKUP_TOOLS = /* @__PURE__ */ new Set(["read", "read_image", "glob", "grep"]);
|
|
476
|
+
function lookupTargets(name2, args) {
|
|
477
|
+
const out = [];
|
|
478
|
+
for (const key of ["path", "file_path", "directory"]) if (typeof args[key] === "string") out.push(args[key]);
|
|
479
|
+
if (name2 === "glob" && typeof args.pattern === "string" && /^(?:[/\\~]|[A-Za-z]:)/.test(args.pattern)) out.push(args.pattern);
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
function lookupLeaks(name2, args, projectDir, readRoots = []) {
|
|
483
|
+
for (const raw of lookupTargets(name2, args)) {
|
|
484
|
+
const target = expandHome(raw.trim());
|
|
485
|
+
if (isSensitiveTarget(target, projectDir)) return true;
|
|
486
|
+
if (!isPathInside(target, projectDir) && !readRoots.some((root) => isPathInside(target, root))) return true;
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
function isPrivateUrl(raw) {
|
|
491
|
+
let url;
|
|
492
|
+
try {
|
|
493
|
+
url = new URL(raw.trim());
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
if (url.username !== "" || url.password !== "") return true;
|
|
498
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
499
|
+
if (host === "" || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal") || host.endsWith(".lan") || host.endsWith(".home.arpa")) return true;
|
|
500
|
+
if (!host.includes(".") && !host.includes(":")) return true;
|
|
501
|
+
const v4 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host);
|
|
502
|
+
if (v4 !== null) {
|
|
503
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
504
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a >= 224;
|
|
505
|
+
}
|
|
506
|
+
if (host.includes(":")) {
|
|
507
|
+
if (host === "::" || host === "::1" || /^f[cd]/.test(host) || /^fe[89ab]/.test(host)) return true;
|
|
508
|
+
return host.startsWith("::ffff:");
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
466
512
|
function decide(name2, execArgs, ctx) {
|
|
467
513
|
const isBash = name2 === "bash";
|
|
468
514
|
const isPwsh = name2 === "pwsh";
|
|
@@ -482,11 +528,10 @@ function decide(name2, execArgs, ctx) {
|
|
|
482
528
|
if (WRITE_TOOLS.has(name2)) {
|
|
483
529
|
const target = String(execArgs.path ?? execArgs.file_path ?? "");
|
|
484
530
|
if (isSensitive(target)) return "ask";
|
|
531
|
+
if (!isPathInside(target, ctx.projectDir)) return "ask";
|
|
485
532
|
}
|
|
486
|
-
if (name2
|
|
487
|
-
|
|
488
|
-
if (isSensitiveTarget(target, ctx.projectDir)) return "ask";
|
|
489
|
-
}
|
|
533
|
+
if (FILE_LOOKUP_TOOLS.has(name2) && lookupLeaks(name2, execArgs, ctx.projectDir, ctx.readRoots)) return "ask";
|
|
534
|
+
if (name2 === "web_fetch" && isPrivateUrl(String(execArgs.url ?? ""))) return "ask";
|
|
490
535
|
if (isShell && bashLeaks(command, ctx.projectDir)) return "ask";
|
|
491
536
|
if (ctx.sessionAllowed.has(ruleKey(name2, execArgs))) return "allow";
|
|
492
537
|
if (READ_ONLY_TOOLS.has(name2)) return "allow";
|
|
@@ -516,6 +561,83 @@ import path4 from "path";
|
|
|
516
561
|
// src/plugins/mcp.ts
|
|
517
562
|
var BRUINE_MCP_SERVICE = "bruineMcp";
|
|
518
563
|
|
|
564
|
+
// src/setup/skills.ts
|
|
565
|
+
import { cp, lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "fs/promises";
|
|
566
|
+
import { existsSync as existsSync3 } from "fs";
|
|
567
|
+
import { homedir as homedir3 } from "os";
|
|
568
|
+
import path5, { dirname, join } from "path";
|
|
569
|
+
var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
570
|
+
function parseSkillFrontmatter(md) {
|
|
571
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
572
|
+
if (m === null || m[1] === void 0) return {};
|
|
573
|
+
const lines = m[1].split(/\r?\n/);
|
|
574
|
+
const out = {};
|
|
575
|
+
const unquote = (v) => v.replaceAll(/^["']|["']$/g, "");
|
|
576
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
577
|
+
const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(lines[i].trimEnd());
|
|
578
|
+
if (kv === null || kv[1] === void 0 || kv[2] === void 0) continue;
|
|
579
|
+
const key = kv[1];
|
|
580
|
+
let value = kv[2].trim();
|
|
581
|
+
const rest = [];
|
|
582
|
+
let j = i + 1;
|
|
583
|
+
while (j < lines.length && (lines[j].trim() === "" || /^\s+\S/.test(lines[j]))) {
|
|
584
|
+
rest.push(lines[j]);
|
|
585
|
+
j += 1;
|
|
586
|
+
}
|
|
587
|
+
while (rest.length > 0 && rest[rest.length - 1].trim() === "") rest.pop();
|
|
588
|
+
const block = /^([>|])[+-]?\d*$/.exec(value);
|
|
589
|
+
if (block !== null) {
|
|
590
|
+
const body = rest.map((l) => l.trim());
|
|
591
|
+
value = block[1] === "|" ? body.join("\n") : body.join(" ").replaceAll(/\s+/g, " ").trim();
|
|
592
|
+
i = j - 1;
|
|
593
|
+
} else if (rest.length > 0 && key !== "name") {
|
|
594
|
+
value = [value, ...rest.map((l) => l.trim())].filter((l) => l !== "").join(" ");
|
|
595
|
+
i = j - 1;
|
|
596
|
+
}
|
|
597
|
+
if (key === "name") out.name = unquote(value);
|
|
598
|
+
if (key === "description") out.description = unquote(value);
|
|
599
|
+
}
|
|
600
|
+
return out;
|
|
601
|
+
}
|
|
602
|
+
async function readSkillsUnder(root) {
|
|
603
|
+
const out = [];
|
|
604
|
+
if (!existsSync3(root)) return out;
|
|
605
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
606
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
607
|
+
const skillPath = join(root, entry.name, "SKILL.md");
|
|
608
|
+
if (!existsSync3(skillPath)) continue;
|
|
609
|
+
const fm = parseSkillFrontmatter(await readFile(skillPath, "utf8"));
|
|
610
|
+
if (fm.name === void 0 || !SAFE_NAME.test(fm.name)) continue;
|
|
611
|
+
out.push({ name: fm.name, description: fm.description ?? "", dir: join(root, entry.name) });
|
|
612
|
+
}
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
async function scanProjectSkills(cwd, pathMod = path5) {
|
|
616
|
+
const out = [];
|
|
617
|
+
const seen = /* @__PURE__ */ new Set();
|
|
618
|
+
for (const rel of [
|
|
619
|
+
[".agents", "skills"],
|
|
620
|
+
[".dsh", "skills"]
|
|
621
|
+
]) {
|
|
622
|
+
for (const skill of await readSkillsUnder(pathMod.join(cwd, ...rel))) {
|
|
623
|
+
if (seen.has(skill.name)) continue;
|
|
624
|
+
seen.add(skill.name);
|
|
625
|
+
out.push(skill);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
629
|
+
}
|
|
630
|
+
async function readAvailableSkills(homeSkillsDir, cwd) {
|
|
631
|
+
const byName = /* @__PURE__ */ new Map();
|
|
632
|
+
for (const skill of await readSkillsUnder(homeSkillsDir)) {
|
|
633
|
+
byName.set(skill.name, { ...skill, scope: "user" });
|
|
634
|
+
}
|
|
635
|
+
for (const skill of await scanProjectSkills(cwd)) {
|
|
636
|
+
byName.set(skill.name, { ...skill, scope: "project" });
|
|
637
|
+
}
|
|
638
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
639
|
+
}
|
|
640
|
+
|
|
519
641
|
// src/plugins/modes.ts
|
|
520
642
|
function mcpPolicy(service, tool) {
|
|
521
643
|
const server = service?.serverOf(tool);
|
|
@@ -539,9 +661,9 @@ var PERMISSION_ORDER = ["ask", "auto", "full"];
|
|
|
539
661
|
function readBruineJson(env = process.env) {
|
|
540
662
|
try {
|
|
541
663
|
const home = runtimeHome(env);
|
|
542
|
-
const
|
|
543
|
-
if (!
|
|
544
|
-
return JSON.parse(readFileSync3(
|
|
664
|
+
const path6 = configReadPath(home);
|
|
665
|
+
if (!existsSync4(path6)) return {};
|
|
666
|
+
return JSON.parse(readFileSync3(path6, "utf8"));
|
|
545
667
|
} catch {
|
|
546
668
|
return {};
|
|
547
669
|
}
|
|
@@ -778,6 +900,19 @@ ${line}
|
|
|
778
900
|
`);
|
|
779
901
|
}
|
|
780
902
|
};
|
|
903
|
+
let skillRoots;
|
|
904
|
+
const readRoots = async () => {
|
|
905
|
+
if (skillRoots !== void 0 && Date.now() - skillRoots.at < 3e4) return skillRoots.roots;
|
|
906
|
+
let roots = [];
|
|
907
|
+
try {
|
|
908
|
+
const home = join2(runtimeHome(), "skills");
|
|
909
|
+
roots = [home, ...(await readAvailableSkills(home, process.cwd())).map((skill) => skill.dir)];
|
|
910
|
+
} catch {
|
|
911
|
+
roots = [];
|
|
912
|
+
}
|
|
913
|
+
skillRoots = { at: Date.now(), roots };
|
|
914
|
+
return roots;
|
|
915
|
+
};
|
|
781
916
|
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
782
917
|
if (!modes.governs(exec.agent)) return next();
|
|
783
918
|
const canAsk = repl?.ui !== void 0;
|
|
@@ -788,6 +923,7 @@ ${line}
|
|
|
788
923
|
plan: modes.plan,
|
|
789
924
|
sessionAllowed: modes.sessionAllowed,
|
|
790
925
|
projectDir: process.cwd(),
|
|
926
|
+
readRoots: await readRoots(),
|
|
791
927
|
mcp: (tool) => mcpPolicy(ctx.get(BRUINE_MCP_SERVICE), tool)
|
|
792
928
|
});
|
|
793
929
|
let decision;
|
package/dist/plugins/render.js
CHANGED
|
@@ -826,6 +826,7 @@ var ReasoningComponent = class {
|
|
|
826
826
|
};
|
|
827
827
|
|
|
828
828
|
// src/ui/tool-call-component.ts
|
|
829
|
+
import { Image } from "@earendil-works/pi-tui";
|
|
829
830
|
import stringWidth4 from "string-width";
|
|
830
831
|
import { isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "path";
|
|
831
832
|
|
|
@@ -1092,7 +1093,7 @@ function readableToolSummary(tool, args) {
|
|
|
1092
1093
|
// src/ui/image-preview.ts
|
|
1093
1094
|
import { readFileSync as readFileSync3, statSync } from "fs";
|
|
1094
1095
|
import { createRequire } from "module";
|
|
1095
|
-
import { inflateSync } from "zlib";
|
|
1096
|
+
import { crc32, deflateSync, inflateSync } from "zlib";
|
|
1096
1097
|
var MAX_PREVIEW_BYTES = 16 * 1024 * 1024;
|
|
1097
1098
|
var MAX_PREVIEW_PIXELS = 4e7;
|
|
1098
1099
|
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
@@ -1308,12 +1309,85 @@ function previewImageFile(path7, cols, rows) {
|
|
|
1308
1309
|
return void 0;
|
|
1309
1310
|
}
|
|
1310
1311
|
}
|
|
1312
|
+
function encodePng(img) {
|
|
1313
|
+
const chunk = (type, data) => {
|
|
1314
|
+
const out = Buffer.alloc(12 + data.length);
|
|
1315
|
+
out.writeUInt32BE(data.length, 0);
|
|
1316
|
+
out.write(type, 4, "ascii");
|
|
1317
|
+
Buffer.from(data).copy(out, 8);
|
|
1318
|
+
out.writeUInt32BE(crc32(Buffer.concat([Buffer.from(type, "ascii"), Buffer.from(data)])), 8 + data.length);
|
|
1319
|
+
return out;
|
|
1320
|
+
};
|
|
1321
|
+
const header = Buffer.alloc(13);
|
|
1322
|
+
header.writeUInt32BE(img.width, 0);
|
|
1323
|
+
header.writeUInt32BE(img.height, 4);
|
|
1324
|
+
header[8] = 8;
|
|
1325
|
+
header[9] = 6;
|
|
1326
|
+
const stride = img.width * 4;
|
|
1327
|
+
const raw = Buffer.alloc((stride + 1) * img.height);
|
|
1328
|
+
for (let y = 0; y < img.height; y += 1) Buffer.from(img.data.buffer, img.data.byteOffset + y * stride, stride).copy(raw, y * (stride + 1) + 1);
|
|
1329
|
+
return new Uint8Array(Buffer.concat([Buffer.from(PNG_SIGNATURE), chunk("IHDR", header), chunk("IDAT", deflateSync(raw)), chunk("IEND", new Uint8Array())]));
|
|
1330
|
+
}
|
|
1331
|
+
function shrink(img, max) {
|
|
1332
|
+
const scale = Math.min(1, max / Math.max(img.width, img.height));
|
|
1333
|
+
if (scale >= 1) return img;
|
|
1334
|
+
const width = Math.max(1, Math.round(img.width * scale));
|
|
1335
|
+
const height = Math.max(1, Math.round(img.height * scale));
|
|
1336
|
+
const data = new Uint8Array(width * height * 4);
|
|
1337
|
+
const sx = img.width / width;
|
|
1338
|
+
const sy = img.height / height;
|
|
1339
|
+
for (let y = 0; y < height; y += 1) {
|
|
1340
|
+
for (let x = 0; x < width; x += 1) {
|
|
1341
|
+
const sum = [0, 0, 0, 0];
|
|
1342
|
+
let n = 0;
|
|
1343
|
+
for (let yy = Math.floor(y * sy); yy < Math.min(img.height, Math.ceil((y + 1) * sy)); yy += 1) {
|
|
1344
|
+
for (let xx = Math.floor(x * sx); xx < Math.min(img.width, Math.ceil((x + 1) * sx)); xx += 1) {
|
|
1345
|
+
const o = (yy * img.width + xx) * 4;
|
|
1346
|
+
for (let c2 = 0; c2 < 4; c2 += 1) sum[c2] += img.data[o + c2];
|
|
1347
|
+
n += 1;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
for (let c2 = 0; c2 < 4; c2 += 1) data[(y * width + x) * 4 + c2] = Math.round(sum[c2] / Math.max(1, n));
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return { width, height, data };
|
|
1354
|
+
}
|
|
1355
|
+
var SEND_MAX_BYTES = 2 * 1024 * 1024;
|
|
1356
|
+
var SEND_MAX_SIDE = 1600;
|
|
1357
|
+
function terminalImageFile(path7, protocol) {
|
|
1358
|
+
try {
|
|
1359
|
+
if (statSync(path7).size > MAX_PREVIEW_BYTES) return void 0;
|
|
1360
|
+
const bytes = new Uint8Array(readFileSync3(path7));
|
|
1361
|
+
const mimeType = sniffImage(bytes);
|
|
1362
|
+
const size = imageSize(bytes);
|
|
1363
|
+
if (mimeType === void 0 || size === void 0) return void 0;
|
|
1364
|
+
const description = describeImage(bytes);
|
|
1365
|
+
const tooBig = bytes.length > SEND_MAX_BYTES || Math.max(size.width, size.height) > SEND_MAX_SIDE;
|
|
1366
|
+
if (!tooBig && (protocol === "iterm2" || mimeType === "image/png")) {
|
|
1367
|
+
return { base64: Buffer.from(bytes).toString("base64"), mimeType, widthPx: size.width, heightPx: size.height, description };
|
|
1368
|
+
}
|
|
1369
|
+
const img = decodeImage(bytes);
|
|
1370
|
+
if (img === void 0) return void 0;
|
|
1371
|
+
const sent = shrink(img, SEND_MAX_SIDE);
|
|
1372
|
+
return { base64: Buffer.from(encodePng(sent)).toString("base64"), mimeType: "image/png", widthPx: sent.width, heightPx: sent.height, description };
|
|
1373
|
+
} catch {
|
|
1374
|
+
return void 0;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/ui/term-images.ts
|
|
1379
|
+
import { getCapabilities, setCapabilityOverrides } from "@earendil-works/pi-tui";
|
|
1380
|
+
function imageProtocol() {
|
|
1381
|
+
return getCapabilities().images;
|
|
1382
|
+
}
|
|
1311
1383
|
|
|
1312
1384
|
// src/ui/tool-call-component.ts
|
|
1313
1385
|
var IMAGE_TOOLS = /* @__PURE__ */ new Set(["read_image", "view_image"]);
|
|
1314
1386
|
var IMAGE_FILE = /\.(png|jpe?g|gif|webp)$/i;
|
|
1315
1387
|
var IMAGE_PREVIEW_COLS = 64;
|
|
1316
1388
|
var IMAGE_PREVIEW_ROWS = 16;
|
|
1389
|
+
var IMAGE_PICTURE_COLS = 72;
|
|
1390
|
+
var IMAGE_PICTURE_ROWS = 20;
|
|
1317
1391
|
var SUMMARY_KEYS2 = ["command", "cmd", "path", "file_path", "url", "query", "pattern", "name"];
|
|
1318
1392
|
var WRITE_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "multi_edit", "notebook_edit"]);
|
|
1319
1393
|
var MAX_OUTPUT_LINES2 = 5;
|
|
@@ -1355,6 +1429,8 @@ var ToolCallComponent = class {
|
|
|
1355
1429
|
#diff;
|
|
1356
1430
|
/** The image file a successful image read showed the model. */
|
|
1357
1431
|
#image;
|
|
1432
|
+
/** That image, prepared once for the terminal's graphics protocol (null: it could not be). */
|
|
1433
|
+
#picture;
|
|
1358
1434
|
#readableSummaries;
|
|
1359
1435
|
get active() {
|
|
1360
1436
|
return this.#done === void 0;
|
|
@@ -1548,6 +1624,12 @@ var ToolCallComponent = class {
|
|
|
1548
1624
|
return out;
|
|
1549
1625
|
}
|
|
1550
1626
|
if (this.#image !== void 0) {
|
|
1627
|
+
const picture = this.#pictureLines(width);
|
|
1628
|
+
if (picture !== void 0) {
|
|
1629
|
+
out.push(...picture.lines.map((l) => ` ${l}`));
|
|
1630
|
+
out.push(ansi.gray(clipCells(` ${picture.description}${dur === void 0 ? "" : ` ${this.icons.think === "*" ? "-" : "\xB7"} ${dur}`}`, width)));
|
|
1631
|
+
return out;
|
|
1632
|
+
}
|
|
1551
1633
|
const preview = previewImageFile(this.#image, Math.min(IMAGE_PREVIEW_COLS, Math.max(1, width - 6)), IMAGE_PREVIEW_ROWS);
|
|
1552
1634
|
if (preview !== void 0) {
|
|
1553
1635
|
if (preview.lines !== void 0) out.push(...preview.lines.map((l) => ` ${l}`));
|
|
@@ -1561,6 +1643,26 @@ var ToolCallComponent = class {
|
|
|
1561
1643
|
if (dur !== void 0) out.push(ansi.gray(clipCells(`Took ${dur}`, width)));
|
|
1562
1644
|
return out;
|
|
1563
1645
|
}
|
|
1646
|
+
/**
|
|
1647
|
+
* The image itself, through the terminal's graphics protocol, when it has one: Kitty, Ghostty,
|
|
1648
|
+
* WezTerm, Warp, iTerm2, Konsole. pi-tui places it and keeps its rows; the half-block sketch
|
|
1649
|
+
* stays for every other terminal.
|
|
1650
|
+
*/
|
|
1651
|
+
#pictureLines(width) {
|
|
1652
|
+
const protocol = imageProtocol();
|
|
1653
|
+
if (protocol === null || this.#image === void 0) return void 0;
|
|
1654
|
+
const key = `${protocol}:${this.#image}`;
|
|
1655
|
+
if (this.#picture === void 0 || this.#picture !== null && this.#picture.key !== key) {
|
|
1656
|
+
const file = terminalImageFile(this.#image, protocol);
|
|
1657
|
+
this.#picture = file === void 0 ? null : {
|
|
1658
|
+
key,
|
|
1659
|
+
description: file.description,
|
|
1660
|
+
image: new Image(file.base64, file.mimeType, { fallbackColor: ansi.gray }, { maxWidthCells: IMAGE_PICTURE_COLS, maxHeightCells: IMAGE_PICTURE_ROWS }, { widthPx: file.widthPx, heightPx: file.heightPx })
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
if (this.#picture === null) return void 0;
|
|
1664
|
+
return { lines: this.#picture.image.render(Math.max(3, Math.min(IMAGE_PICTURE_COLS + 2, width - 4))), description: this.#picture.description };
|
|
1665
|
+
}
|
|
1564
1666
|
invalidate() {
|
|
1565
1667
|
}
|
|
1566
1668
|
};
|
|
@@ -2284,7 +2386,8 @@ async function turnChanges(opts) {
|
|
|
2284
2386
|
}
|
|
2285
2387
|
|
|
2286
2388
|
// src/plugins/modes.ts
|
|
2287
|
-
import { existsSync as
|
|
2389
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
2390
|
+
import { join as join4 } from "path";
|
|
2288
2391
|
import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
|
|
2289
2392
|
import { Text as Text2 } from "@earendil-works/pi-tui";
|
|
2290
2393
|
|
|
@@ -2302,6 +2405,12 @@ import path3 from "path";
|
|
|
2302
2405
|
import { mkdirSync, readFileSync as readFileSync5, renameSync, writeFileSync } from "fs";
|
|
2303
2406
|
import path4 from "path";
|
|
2304
2407
|
|
|
2408
|
+
// src/setup/skills.ts
|
|
2409
|
+
import { cp, lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "fs/promises";
|
|
2410
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2411
|
+
import { homedir as homedir3 } from "os";
|
|
2412
|
+
import path5, { dirname, join as join3 } from "path";
|
|
2413
|
+
|
|
2305
2414
|
// src/plugins/modes.ts
|
|
2306
2415
|
var PLAN_ON_TEXT = "Plan mode is on: do not modify files or run modifying commands.";
|
|
2307
2416
|
var PLAN_OFF_TEXT = "Plan mode is off.";
|
|
@@ -2636,22 +2745,16 @@ import {
|
|
|
2636
2745
|
// src/ui/footer.ts
|
|
2637
2746
|
import { truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth5 } from "@earendil-works/pi-tui";
|
|
2638
2747
|
import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2639
|
-
import { homedir as
|
|
2640
|
-
import { isAbsolute as isAbsolute5, join as
|
|
2748
|
+
import { homedir as homedir5 } from "os";
|
|
2749
|
+
import { isAbsolute as isAbsolute5, join as join5 } from "path";
|
|
2641
2750
|
|
|
2642
2751
|
// src/ui/place.ts
|
|
2643
|
-
import { homedir as
|
|
2752
|
+
import { homedir as homedir4 } from "os";
|
|
2644
2753
|
import { posix, win32 } from "path";
|
|
2645
2754
|
|
|
2646
2755
|
// src/ui/jump-latest.ts
|
|
2647
2756
|
import { stripTerminalSequences, visibleWidth as visibleWidth6 } from "@earendil-works/pi-tui";
|
|
2648
2757
|
|
|
2649
|
-
// src/setup/skills.ts
|
|
2650
|
-
import { cp, lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "fs/promises";
|
|
2651
|
-
import { existsSync as existsSync4 } from "fs";
|
|
2652
|
-
import { homedir as homedir5 } from "os";
|
|
2653
|
-
import path5, { dirname, join as join4 } from "path";
|
|
2654
|
-
|
|
2655
2758
|
// src/ui/shell.ts
|
|
2656
2759
|
import { Container as Container2 } from "@earendil-works/pi-tui";
|
|
2657
2760
|
|
|
@@ -2706,7 +2809,7 @@ var LOGO_BESIDE_GAP = " ".repeat(LOGO_GAP);
|
|
|
2706
2809
|
|
|
2707
2810
|
// src/ui/intro.ts
|
|
2708
2811
|
import { existsSync as existsSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
2709
|
-
import { join as
|
|
2812
|
+
import { join as join6 } from "path";
|
|
2710
2813
|
|
|
2711
2814
|
// src/ui/intro-effects.ts
|
|
2712
2815
|
var H = LOGO.length;
|
|
@@ -2736,7 +2839,7 @@ import { matchesKey as matchesKey3, visibleWidth as visibleWidth12, wrapTextWith
|
|
|
2736
2839
|
// src/ui/file-complete.ts
|
|
2737
2840
|
import { CombinedAutocompleteProvider } from "@earendil-works/pi-tui";
|
|
2738
2841
|
import { existsSync as existsSync6, readdirSync } from "fs";
|
|
2739
|
-
import { basename, delimiter as delimiter2, join as
|
|
2842
|
+
import { basename, delimiter as delimiter2, join as join7, relative as relative2 } from "path";
|
|
2740
2843
|
|
|
2741
2844
|
// src/image/clipboard.ts
|
|
2742
2845
|
import { readFileSync as readFileSync10 } from "fs";
|
|
@@ -2760,7 +2863,7 @@ import { visibleWidth as visibleWidth14 } from "@earendil-works/pi-tui";
|
|
|
2760
2863
|
// src/setup/discover.ts
|
|
2761
2864
|
import { readFileSync as readFileSync11 } from "fs";
|
|
2762
2865
|
import { networkInterfaces } from "os";
|
|
2763
|
-
import { join as
|
|
2866
|
+
import { join as join8 } from "path";
|
|
2764
2867
|
import { spawnSync } from "child_process";
|
|
2765
2868
|
import { parse as parseYaml } from "yaml";
|
|
2766
2869
|
|
|
@@ -2795,7 +2898,7 @@ function settingsEntryAnswer(entry) {
|
|
|
2795
2898
|
|
|
2796
2899
|
// src/ui/bruine-ui.ts
|
|
2797
2900
|
import { existsSync as existsSync7, readFileSync as readFileSync12 } from "fs";
|
|
2798
|
-
import { join as
|
|
2901
|
+
import { join as join10 } from "path";
|
|
2799
2902
|
|
|
2800
2903
|
// src/update.ts
|
|
2801
2904
|
import { spawn } from "child_process";
|
|
@@ -2810,7 +2913,7 @@ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
|
2810
2913
|
function readSettingsRoute(dshHome) {
|
|
2811
2914
|
try {
|
|
2812
2915
|
const home = dshHome ?? runtimeHome();
|
|
2813
|
-
const p =
|
|
2916
|
+
const p = join10(home, "settings.yaml");
|
|
2814
2917
|
if (!existsSync7(p)) return void 0;
|
|
2815
2918
|
const doc = parseBruineSettingsYaml(readFileSync12(p, "utf8"));
|
|
2816
2919
|
if (doc === void 0) return void 0;
|
|
@@ -2840,7 +2943,7 @@ function parseBruineSettingsYaml(text) {
|
|
|
2840
2943
|
|
|
2841
2944
|
// src/ui/errors.ts
|
|
2842
2945
|
import { appendFile, mkdir as mkdir4 } from "fs/promises";
|
|
2843
|
-
import { join as
|
|
2946
|
+
import { join as join11 } from "path";
|
|
2844
2947
|
import { truncateToWidth as truncateToWidth7, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
|
|
2845
2948
|
function firstLine(text) {
|
|
2846
2949
|
return (text.split(/\r?\n/)[0] ?? text).trim();
|
|
@@ -2932,14 +3035,14 @@ async function fetchAvailableModels(baseUrl) {
|
|
|
2932
3035
|
}
|
|
2933
3036
|
function bruineLogPath(env = process.env) {
|
|
2934
3037
|
const home = runtimeHome(env);
|
|
2935
|
-
return
|
|
3038
|
+
return join11(home, "logs", "bruine.log");
|
|
2936
3039
|
}
|
|
2937
3040
|
async function appendErrorLog(failure, extra = "") {
|
|
2938
3041
|
const path7 = bruineLogPath();
|
|
2939
3042
|
const line = `${(/* @__PURE__ */ new Date()).toISOString()} [llm-error] code=${String(failure.code ?? "UNKNOWN")}${failure.status === void 0 ? "" : ` status=${String(failure.status)}`} ${String(failure.message ?? "").slice(0, 2e3)}${extra === "" ? "" : ` ${extra}`}
|
|
2940
3043
|
`;
|
|
2941
3044
|
try {
|
|
2942
|
-
await mkdir4(
|
|
3045
|
+
await mkdir4(join11(path7, ".."), { recursive: true });
|
|
2943
3046
|
await appendFile(path7, line, "utf8");
|
|
2944
3047
|
} catch {
|
|
2945
3048
|
}
|
|
@@ -2983,7 +3086,7 @@ function retryLine(event) {
|
|
|
2983
3086
|
|
|
2984
3087
|
// src/plugins/silence.ts
|
|
2985
3088
|
import { existsSync as existsSync8, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
|
|
2986
|
-
import { join as
|
|
3089
|
+
import { join as join12 } from "path";
|
|
2987
3090
|
import { parse as parseYaml3 } from "yaml";
|
|
2988
3091
|
var BRUINE_SILENCE_SERVICE = "bruineSilence";
|
|
2989
3092
|
|
package/dist/plugins/repl.js
CHANGED
|
@@ -942,6 +942,45 @@ import { readFileSync } from "fs";
|
|
|
942
942
|
import { randomUUID } from "crypto";
|
|
943
943
|
import { visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
944
944
|
|
|
945
|
+
// src/ui/term-images.ts
|
|
946
|
+
import { getCapabilities, setCapabilityOverrides } from "@earendil-works/pi-tui";
|
|
947
|
+
function imageProtocolFor(env = process.env) {
|
|
948
|
+
const asked = env.BRUINE_IMAGES?.trim().toLowerCase();
|
|
949
|
+
if (asked === "kitty" || asked === "iterm2") return asked;
|
|
950
|
+
if (asked === "blocks" || asked === "off" || asked === "none" || asked === "0") return null;
|
|
951
|
+
if (env.TMUX !== void 0 || (env.TERM ?? "").startsWith("tmux") || (env.TERM ?? "").startsWith("screen")) return void 0;
|
|
952
|
+
const konsole = Number.parseInt(env.KONSOLE_VERSION ?? "", 10);
|
|
953
|
+
if (Number.isFinite(konsole) && konsole >= 220400) return "iterm2";
|
|
954
|
+
return void 0;
|
|
955
|
+
}
|
|
956
|
+
function applyImageProtocol(env = process.env) {
|
|
957
|
+
const protocol = imageProtocolFor(env);
|
|
958
|
+
if (protocol !== void 0) setCapabilityOverrides({ images: protocol });
|
|
959
|
+
}
|
|
960
|
+
function imageProtocol() {
|
|
961
|
+
return getCapabilities().images;
|
|
962
|
+
}
|
|
963
|
+
function imageRows(lines) {
|
|
964
|
+
const rows = /* @__PURE__ */ new Set();
|
|
965
|
+
lines.forEach((line, at) => {
|
|
966
|
+
if (line.includes("\x1B_G")) {
|
|
967
|
+
const tall = Number.parseInt(/[;,]r=(\d+)/.exec(line)?.[1] ?? "1", 10);
|
|
968
|
+
for (let i = 0; i < Math.max(1, tall); i += 1) rows.add(at + i);
|
|
969
|
+
} else if (line.includes("\x1B]1337;File=")) {
|
|
970
|
+
const up = Number.parseInt(/\x1b\[(\d+)A\x1b\]1337;File=/.exec(line)?.[1] ?? "0", 10);
|
|
971
|
+
for (let i = 0; i <= up; i += 1) rows.add(at - i);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
return rows;
|
|
975
|
+
}
|
|
976
|
+
function splitImageLine(line) {
|
|
977
|
+
const kitty = line.indexOf("\x1B_G");
|
|
978
|
+
const iterm = /(?:\x1b\[\d+A)?\x1b\]1337;File=/.exec(line);
|
|
979
|
+
const at = kitty >= 0 ? kitty : iterm?.index ?? -1;
|
|
980
|
+
if (at < 0) return void 0;
|
|
981
|
+
return { before: line.slice(0, at), sequence: line.slice(at) };
|
|
982
|
+
}
|
|
983
|
+
|
|
945
984
|
// src/ui/logo-motion.ts
|
|
946
985
|
var LOGO = [
|
|
947
986
|
"\u250F\u2513 \u250F\u2501\u2513\u257B \u257B\u257B\u250F\u2513\u257B\u250F\u2501\u2578",
|
|
@@ -1070,9 +1109,11 @@ var WeatherBackdrop = class {
|
|
|
1070
1109
|
}
|
|
1071
1110
|
const ink = storm2 || this.#weather.level >= 0.85 ? STORM_INK : QUIET_INK;
|
|
1072
1111
|
const out = [...lines];
|
|
1112
|
+
const pictures = imageRows(lines);
|
|
1073
1113
|
for (let y = 0; y < height; y += 1) {
|
|
1074
1114
|
const row = from + y;
|
|
1075
1115
|
if (row >= controlsFrom && row < content.length) continue;
|
|
1116
|
+
if (pictures.has(row)) continue;
|
|
1076
1117
|
const line = lines[from + y];
|
|
1077
1118
|
if (hasBackground(line)) continue;
|
|
1078
1119
|
const padded = line + " ".repeat(Math.max(0, width - visibleWidth2(line)));
|
|
@@ -1329,6 +1370,14 @@ var ChatTranscript = class extends Container {
|
|
|
1329
1370
|
}
|
|
1330
1371
|
for (const [i, line] of block.entries()) {
|
|
1331
1372
|
const clean = child.transcriptStyle === "reasoning" ? reasoningStyle(withoutEmoji(line)) : withoutEmoji(line);
|
|
1373
|
+
const picture = splitImageLine(clean);
|
|
1374
|
+
if (picture !== void 0) {
|
|
1375
|
+
const lead = rail === void 0 ? ` ${picture.before}` : `${showRail ? railPaint(rail, railChar, (i + 1) / railSpan) : " "} ${picture.before}`;
|
|
1376
|
+
const row = rail === void 0 ? lead : card(role, lead);
|
|
1377
|
+
const back = rail === void 0 ? 0 : cardCells - visibleWidth3(lead);
|
|
1378
|
+
piece2.push(`${row}${back > 0 ? `\x1B[${String(back)}D` : ""}${picture.sequence}`);
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1332
1381
|
if (rail === void 0) {
|
|
1333
1382
|
piece2.push(truncateToWidth(` ${clean}`, width - 2));
|
|
1334
1383
|
} else {
|
|
@@ -4734,6 +4783,7 @@ function fileLink(text, absolutePath) {
|
|
|
4734
4783
|
}
|
|
4735
4784
|
|
|
4736
4785
|
// src/ui/tool-call-component.ts
|
|
4786
|
+
import { Image } from "@earendil-works/pi-tui";
|
|
4737
4787
|
import stringWidth7 from "string-width";
|
|
4738
4788
|
import { isAbsolute as isAbsolute4, relative as relative2, resolve as resolve2 } from "path";
|
|
4739
4789
|
|
|
@@ -4969,7 +5019,7 @@ function readableToolSummary(tool, args) {
|
|
|
4969
5019
|
// src/ui/image-preview.ts
|
|
4970
5020
|
import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
4971
5021
|
import { createRequire } from "module";
|
|
4972
|
-
import { inflateSync } from "zlib";
|
|
5022
|
+
import { crc32, deflateSync, inflateSync } from "zlib";
|
|
4973
5023
|
var MAX_PREVIEW_BYTES = 16 * 1024 * 1024;
|
|
4974
5024
|
var MAX_PREVIEW_PIXELS = 4e7;
|
|
4975
5025
|
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
@@ -5185,12 +5235,79 @@ function previewImageFile(path8, cols, rows) {
|
|
|
5185
5235
|
return void 0;
|
|
5186
5236
|
}
|
|
5187
5237
|
}
|
|
5238
|
+
function encodePng(img) {
|
|
5239
|
+
const chunk = (type, data) => {
|
|
5240
|
+
const out = Buffer.alloc(12 + data.length);
|
|
5241
|
+
out.writeUInt32BE(data.length, 0);
|
|
5242
|
+
out.write(type, 4, "ascii");
|
|
5243
|
+
Buffer.from(data).copy(out, 8);
|
|
5244
|
+
out.writeUInt32BE(crc32(Buffer.concat([Buffer.from(type, "ascii"), Buffer.from(data)])), 8 + data.length);
|
|
5245
|
+
return out;
|
|
5246
|
+
};
|
|
5247
|
+
const header = Buffer.alloc(13);
|
|
5248
|
+
header.writeUInt32BE(img.width, 0);
|
|
5249
|
+
header.writeUInt32BE(img.height, 4);
|
|
5250
|
+
header[8] = 8;
|
|
5251
|
+
header[9] = 6;
|
|
5252
|
+
const stride = img.width * 4;
|
|
5253
|
+
const raw = Buffer.alloc((stride + 1) * img.height);
|
|
5254
|
+
for (let y = 0; y < img.height; y += 1) Buffer.from(img.data.buffer, img.data.byteOffset + y * stride, stride).copy(raw, y * (stride + 1) + 1);
|
|
5255
|
+
return new Uint8Array(Buffer.concat([Buffer.from(PNG_SIGNATURE), chunk("IHDR", header), chunk("IDAT", deflateSync(raw)), chunk("IEND", new Uint8Array())]));
|
|
5256
|
+
}
|
|
5257
|
+
function shrink(img, max) {
|
|
5258
|
+
const scale = Math.min(1, max / Math.max(img.width, img.height));
|
|
5259
|
+
if (scale >= 1) return img;
|
|
5260
|
+
const width = Math.max(1, Math.round(img.width * scale));
|
|
5261
|
+
const height = Math.max(1, Math.round(img.height * scale));
|
|
5262
|
+
const data = new Uint8Array(width * height * 4);
|
|
5263
|
+
const sx = img.width / width;
|
|
5264
|
+
const sy = img.height / height;
|
|
5265
|
+
for (let y = 0; y < height; y += 1) {
|
|
5266
|
+
for (let x = 0; x < width; x += 1) {
|
|
5267
|
+
const sum = [0, 0, 0, 0];
|
|
5268
|
+
let n = 0;
|
|
5269
|
+
for (let yy = Math.floor(y * sy); yy < Math.min(img.height, Math.ceil((y + 1) * sy)); yy += 1) {
|
|
5270
|
+
for (let xx = Math.floor(x * sx); xx < Math.min(img.width, Math.ceil((x + 1) * sx)); xx += 1) {
|
|
5271
|
+
const o = (yy * img.width + xx) * 4;
|
|
5272
|
+
for (let c2 = 0; c2 < 4; c2 += 1) sum[c2] += img.data[o + c2];
|
|
5273
|
+
n += 1;
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
for (let c2 = 0; c2 < 4; c2 += 1) data[(y * width + x) * 4 + c2] = Math.round(sum[c2] / Math.max(1, n));
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
return { width, height, data };
|
|
5280
|
+
}
|
|
5281
|
+
var SEND_MAX_BYTES = 2 * 1024 * 1024;
|
|
5282
|
+
var SEND_MAX_SIDE = 1600;
|
|
5283
|
+
function terminalImageFile(path8, protocol) {
|
|
5284
|
+
try {
|
|
5285
|
+
if (statSync2(path8).size > MAX_PREVIEW_BYTES) return void 0;
|
|
5286
|
+
const bytes = new Uint8Array(readFileSync7(path8));
|
|
5287
|
+
const mimeType = sniffImage(bytes);
|
|
5288
|
+
const size = imageSize(bytes);
|
|
5289
|
+
if (mimeType === void 0 || size === void 0) return void 0;
|
|
5290
|
+
const description = describeImage(bytes);
|
|
5291
|
+
const tooBig = bytes.length > SEND_MAX_BYTES || Math.max(size.width, size.height) > SEND_MAX_SIDE;
|
|
5292
|
+
if (!tooBig && (protocol === "iterm2" || mimeType === "image/png")) {
|
|
5293
|
+
return { base64: Buffer.from(bytes).toString("base64"), mimeType, widthPx: size.width, heightPx: size.height, description };
|
|
5294
|
+
}
|
|
5295
|
+
const img = decodeImage(bytes);
|
|
5296
|
+
if (img === void 0) return void 0;
|
|
5297
|
+
const sent = shrink(img, SEND_MAX_SIDE);
|
|
5298
|
+
return { base64: Buffer.from(encodePng(sent)).toString("base64"), mimeType: "image/png", widthPx: sent.width, heightPx: sent.height, description };
|
|
5299
|
+
} catch {
|
|
5300
|
+
return void 0;
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5188
5303
|
|
|
5189
5304
|
// src/ui/tool-call-component.ts
|
|
5190
5305
|
var IMAGE_TOOLS = /* @__PURE__ */ new Set(["read_image", "view_image"]);
|
|
5191
5306
|
var IMAGE_FILE = /\.(png|jpe?g|gif|webp)$/i;
|
|
5192
5307
|
var IMAGE_PREVIEW_COLS = 64;
|
|
5193
5308
|
var IMAGE_PREVIEW_ROWS = 16;
|
|
5309
|
+
var IMAGE_PICTURE_COLS = 72;
|
|
5310
|
+
var IMAGE_PICTURE_ROWS = 20;
|
|
5194
5311
|
var SUMMARY_KEYS = ["command", "cmd", "path", "file_path", "url", "query", "pattern", "name"];
|
|
5195
5312
|
var WRITE_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "multi_edit", "notebook_edit"]);
|
|
5196
5313
|
var MAX_OUTPUT_LINES = 5;
|
|
@@ -5232,6 +5349,8 @@ var ToolCallComponent = class {
|
|
|
5232
5349
|
#diff;
|
|
5233
5350
|
/** The image file a successful image read showed the model. */
|
|
5234
5351
|
#image;
|
|
5352
|
+
/** That image, prepared once for the terminal's graphics protocol (null: it could not be). */
|
|
5353
|
+
#picture;
|
|
5235
5354
|
#readableSummaries;
|
|
5236
5355
|
get active() {
|
|
5237
5356
|
return this.#done === void 0;
|
|
@@ -5425,6 +5544,12 @@ var ToolCallComponent = class {
|
|
|
5425
5544
|
return out;
|
|
5426
5545
|
}
|
|
5427
5546
|
if (this.#image !== void 0) {
|
|
5547
|
+
const picture = this.#pictureLines(width);
|
|
5548
|
+
if (picture !== void 0) {
|
|
5549
|
+
out.push(...picture.lines.map((l) => ` ${l}`));
|
|
5550
|
+
out.push(ansi.gray(clipCells(` ${picture.description}${dur === void 0 ? "" : ` ${this.icons.think === "*" ? "-" : "\xB7"} ${dur}`}`, width)));
|
|
5551
|
+
return out;
|
|
5552
|
+
}
|
|
5428
5553
|
const preview = previewImageFile(this.#image, Math.min(IMAGE_PREVIEW_COLS, Math.max(1, width - 6)), IMAGE_PREVIEW_ROWS);
|
|
5429
5554
|
if (preview !== void 0) {
|
|
5430
5555
|
if (preview.lines !== void 0) out.push(...preview.lines.map((l) => ` ${l}`));
|
|
@@ -5438,6 +5563,26 @@ var ToolCallComponent = class {
|
|
|
5438
5563
|
if (dur !== void 0) out.push(ansi.gray(clipCells(`Took ${dur}`, width)));
|
|
5439
5564
|
return out;
|
|
5440
5565
|
}
|
|
5566
|
+
/**
|
|
5567
|
+
* The image itself, through the terminal's graphics protocol, when it has one: Kitty, Ghostty,
|
|
5568
|
+
* WezTerm, Warp, iTerm2, Konsole. pi-tui places it and keeps its rows; the half-block sketch
|
|
5569
|
+
* stays for every other terminal.
|
|
5570
|
+
*/
|
|
5571
|
+
#pictureLines(width) {
|
|
5572
|
+
const protocol = imageProtocol();
|
|
5573
|
+
if (protocol === null || this.#image === void 0) return void 0;
|
|
5574
|
+
const key = `${protocol}:${this.#image}`;
|
|
5575
|
+
if (this.#picture === void 0 || this.#picture !== null && this.#picture.key !== key) {
|
|
5576
|
+
const file = terminalImageFile(this.#image, protocol);
|
|
5577
|
+
this.#picture = file === void 0 ? null : {
|
|
5578
|
+
key,
|
|
5579
|
+
description: file.description,
|
|
5580
|
+
image: new Image(file.base64, file.mimeType, { fallbackColor: ansi.gray }, { maxWidthCells: IMAGE_PICTURE_COLS, maxHeightCells: IMAGE_PICTURE_ROWS }, { widthPx: file.widthPx, heightPx: file.heightPx })
|
|
5581
|
+
};
|
|
5582
|
+
}
|
|
5583
|
+
if (this.#picture === null) return void 0;
|
|
5584
|
+
return { lines: this.#picture.image.render(Math.max(3, Math.min(IMAGE_PICTURE_COLS + 2, width - 4))), description: this.#picture.description };
|
|
5585
|
+
}
|
|
5441
5586
|
invalidate() {
|
|
5442
5587
|
}
|
|
5443
5588
|
};
|
|
@@ -6249,6 +6394,7 @@ var BruineUi = class {
|
|
|
6249
6394
|
if (process.stdout.isTTY === true && process.env.CI !== "1" && appEnv("NO_CLEAR") !== "1") {
|
|
6250
6395
|
this.terminal.clearScreen();
|
|
6251
6396
|
}
|
|
6397
|
+
applyImageProtocol();
|
|
6252
6398
|
this.tui.start();
|
|
6253
6399
|
this.mouse.start();
|
|
6254
6400
|
void this.probeBackdrop();
|
|
@@ -7017,6 +7163,7 @@ var BruineUi = class {
|
|
|
7017
7163
|
|
|
7018
7164
|
// src/plugins/modes.ts
|
|
7019
7165
|
import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
|
|
7166
|
+
import { join as join10 } from "path";
|
|
7020
7167
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
7021
7168
|
import { Text as Text3 } from "@earendil-works/pi-tui";
|
|
7022
7169
|
|
|
@@ -7031,12 +7178,17 @@ import { readFileSync as readFileSync10 } from "fs";
|
|
|
7031
7178
|
import path6 from "path";
|
|
7032
7179
|
var PROJECT_MCP_FILE = ".mcp.json";
|
|
7033
7180
|
function fingerprint(server) {
|
|
7034
|
-
const
|
|
7181
|
+
const may = { alwaysAllow: [...server.alwaysAllow].sort(), readOnly: server.readOnly };
|
|
7182
|
+
const what = server.transport === "stdio" ? { t: "stdio", command: server.command, args: server.args ?? [], env: server.env ?? {}, cwd: server.cwd ?? "", may } : { t: "http", url: server.url, headers: server.headers ?? {}, may };
|
|
7035
7183
|
return createHash("sha256").update(JSON.stringify(what)).digest("hex").slice(0, 16);
|
|
7036
7184
|
}
|
|
7037
7185
|
function describeServer(server) {
|
|
7038
|
-
|
|
7039
|
-
|
|
7186
|
+
const where = server.transport === "stdio" ? [server.command, ...server.args ?? []].join(" ") : server.url ?? "";
|
|
7187
|
+
const may = [
|
|
7188
|
+
...server.readOnly ? ["marked read-only: usable in Plan mode, never asks"] : [],
|
|
7189
|
+
...server.alwaysAllow.length > 0 ? [`runs without asking: ${server.alwaysAllow.join(", ")}`] : []
|
|
7190
|
+
];
|
|
7191
|
+
return may.length === 0 ? where : `${where} (${may.join("; ")})`;
|
|
7040
7192
|
}
|
|
7041
7193
|
|
|
7042
7194
|
// src/mcp/state.ts
|
|
@@ -7383,13 +7535,13 @@ var SPACE_BUNNY_NOTICE = [
|
|
|
7383
7535
|
|
|
7384
7536
|
// src/setup/zen-route.ts
|
|
7385
7537
|
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
7386
|
-
import { join as
|
|
7538
|
+
import { join as join12 } from "path";
|
|
7387
7539
|
import { parseDocument } from "yaml";
|
|
7388
7540
|
|
|
7389
7541
|
// src/setup/simple.ts
|
|
7390
7542
|
import { chmod as chmod2, mkdir as mkdir4, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
|
|
7391
7543
|
import { existsSync as existsSync10 } from "fs";
|
|
7392
|
-
import { dirname as dirname2, join as
|
|
7544
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
7393
7545
|
|
|
7394
7546
|
// src/setup/cloud.ts
|
|
7395
7547
|
var DEEPSEEK_OFFICIAL = {
|
|
@@ -7416,7 +7568,7 @@ async function writeAtomic(path8, content, mode) {
|
|
|
7416
7568
|
// src/setup/zen-route.ts
|
|
7417
7569
|
var ROUTE_PATH = ["llm-pi-ai", "providers", SPACE_BUNNY.routeName];
|
|
7418
7570
|
async function addSpaceBunnyToHome(home) {
|
|
7419
|
-
const file =
|
|
7571
|
+
const file = join12(home, "settings.yaml");
|
|
7420
7572
|
const doc = parseDocument(existsSync11(file) ? readFileSync14(file, "utf8") : "");
|
|
7421
7573
|
if (doc.hasIn([...ROUTE_PATH])) {
|
|
7422
7574
|
await repairSpaceBunnyRoute(home);
|
|
@@ -7427,7 +7579,7 @@ async function addSpaceBunnyToHome(home) {
|
|
|
7427
7579
|
return "added";
|
|
7428
7580
|
}
|
|
7429
7581
|
async function repairSpaceBunnyRoute(home) {
|
|
7430
|
-
const file =
|
|
7582
|
+
const file = join12(home, "settings.yaml");
|
|
7431
7583
|
if (!existsSync11(file)) return false;
|
|
7432
7584
|
let doc;
|
|
7433
7585
|
try {
|
|
@@ -7475,12 +7627,12 @@ async function repairSpaceBunnyRoute(home) {
|
|
|
7475
7627
|
// src/plugins/model.ts
|
|
7476
7628
|
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
7477
7629
|
import { chmod as chmod3, rename as rename6, writeFile as writeFile6 } from "fs/promises";
|
|
7478
|
-
import { join as
|
|
7630
|
+
import { join as join14 } from "path";
|
|
7479
7631
|
import { matchesKey as matchesKey6 } from "@earendil-works/pi-tui";
|
|
7480
7632
|
|
|
7481
7633
|
// src/ui/errors.ts
|
|
7482
7634
|
import { appendFile, mkdir as mkdir5 } from "fs/promises";
|
|
7483
|
-
import { join as
|
|
7635
|
+
import { join as join13 } from "path";
|
|
7484
7636
|
import { truncateToWidth as truncateToWidth7, wrapTextWithAnsi as wrapTextWithAnsi3 } from "@earendil-works/pi-tui";
|
|
7485
7637
|
function formatK2(n) {
|
|
7486
7638
|
const k = n / 1e3;
|
|
@@ -7630,7 +7782,7 @@ function readEnvKeys(dshHome) {
|
|
|
7630
7782
|
const out = /* @__PURE__ */ new Set();
|
|
7631
7783
|
const home = dshHome ?? runtimeHome();
|
|
7632
7784
|
try {
|
|
7633
|
-
const path8 =
|
|
7785
|
+
const path8 = join14(home, ".env");
|
|
7634
7786
|
if (!existsSync12(path8)) return out;
|
|
7635
7787
|
for (const line of readFileSync15(path8, "utf8").split(/\r?\n/)) {
|
|
7636
7788
|
const name4 = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
@@ -8211,7 +8363,7 @@ var QuestionCallComponent = class {
|
|
|
8211
8363
|
|
|
8212
8364
|
// src/plugins/silence.ts
|
|
8213
8365
|
import { existsSync as existsSync13, readFileSync as readFileSync16, statSync as statSync3 } from "fs";
|
|
8214
|
-
import { join as
|
|
8366
|
+
import { join as join15 } from "path";
|
|
8215
8367
|
import { parse as parseYaml3 } from "yaml";
|
|
8216
8368
|
|
|
8217
8369
|
// src/plugins/render.ts
|
|
@@ -8435,7 +8587,7 @@ function answersIn(output) {
|
|
|
8435
8587
|
// src/plugins/config-edit.ts
|
|
8436
8588
|
import { spawn as spawn3 } from "child_process";
|
|
8437
8589
|
import { existsSync as existsSync14, readFileSync as readFileSync18 } from "fs";
|
|
8438
|
-
import { delimiter as delimiter3, join as
|
|
8590
|
+
import { delimiter as delimiter3, join as join16 } from "path";
|
|
8439
8591
|
var TERMINAL_EDITORS = /* @__PURE__ */ new Set([
|
|
8440
8592
|
"vi",
|
|
8441
8593
|
"vim",
|
|
@@ -8462,7 +8614,7 @@ var TERMINAL_EDITORS = /* @__PURE__ */ new Set([
|
|
|
8462
8614
|
]);
|
|
8463
8615
|
var GUI_EDITORS = ["kate", "kwrite", "gedit", "xed", "mousepad", "pluma", "code", "codium", "subl", "zed"];
|
|
8464
8616
|
function configFiles(home, exists2 = existsSync14) {
|
|
8465
|
-
return [
|
|
8617
|
+
return [join16(home, "settings.yaml"), configReadPath(home)].filter((p) => exists2(p));
|
|
8466
8618
|
}
|
|
8467
8619
|
function baseName(command) {
|
|
8468
8620
|
return command.split(/[\\/]/).pop()?.replace(/\.exe$/i, "") ?? command;
|
|
@@ -8476,7 +8628,7 @@ function onPath(command, env, exists2) {
|
|
|
8476
8628
|
if (/[\\/]/.test(command)) return exists2(command);
|
|
8477
8629
|
const dirs = (env.PATH ?? "").split(delimiter3).filter((d) => d !== "");
|
|
8478
8630
|
const names = process.platform === "win32" ? [command, `${command}.exe`, `${command}.cmd`] : [command];
|
|
8479
|
-
return dirs.some((dir) => names.some((name4) => exists2(
|
|
8631
|
+
return dirs.some((dir) => names.some((name4) => exists2(join16(dir, name4))));
|
|
8480
8632
|
}
|
|
8481
8633
|
function editorFromBruineJson(home) {
|
|
8482
8634
|
try {
|
|
@@ -8639,13 +8791,13 @@ async function stopTask(services, agent, sessionId, name4) {
|
|
|
8639
8791
|
|
|
8640
8792
|
// src/plugins/verify.ts
|
|
8641
8793
|
import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
|
|
8642
|
-
import { join as
|
|
8794
|
+
import { join as join17 } from "path";
|
|
8643
8795
|
function verificationPlan(cwd) {
|
|
8644
|
-
const packagePath =
|
|
8796
|
+
const packagePath = join17(cwd, "package.json");
|
|
8645
8797
|
if (existsSync15(packagePath)) {
|
|
8646
8798
|
try {
|
|
8647
8799
|
const doc = JSON.parse(readFileSync19(packagePath, "utf8"));
|
|
8648
|
-
const manager = doc.packageManager?.split("@")[0] ?? (existsSync15(
|
|
8800
|
+
const manager = doc.packageManager?.split("@")[0] ?? (existsSync15(join17(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync15(join17(cwd, "yarn.lock")) ? "yarn" : "npm");
|
|
8649
8801
|
const cli = ["pnpm", "yarn", "bun", "npm"].includes(manager) ? manager : "npm";
|
|
8650
8802
|
const checks = [];
|
|
8651
8803
|
if (typeof doc.scripts?.typecheck === "string") checks.push({ label: "Typecheck", command: `${cli} run typecheck` });
|
|
@@ -8655,8 +8807,8 @@ function verificationPlan(cwd) {
|
|
|
8655
8807
|
return [];
|
|
8656
8808
|
}
|
|
8657
8809
|
}
|
|
8658
|
-
if (existsSync15(
|
|
8659
|
-
if (existsSync15(
|
|
8810
|
+
if (existsSync15(join17(cwd, "Cargo.toml"))) return [{ label: "Tests", command: "cargo test" }];
|
|
8811
|
+
if (existsSync15(join17(cwd, "go.mod"))) return [{ label: "Tests", command: "go test ./..." }];
|
|
8660
8812
|
return [];
|
|
8661
8813
|
}
|
|
8662
8814
|
async function runVerification(cwd, run = (command, dir) => runShell(command, dir)) {
|
|
@@ -8675,7 +8827,7 @@ ${formatShell(check.command, result)}`)
|
|
|
8675
8827
|
}
|
|
8676
8828
|
|
|
8677
8829
|
// src/plugins/repl.ts
|
|
8678
|
-
import { join as
|
|
8830
|
+
import { join as join18 } from "path";
|
|
8679
8831
|
|
|
8680
8832
|
// src/image/attach.ts
|
|
8681
8833
|
function attachFailureNotice(failures) {
|
|
@@ -9056,7 +9208,7 @@ async function runRepl(ctx, exit) {
|
|
|
9056
9208
|
void sendWithImages(text, images);
|
|
9057
9209
|
};
|
|
9058
9210
|
const flush = (session) => sessions.flush(session);
|
|
9059
|
-
const skillsDir =
|
|
9211
|
+
const skillsDir = join18(runtimeHome(), "skills");
|
|
9060
9212
|
let skillNames = [];
|
|
9061
9213
|
const refreshSkillNames = () => {
|
|
9062
9214
|
void availableSkillNames(skillsDir, process.cwd()).then((names) => {
|