@ziamana/bruine 0.1.1 → 0.1.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/README.md +8 -5
- package/dist/bin.js +104 -35
- package/dist/plugins/mcp.js +2 -1
- package/dist/plugins/modes.js +145 -9
- package/dist/plugins/render.js +20 -19
- package/dist/plugins/repl.js +27 -21
- 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
|
`;
|
|
@@ -1180,6 +1180,48 @@ var init_skills = __esm({
|
|
|
1180
1180
|
}
|
|
1181
1181
|
});
|
|
1182
1182
|
|
|
1183
|
+
// src/setup/default-skills.ts
|
|
1184
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1185
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1186
|
+
import { homedir as homedir3 } from "os";
|
|
1187
|
+
import { join as join4 } from "path";
|
|
1188
|
+
function defaultSkills(bundled) {
|
|
1189
|
+
return bundled.map((s) => s.name).filter((name) => !NOT_BY_DEFAULT.includes(name));
|
|
1190
|
+
}
|
|
1191
|
+
async function ensureDefaultSkills(opts) {
|
|
1192
|
+
const homeSkillsDir = join4(opts.dshHome, "skills");
|
|
1193
|
+
let doc = {};
|
|
1194
|
+
try {
|
|
1195
|
+
doc = JSON.parse(await readFile4(configReadPath(opts.dshHome), "utf8"));
|
|
1196
|
+
} catch {
|
|
1197
|
+
doc = {};
|
|
1198
|
+
}
|
|
1199
|
+
const nothing = { installed: [], kept: [] };
|
|
1200
|
+
if (Array.isArray(doc.skills)) return nothing;
|
|
1201
|
+
if (existsSync6(manifestReadPath(homeSkillsDir))) return nothing;
|
|
1202
|
+
const bundled = await readBundledSkills(opts.bundledRoot);
|
|
1203
|
+
if (bundled.length === 0) return nothing;
|
|
1204
|
+
const shipped = new Set(bundled.map((s) => s.name));
|
|
1205
|
+
const home = opts.home ?? homedir3();
|
|
1206
|
+
const mine = (await scanFoundSkills(home)).filter((s) => s.source === "agents" && !shipped.has(s.name));
|
|
1207
|
+
const chosen = [...defaultSkills(bundled), ...mine.map((s) => s.name)];
|
|
1208
|
+
const result = await syncSkills({ homeSkillsDir, bundledRoot: opts.bundledRoot, chosen, home });
|
|
1209
|
+
doc.skills = chosen;
|
|
1210
|
+
await writeAtomic2(configWritePath(opts.dshHome), `${JSON.stringify(doc, null, 2)}
|
|
1211
|
+
`);
|
|
1212
|
+
return { installed: result.installed, kept: mine.map((s) => s.name).filter((n) => result.installed.includes(n)) };
|
|
1213
|
+
}
|
|
1214
|
+
var NOT_BY_DEFAULT;
|
|
1215
|
+
var init_default_skills = __esm({
|
|
1216
|
+
"src/setup/default-skills.ts"() {
|
|
1217
|
+
"use strict";
|
|
1218
|
+
init_compat();
|
|
1219
|
+
init_simple();
|
|
1220
|
+
init_skills();
|
|
1221
|
+
NOT_BY_DEFAULT = ["remotion", "remotion-best-practices"];
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1183
1225
|
// src/render/chars.ts
|
|
1184
1226
|
function iconsFor(env = process.env, platform = process.platform) {
|
|
1185
1227
|
if (appEnv("ASCII", env) === "1") return ASCII_ICONS;
|
|
@@ -1518,7 +1560,7 @@ var init_theme = __esm({
|
|
|
1518
1560
|
// src/update.ts
|
|
1519
1561
|
import { spawn } from "child_process";
|
|
1520
1562
|
import { realpathSync } from "fs";
|
|
1521
|
-
import { mkdir as mkdir4, readFile as
|
|
1563
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
1522
1564
|
import path5 from "path";
|
|
1523
1565
|
function updateCachePath(dshHome, pathMod = path5) {
|
|
1524
1566
|
return pathMod.join(dshHome, UPDATE_CACHE_FILE);
|
|
@@ -1533,7 +1575,7 @@ function updateCheckEnabled(opts) {
|
|
|
1533
1575
|
async function readUpdateCache(dshHome, pathMod = path5) {
|
|
1534
1576
|
try {
|
|
1535
1577
|
const doc = JSON.parse(
|
|
1536
|
-
await
|
|
1578
|
+
await readFile5(updateCachePath(dshHome, pathMod), "utf8")
|
|
1537
1579
|
);
|
|
1538
1580
|
if (typeof doc.checkedAt !== "number" || typeof doc.latest !== "string") return void 0;
|
|
1539
1581
|
return { checkedAt: doc.checkedAt, latest: doc.latest };
|
|
@@ -1552,7 +1594,7 @@ async function writeUpdateCache(dshHome, cache, pathMod) {
|
|
|
1552
1594
|
async function readBruineJsonDoc(dshHome) {
|
|
1553
1595
|
try {
|
|
1554
1596
|
return JSON.parse(
|
|
1555
|
-
await
|
|
1597
|
+
await readFile5(configReadPath(dshHome), "utf8")
|
|
1556
1598
|
);
|
|
1557
1599
|
} catch {
|
|
1558
1600
|
return {};
|
|
@@ -1761,8 +1803,8 @@ var init_keys = __esm({
|
|
|
1761
1803
|
});
|
|
1762
1804
|
|
|
1763
1805
|
// src/setup/flow.ts
|
|
1764
|
-
import { chmod as chmod2, mkdir as mkdir5, readFile as
|
|
1765
|
-
import { dirname as dirname3, join as
|
|
1806
|
+
import { chmod as chmod2, mkdir as mkdir5, readFile as readFile6, rename as rename5, writeFile as writeFile5 } from "fs/promises";
|
|
1807
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1766
1808
|
function roleFromModelRef(ref) {
|
|
1767
1809
|
if (ref.provider === "deepseek-official") return { cloud: "deepseek-official", model: ref.model };
|
|
1768
1810
|
if (typeof ref.baseUrl === "string" && ref.baseUrl !== "") {
|
|
@@ -1811,10 +1853,10 @@ function usesLocalServer(answers) {
|
|
|
1811
1853
|
}
|
|
1812
1854
|
async function commitPlan(dshHome, plan) {
|
|
1813
1855
|
if (plan.env.some(([name]) => name === void 0)) throw new Error("bad env entry");
|
|
1814
|
-
await writeAtom(
|
|
1856
|
+
await writeAtom(join6(dshHome, "settings.yaml"), plan.settingsYaml, 384);
|
|
1815
1857
|
let bruineJson = plan.bruineJson;
|
|
1816
1858
|
try {
|
|
1817
|
-
const previous = JSON.parse(await
|
|
1859
|
+
const previous = JSON.parse(await readFile6(configReadPath(dshHome), "utf8"));
|
|
1818
1860
|
if (previous.reasoningEffort !== void 0) {
|
|
1819
1861
|
const doc = JSON.parse(bruineJson);
|
|
1820
1862
|
doc.reasoningEffort = previous.reasoningEffort;
|
|
@@ -1825,7 +1867,7 @@ async function commitPlan(dshHome, plan) {
|
|
|
1825
1867
|
}
|
|
1826
1868
|
await writeAtom(configWritePath(dshHome), bruineJson, 384);
|
|
1827
1869
|
for (const [name, value] of plan.env) {
|
|
1828
|
-
await writeEnvVar(
|
|
1870
|
+
await writeEnvVar(join6(dshHome, ".env"), name, value);
|
|
1829
1871
|
}
|
|
1830
1872
|
await syncSkills(plan.skills);
|
|
1831
1873
|
}
|
|
@@ -2141,7 +2183,7 @@ var init_flow = __esm({
|
|
|
2141
2183
|
`,
|
|
2142
2184
|
env,
|
|
2143
2185
|
skills: {
|
|
2144
|
-
homeSkillsDir:
|
|
2186
|
+
homeSkillsDir: join6(opts.dshHome, "skills"),
|
|
2145
2187
|
bundledRoot: opts.bundledSkillsRoot,
|
|
2146
2188
|
chosen: a.skills
|
|
2147
2189
|
}
|
|
@@ -3103,10 +3145,10 @@ import {
|
|
|
3103
3145
|
isKeyRelease as isKeyRelease4,
|
|
3104
3146
|
matchesKey as matchesKey4
|
|
3105
3147
|
} from "@earendil-works/pi-tui";
|
|
3106
|
-
import { existsSync as
|
|
3148
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
3107
3149
|
import { mkdir as mkdir6, rename as rename6, writeFile as writeFile6 } from "fs/promises";
|
|
3108
|
-
import { homedir as
|
|
3109
|
-
import { dirname as dirname4, join as
|
|
3150
|
+
import { homedir as homedir4 } from "os";
|
|
3151
|
+
import { dirname as dirname4, join as join7, resolve } from "path";
|
|
3110
3152
|
import { fileURLToPath } from "url";
|
|
3111
3153
|
import { parse as parseYaml3 } from "yaml";
|
|
3112
3154
|
async function setSuggestionsChoice(dshHome, value) {
|
|
@@ -3123,7 +3165,7 @@ function bundledSkillsRoot() {
|
|
|
3123
3165
|
const dir = dirname4(fileURLToPath(import.meta.url));
|
|
3124
3166
|
for (const up of [1, 2, 3]) {
|
|
3125
3167
|
const candidate = resolve(dir, "..".repeat(up), "skills");
|
|
3126
|
-
if (
|
|
3168
|
+
if (existsSync7(join7(candidate, "git-workflow", "SKILL.md"))) return candidate;
|
|
3127
3169
|
}
|
|
3128
3170
|
return resolve(dir, "..", "skills");
|
|
3129
3171
|
}
|
|
@@ -3144,7 +3186,7 @@ function isExistingInstall(prefill) {
|
|
|
3144
3186
|
function readEnvFile(dshHome) {
|
|
3145
3187
|
const out = {};
|
|
3146
3188
|
try {
|
|
3147
|
-
for (const line of readFileSync4(
|
|
3189
|
+
for (const line of readFileSync4(join7(dshHome, ".env"), "utf8").split(/\r?\n/)) {
|
|
3148
3190
|
const m = /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
3149
3191
|
if (m !== null && m[1] !== void 0 && m[2] !== void 0) out[m[1]] = m[2];
|
|
3150
3192
|
}
|
|
@@ -3161,7 +3203,7 @@ function loadPrefill(dshHome) {
|
|
|
3161
3203
|
}
|
|
3162
3204
|
let settingsDoc;
|
|
3163
3205
|
try {
|
|
3164
|
-
settingsDoc = parseSettingsYaml(readFileSync4(
|
|
3206
|
+
settingsDoc = parseSettingsYaml(readFileSync4(join7(dshHome, "settings.yaml"), "utf8"));
|
|
3165
3207
|
} catch {
|
|
3166
3208
|
settingsDoc = void 0;
|
|
3167
3209
|
}
|
|
@@ -3379,7 +3421,7 @@ function savedSkillsList(dshHome) {
|
|
|
3379
3421
|
}
|
|
3380
3422
|
function installedSkillsList(dshHome) {
|
|
3381
3423
|
try {
|
|
3382
|
-
const raw = JSON.parse(readFileSync4(manifestReadPath(
|
|
3424
|
+
const raw = JSON.parse(readFileSync4(manifestReadPath(join7(dshHome, "skills")), "utf8"));
|
|
3383
3425
|
const names = Object.keys(raw).filter((n) => n !== "" && typeof raw[n] !== "undefined");
|
|
3384
3426
|
return names.length > 0 ? names.sort((a, b) => a.localeCompare(b)) : [];
|
|
3385
3427
|
} catch {
|
|
@@ -3392,8 +3434,10 @@ function initialSkillChecks(items, saved, found) {
|
|
|
3392
3434
|
if (item.disabled === true) return;
|
|
3393
3435
|
if (saved !== void 0) {
|
|
3394
3436
|
if (saved.includes(item.value)) checked.add(i);
|
|
3395
|
-
} else if (
|
|
3396
|
-
|
|
3437
|
+
} else if (!NOT_BY_DEFAULT.includes(item.value)) {
|
|
3438
|
+
const foundRow = found.find((f) => f.name === item.value);
|
|
3439
|
+
const shipped = !item.value.startsWith("#") && foundRow === void 0;
|
|
3440
|
+
if (shipped || RECOMMENDED_SKILLS.includes(item.value) || foundRow?.source === "agents") checked.add(i);
|
|
3397
3441
|
}
|
|
3398
3442
|
});
|
|
3399
3443
|
return checked;
|
|
@@ -3588,6 +3632,9 @@ async function runFullSetup(dshHome, opts = {}) {
|
|
|
3588
3632
|
async function drive() {
|
|
3589
3633
|
const bundledRoot = opts.bundledSkillsRoot ?? bundledSkillsRoot();
|
|
3590
3634
|
const bundled = await readBundledSkills(bundledRoot);
|
|
3635
|
+
if (flow.answers.skills.length === 0 && savedSkillsList(dshHome) === void 0 && installedSkillsList(dshHome) === void 0) {
|
|
3636
|
+
flow.answers.skills = defaultSkills(bundled);
|
|
3637
|
+
}
|
|
3591
3638
|
const showWelcome = async () => {
|
|
3592
3639
|
keyHelp = "Enter continue \xB7 Ctrl+C quit";
|
|
3593
3640
|
status = "Bruine setup";
|
|
@@ -4208,7 +4255,7 @@ async function runFullSetup(dshHome, opts = {}) {
|
|
|
4208
4255
|
}
|
|
4209
4256
|
async function stepSkills(bundled) {
|
|
4210
4257
|
displayStep = "skills";
|
|
4211
|
-
const allFound = await scanFoundSkills(
|
|
4258
|
+
const allFound = await scanFoundSkills(homedir4());
|
|
4212
4259
|
const shippedNames = new Set(bundled.map((s) => s.name));
|
|
4213
4260
|
const found = allFound.filter((s) => !shippedNames.has(s.name));
|
|
4214
4261
|
const project2 = await scanProjectSkills(process.cwd());
|
|
@@ -4232,7 +4279,7 @@ async function runFullSetup(dshHome, opts = {}) {
|
|
|
4232
4279
|
const saved = skillsSubmitted ? flow.answers.skills : savedSkillsList(dshHome);
|
|
4233
4280
|
const checked = initialSkillChecks(items, saved, found);
|
|
4234
4281
|
const sel = await checkStep(
|
|
4235
|
-
saved !== void 0 && saved.length > 0 ? `Skills: Space toggles, Enter continues, s skips (keeps ${String(saved.length)} enabled)` : "Skills: Space toggles, Enter continues, s skips (
|
|
4282
|
+
saved !== void 0 && saved.length > 0 ? `Skills: Space toggles, Enter continues, s skips (keeps ${String(saved.length)} enabled)` : "Skills: Space toggles, Enter continues, s skips (keeps the recommended ones)",
|
|
4236
4283
|
items,
|
|
4237
4284
|
checked
|
|
4238
4285
|
);
|
|
@@ -4457,6 +4504,7 @@ var init_full = __esm({
|
|
|
4457
4504
|
init_cloud();
|
|
4458
4505
|
init_spacebunny();
|
|
4459
4506
|
init_discover();
|
|
4507
|
+
init_default_skills();
|
|
4460
4508
|
init_skills();
|
|
4461
4509
|
init_simple();
|
|
4462
4510
|
init_update();
|
|
@@ -4607,12 +4655,17 @@ function readMcpServers(home, projectDir, env = process.env) {
|
|
|
4607
4655
|
return { servers, problems };
|
|
4608
4656
|
}
|
|
4609
4657
|
function fingerprint(server) {
|
|
4610
|
-
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 };
|
|
4611
4660
|
return createHash("sha256").update(JSON.stringify(what)).digest("hex").slice(0, 16);
|
|
4612
4661
|
}
|
|
4613
4662
|
function describeServer(server) {
|
|
4614
|
-
|
|
4615
|
-
|
|
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("; ")})`;
|
|
4616
4669
|
}
|
|
4617
4670
|
var PROJECT_MCP_FILE, isRecord;
|
|
4618
4671
|
var init_config = __esm({
|
|
@@ -4730,10 +4783,10 @@ var init_approve = __esm({
|
|
|
4730
4783
|
// src/bin.ts
|
|
4731
4784
|
init_compat();
|
|
4732
4785
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
|
|
4733
|
-
import { existsSync as
|
|
4786
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "fs";
|
|
4734
4787
|
import { createInterface } from "readline";
|
|
4735
4788
|
import os from "os";
|
|
4736
|
-
import { dirname as dirname5, join as
|
|
4789
|
+
import { dirname as dirname5, join as join8, resolve as resolve2 } from "path";
|
|
4737
4790
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4738
4791
|
import { createRequire as createRequire3 } from "module";
|
|
4739
4792
|
|
|
@@ -4962,6 +5015,7 @@ function resolveDshBaseDir() {
|
|
|
4962
5015
|
// src/bin.ts
|
|
4963
5016
|
init_simple();
|
|
4964
5017
|
init_skills();
|
|
5018
|
+
init_default_skills();
|
|
4965
5019
|
init_discover();
|
|
4966
5020
|
init_theme();
|
|
4967
5021
|
|
|
@@ -5075,15 +5129,15 @@ var DSH_MISSING = "bruine: could not launch dsh (missing or broken install). Rei
|
|
|
5075
5129
|
function selfPackageRoot() {
|
|
5076
5130
|
try {
|
|
5077
5131
|
const root = resolve2(dirname5(fileURLToPath2(import.meta.url)), "..");
|
|
5078
|
-
const manifest = JSON.parse(readFileSync7(
|
|
5132
|
+
const manifest = JSON.parse(readFileSync7(join8(root, "package.json"), "utf8"));
|
|
5079
5133
|
return manifest.name === pkg2.name ? root : void 0;
|
|
5080
5134
|
} catch {
|
|
5081
5135
|
return void 0;
|
|
5082
5136
|
}
|
|
5083
5137
|
}
|
|
5084
5138
|
function ensureBundleInstalled(profileDir, dshEntry, env, headless = false) {
|
|
5085
|
-
const marker =
|
|
5086
|
-
if (
|
|
5139
|
+
const marker = join8(profileDir, "node_modules", pkg2.name, "package.json");
|
|
5140
|
+
if (existsSync8(marker)) return;
|
|
5087
5141
|
const root = selfPackageRoot();
|
|
5088
5142
|
if (!headless) console.log("Setting up bruine (one time)\u2026");
|
|
5089
5143
|
const dshBase = resolveDshBaseDir();
|
|
@@ -5120,12 +5174,12 @@ function readBruineJson(dshHome) {
|
|
|
5120
5174
|
}
|
|
5121
5175
|
async function printSkills(dshHome) {
|
|
5122
5176
|
const { readInstalledSkills: readInstalledSkills2 } = await Promise.resolve().then(() => (init_skills(), skills_exports));
|
|
5123
|
-
const skills = await readInstalledSkills2(
|
|
5177
|
+
const skills = await readInstalledSkills2(join8(dshHome, "skills"));
|
|
5124
5178
|
if (skills.length === 0) {
|
|
5125
5179
|
console.log("No skills enabled. Run `bruine setup` to pick skills.");
|
|
5126
5180
|
return;
|
|
5127
5181
|
}
|
|
5128
|
-
console.log(`Skills enabled in ${
|
|
5182
|
+
console.log(`Skills enabled in ${join8(dshHome, "skills")}:`);
|
|
5129
5183
|
for (const s of skills) {
|
|
5130
5184
|
const kind = s.copied === true ? "linked-copy" : s.kind;
|
|
5131
5185
|
console.log(` ${s.name} ${kind}${s.source !== "" ? ` ${s.source}` : ""}`);
|
|
@@ -5169,7 +5223,7 @@ async function runUpdate() {
|
|
|
5169
5223
|
let version = "unknown";
|
|
5170
5224
|
try {
|
|
5171
5225
|
const manifest = JSON.parse(
|
|
5172
|
-
readFileSync7(
|
|
5226
|
+
readFileSync7(join8(dirname5(real), "..", "package.json"), "utf8")
|
|
5173
5227
|
);
|
|
5174
5228
|
version = manifest.version ?? "unknown";
|
|
5175
5229
|
} catch {
|
|
@@ -5303,7 +5357,7 @@ async function main() {
|
|
|
5303
5357
|
console.error("bruine: --continue cannot be combined with -p");
|
|
5304
5358
|
process.exit(2);
|
|
5305
5359
|
}
|
|
5306
|
-
if (!
|
|
5360
|
+
if (!existsSync8(join8(dshHome, "settings.yaml"))) {
|
|
5307
5361
|
if (headless) {
|
|
5308
5362
|
console.error("bruine: run `bruine setup` before using -p");
|
|
5309
5363
|
process.exit(2);
|
|
@@ -5337,7 +5391,22 @@ async function main() {
|
|
|
5337
5391
|
});
|
|
5338
5392
|
const { dir } = await ensureProfile(dshHome);
|
|
5339
5393
|
ensureBundleInstalled(dir, dshEntry, env, headless);
|
|
5340
|
-
const
|
|
5394
|
+
const defaults = await ensureDefaultSkills({
|
|
5395
|
+
dshHome,
|
|
5396
|
+
bundledRoot: resolve2(dirname5(fileURLToPath2(import.meta.url)), "..", "skills")
|
|
5397
|
+
});
|
|
5398
|
+
const shipped = defaults.installed.filter((name) => !defaults.kept.includes(name));
|
|
5399
|
+
const sayDefault = (said) => {
|
|
5400
|
+
const line = process.stdout.isTTY === true ? ansi.dim(said) : said;
|
|
5401
|
+
if (headless) console.error(line);
|
|
5402
|
+
else console.log(line);
|
|
5403
|
+
};
|
|
5404
|
+
if (shipped.length > 0) sayDefault(`bruine: ${String(shipped.length)} skills are on (turn any off with bruine setup)`);
|
|
5405
|
+
if (defaults.kept.length > 0) {
|
|
5406
|
+
const k = String(defaults.kept.length);
|
|
5407
|
+
sayDefault(`bruine: kept your ${k} skill${defaults.kept.length === 1 ? "" : "s"} from .agents/skills (manage them with bruine setup)`);
|
|
5408
|
+
}
|
|
5409
|
+
const migrated = await migrateAgentsSkills({ homeSkillsDir: join8(dshHome, "skills") });
|
|
5341
5410
|
if (migrated.linked.length > 0) {
|
|
5342
5411
|
const n = String(migrated.linked.length);
|
|
5343
5412
|
const kept = `bruine: kept your ${n} skill${migrated.linked.length === 1 ? "" : "s"} from .agents/skills (manage them with bruine setup)`;
|
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
|
@@ -2284,7 +2284,8 @@ async function turnChanges(opts) {
|
|
|
2284
2284
|
}
|
|
2285
2285
|
|
|
2286
2286
|
// src/plugins/modes.ts
|
|
2287
|
-
import { existsSync as
|
|
2287
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
2288
|
+
import { join as join4 } from "path";
|
|
2288
2289
|
import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
|
|
2289
2290
|
import { Text as Text2 } from "@earendil-works/pi-tui";
|
|
2290
2291
|
|
|
@@ -2302,6 +2303,12 @@ import path3 from "path";
|
|
|
2302
2303
|
import { mkdirSync, readFileSync as readFileSync5, renameSync, writeFileSync } from "fs";
|
|
2303
2304
|
import path4 from "path";
|
|
2304
2305
|
|
|
2306
|
+
// src/setup/skills.ts
|
|
2307
|
+
import { cp, lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "fs/promises";
|
|
2308
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2309
|
+
import { homedir as homedir3 } from "os";
|
|
2310
|
+
import path5, { dirname, join as join3 } from "path";
|
|
2311
|
+
|
|
2305
2312
|
// src/plugins/modes.ts
|
|
2306
2313
|
var PLAN_ON_TEXT = "Plan mode is on: do not modify files or run modifying commands.";
|
|
2307
2314
|
var PLAN_OFF_TEXT = "Plan mode is off.";
|
|
@@ -2636,22 +2643,16 @@ import {
|
|
|
2636
2643
|
// src/ui/footer.ts
|
|
2637
2644
|
import { truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth5 } from "@earendil-works/pi-tui";
|
|
2638
2645
|
import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2639
|
-
import { homedir as
|
|
2640
|
-
import { isAbsolute as isAbsolute5, join as
|
|
2646
|
+
import { homedir as homedir5 } from "os";
|
|
2647
|
+
import { isAbsolute as isAbsolute5, join as join5 } from "path";
|
|
2641
2648
|
|
|
2642
2649
|
// src/ui/place.ts
|
|
2643
|
-
import { homedir as
|
|
2650
|
+
import { homedir as homedir4 } from "os";
|
|
2644
2651
|
import { posix, win32 } from "path";
|
|
2645
2652
|
|
|
2646
2653
|
// src/ui/jump-latest.ts
|
|
2647
2654
|
import { stripTerminalSequences, visibleWidth as visibleWidth6 } from "@earendil-works/pi-tui";
|
|
2648
2655
|
|
|
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
2656
|
// src/ui/shell.ts
|
|
2656
2657
|
import { Container as Container2 } from "@earendil-works/pi-tui";
|
|
2657
2658
|
|
|
@@ -2706,7 +2707,7 @@ var LOGO_BESIDE_GAP = " ".repeat(LOGO_GAP);
|
|
|
2706
2707
|
|
|
2707
2708
|
// src/ui/intro.ts
|
|
2708
2709
|
import { existsSync as existsSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
2709
|
-
import { join as
|
|
2710
|
+
import { join as join6 } from "path";
|
|
2710
2711
|
|
|
2711
2712
|
// src/ui/intro-effects.ts
|
|
2712
2713
|
var H = LOGO.length;
|
|
@@ -2736,7 +2737,7 @@ import { matchesKey as matchesKey3, visibleWidth as visibleWidth12, wrapTextWith
|
|
|
2736
2737
|
// src/ui/file-complete.ts
|
|
2737
2738
|
import { CombinedAutocompleteProvider } from "@earendil-works/pi-tui";
|
|
2738
2739
|
import { existsSync as existsSync6, readdirSync } from "fs";
|
|
2739
|
-
import { basename, delimiter as delimiter2, join as
|
|
2740
|
+
import { basename, delimiter as delimiter2, join as join7, relative as relative2 } from "path";
|
|
2740
2741
|
|
|
2741
2742
|
// src/image/clipboard.ts
|
|
2742
2743
|
import { readFileSync as readFileSync10 } from "fs";
|
|
@@ -2760,7 +2761,7 @@ import { visibleWidth as visibleWidth14 } from "@earendil-works/pi-tui";
|
|
|
2760
2761
|
// src/setup/discover.ts
|
|
2761
2762
|
import { readFileSync as readFileSync11 } from "fs";
|
|
2762
2763
|
import { networkInterfaces } from "os";
|
|
2763
|
-
import { join as
|
|
2764
|
+
import { join as join8 } from "path";
|
|
2764
2765
|
import { spawnSync } from "child_process";
|
|
2765
2766
|
import { parse as parseYaml } from "yaml";
|
|
2766
2767
|
|
|
@@ -2795,7 +2796,7 @@ function settingsEntryAnswer(entry) {
|
|
|
2795
2796
|
|
|
2796
2797
|
// src/ui/bruine-ui.ts
|
|
2797
2798
|
import { existsSync as existsSync7, readFileSync as readFileSync12 } from "fs";
|
|
2798
|
-
import { join as
|
|
2799
|
+
import { join as join10 } from "path";
|
|
2799
2800
|
|
|
2800
2801
|
// src/update.ts
|
|
2801
2802
|
import { spawn } from "child_process";
|
|
@@ -2810,7 +2811,7 @@ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
|
2810
2811
|
function readSettingsRoute(dshHome) {
|
|
2811
2812
|
try {
|
|
2812
2813
|
const home = dshHome ?? runtimeHome();
|
|
2813
|
-
const p =
|
|
2814
|
+
const p = join10(home, "settings.yaml");
|
|
2814
2815
|
if (!existsSync7(p)) return void 0;
|
|
2815
2816
|
const doc = parseBruineSettingsYaml(readFileSync12(p, "utf8"));
|
|
2816
2817
|
if (doc === void 0) return void 0;
|
|
@@ -2840,7 +2841,7 @@ function parseBruineSettingsYaml(text) {
|
|
|
2840
2841
|
|
|
2841
2842
|
// src/ui/errors.ts
|
|
2842
2843
|
import { appendFile, mkdir as mkdir4 } from "fs/promises";
|
|
2843
|
-
import { join as
|
|
2844
|
+
import { join as join11 } from "path";
|
|
2844
2845
|
import { truncateToWidth as truncateToWidth7, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
|
|
2845
2846
|
function firstLine(text) {
|
|
2846
2847
|
return (text.split(/\r?\n/)[0] ?? text).trim();
|
|
@@ -2932,14 +2933,14 @@ async function fetchAvailableModels(baseUrl) {
|
|
|
2932
2933
|
}
|
|
2933
2934
|
function bruineLogPath(env = process.env) {
|
|
2934
2935
|
const home = runtimeHome(env);
|
|
2935
|
-
return
|
|
2936
|
+
return join11(home, "logs", "bruine.log");
|
|
2936
2937
|
}
|
|
2937
2938
|
async function appendErrorLog(failure, extra = "") {
|
|
2938
2939
|
const path7 = bruineLogPath();
|
|
2939
2940
|
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
2941
|
`;
|
|
2941
2942
|
try {
|
|
2942
|
-
await mkdir4(
|
|
2943
|
+
await mkdir4(join11(path7, ".."), { recursive: true });
|
|
2943
2944
|
await appendFile(path7, line, "utf8");
|
|
2944
2945
|
} catch {
|
|
2945
2946
|
}
|
|
@@ -2983,7 +2984,7 @@ function retryLine(event) {
|
|
|
2983
2984
|
|
|
2984
2985
|
// src/plugins/silence.ts
|
|
2985
2986
|
import { existsSync as existsSync8, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
|
|
2986
|
-
import { join as
|
|
2987
|
+
import { join as join12 } from "path";
|
|
2987
2988
|
import { parse as parseYaml3 } from "yaml";
|
|
2988
2989
|
var BRUINE_SILENCE_SERVICE = "bruineSilence";
|
|
2989
2990
|
|
package/dist/plugins/repl.js
CHANGED
|
@@ -7017,6 +7017,7 @@ var BruineUi = class {
|
|
|
7017
7017
|
|
|
7018
7018
|
// src/plugins/modes.ts
|
|
7019
7019
|
import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
|
|
7020
|
+
import { join as join10 } from "path";
|
|
7020
7021
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
7021
7022
|
import { Text as Text3 } from "@earendil-works/pi-tui";
|
|
7022
7023
|
|
|
@@ -7031,12 +7032,17 @@ import { readFileSync as readFileSync10 } from "fs";
|
|
|
7031
7032
|
import path6 from "path";
|
|
7032
7033
|
var PROJECT_MCP_FILE = ".mcp.json";
|
|
7033
7034
|
function fingerprint(server) {
|
|
7034
|
-
const
|
|
7035
|
+
const may = { alwaysAllow: [...server.alwaysAllow].sort(), readOnly: server.readOnly };
|
|
7036
|
+
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
7037
|
return createHash("sha256").update(JSON.stringify(what)).digest("hex").slice(0, 16);
|
|
7036
7038
|
}
|
|
7037
7039
|
function describeServer(server) {
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
+
const where = server.transport === "stdio" ? [server.command, ...server.args ?? []].join(" ") : server.url ?? "";
|
|
7041
|
+
const may = [
|
|
7042
|
+
...server.readOnly ? ["marked read-only: usable in Plan mode, never asks"] : [],
|
|
7043
|
+
...server.alwaysAllow.length > 0 ? [`runs without asking: ${server.alwaysAllow.join(", ")}`] : []
|
|
7044
|
+
];
|
|
7045
|
+
return may.length === 0 ? where : `${where} (${may.join("; ")})`;
|
|
7040
7046
|
}
|
|
7041
7047
|
|
|
7042
7048
|
// src/mcp/state.ts
|
|
@@ -7383,13 +7389,13 @@ var SPACE_BUNNY_NOTICE = [
|
|
|
7383
7389
|
|
|
7384
7390
|
// src/setup/zen-route.ts
|
|
7385
7391
|
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
7386
|
-
import { join as
|
|
7392
|
+
import { join as join12 } from "path";
|
|
7387
7393
|
import { parseDocument } from "yaml";
|
|
7388
7394
|
|
|
7389
7395
|
// src/setup/simple.ts
|
|
7390
7396
|
import { chmod as chmod2, mkdir as mkdir4, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
|
|
7391
7397
|
import { existsSync as existsSync10 } from "fs";
|
|
7392
|
-
import { dirname as dirname2, join as
|
|
7398
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
7393
7399
|
|
|
7394
7400
|
// src/setup/cloud.ts
|
|
7395
7401
|
var DEEPSEEK_OFFICIAL = {
|
|
@@ -7416,7 +7422,7 @@ async function writeAtomic(path8, content, mode) {
|
|
|
7416
7422
|
// src/setup/zen-route.ts
|
|
7417
7423
|
var ROUTE_PATH = ["llm-pi-ai", "providers", SPACE_BUNNY.routeName];
|
|
7418
7424
|
async function addSpaceBunnyToHome(home) {
|
|
7419
|
-
const file =
|
|
7425
|
+
const file = join12(home, "settings.yaml");
|
|
7420
7426
|
const doc = parseDocument(existsSync11(file) ? readFileSync14(file, "utf8") : "");
|
|
7421
7427
|
if (doc.hasIn([...ROUTE_PATH])) {
|
|
7422
7428
|
await repairSpaceBunnyRoute(home);
|
|
@@ -7427,7 +7433,7 @@ async function addSpaceBunnyToHome(home) {
|
|
|
7427
7433
|
return "added";
|
|
7428
7434
|
}
|
|
7429
7435
|
async function repairSpaceBunnyRoute(home) {
|
|
7430
|
-
const file =
|
|
7436
|
+
const file = join12(home, "settings.yaml");
|
|
7431
7437
|
if (!existsSync11(file)) return false;
|
|
7432
7438
|
let doc;
|
|
7433
7439
|
try {
|
|
@@ -7475,12 +7481,12 @@ async function repairSpaceBunnyRoute(home) {
|
|
|
7475
7481
|
// src/plugins/model.ts
|
|
7476
7482
|
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
7477
7483
|
import { chmod as chmod3, rename as rename6, writeFile as writeFile6 } from "fs/promises";
|
|
7478
|
-
import { join as
|
|
7484
|
+
import { join as join14 } from "path";
|
|
7479
7485
|
import { matchesKey as matchesKey6 } from "@earendil-works/pi-tui";
|
|
7480
7486
|
|
|
7481
7487
|
// src/ui/errors.ts
|
|
7482
7488
|
import { appendFile, mkdir as mkdir5 } from "fs/promises";
|
|
7483
|
-
import { join as
|
|
7489
|
+
import { join as join13 } from "path";
|
|
7484
7490
|
import { truncateToWidth as truncateToWidth7, wrapTextWithAnsi as wrapTextWithAnsi3 } from "@earendil-works/pi-tui";
|
|
7485
7491
|
function formatK2(n) {
|
|
7486
7492
|
const k = n / 1e3;
|
|
@@ -7630,7 +7636,7 @@ function readEnvKeys(dshHome) {
|
|
|
7630
7636
|
const out = /* @__PURE__ */ new Set();
|
|
7631
7637
|
const home = dshHome ?? runtimeHome();
|
|
7632
7638
|
try {
|
|
7633
|
-
const path8 =
|
|
7639
|
+
const path8 = join14(home, ".env");
|
|
7634
7640
|
if (!existsSync12(path8)) return out;
|
|
7635
7641
|
for (const line of readFileSync15(path8, "utf8").split(/\r?\n/)) {
|
|
7636
7642
|
const name4 = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
@@ -8211,7 +8217,7 @@ var QuestionCallComponent = class {
|
|
|
8211
8217
|
|
|
8212
8218
|
// src/plugins/silence.ts
|
|
8213
8219
|
import { existsSync as existsSync13, readFileSync as readFileSync16, statSync as statSync3 } from "fs";
|
|
8214
|
-
import { join as
|
|
8220
|
+
import { join as join15 } from "path";
|
|
8215
8221
|
import { parse as parseYaml3 } from "yaml";
|
|
8216
8222
|
|
|
8217
8223
|
// src/plugins/render.ts
|
|
@@ -8435,7 +8441,7 @@ function answersIn(output) {
|
|
|
8435
8441
|
// src/plugins/config-edit.ts
|
|
8436
8442
|
import { spawn as spawn3 } from "child_process";
|
|
8437
8443
|
import { existsSync as existsSync14, readFileSync as readFileSync18 } from "fs";
|
|
8438
|
-
import { delimiter as delimiter3, join as
|
|
8444
|
+
import { delimiter as delimiter3, join as join16 } from "path";
|
|
8439
8445
|
var TERMINAL_EDITORS = /* @__PURE__ */ new Set([
|
|
8440
8446
|
"vi",
|
|
8441
8447
|
"vim",
|
|
@@ -8462,7 +8468,7 @@ var TERMINAL_EDITORS = /* @__PURE__ */ new Set([
|
|
|
8462
8468
|
]);
|
|
8463
8469
|
var GUI_EDITORS = ["kate", "kwrite", "gedit", "xed", "mousepad", "pluma", "code", "codium", "subl", "zed"];
|
|
8464
8470
|
function configFiles(home, exists2 = existsSync14) {
|
|
8465
|
-
return [
|
|
8471
|
+
return [join16(home, "settings.yaml"), configReadPath(home)].filter((p) => exists2(p));
|
|
8466
8472
|
}
|
|
8467
8473
|
function baseName(command) {
|
|
8468
8474
|
return command.split(/[\\/]/).pop()?.replace(/\.exe$/i, "") ?? command;
|
|
@@ -8476,7 +8482,7 @@ function onPath(command, env, exists2) {
|
|
|
8476
8482
|
if (/[\\/]/.test(command)) return exists2(command);
|
|
8477
8483
|
const dirs = (env.PATH ?? "").split(delimiter3).filter((d) => d !== "");
|
|
8478
8484
|
const names = process.platform === "win32" ? [command, `${command}.exe`, `${command}.cmd`] : [command];
|
|
8479
|
-
return dirs.some((dir) => names.some((name4) => exists2(
|
|
8485
|
+
return dirs.some((dir) => names.some((name4) => exists2(join16(dir, name4))));
|
|
8480
8486
|
}
|
|
8481
8487
|
function editorFromBruineJson(home) {
|
|
8482
8488
|
try {
|
|
@@ -8639,13 +8645,13 @@ async function stopTask(services, agent, sessionId, name4) {
|
|
|
8639
8645
|
|
|
8640
8646
|
// src/plugins/verify.ts
|
|
8641
8647
|
import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
|
|
8642
|
-
import { join as
|
|
8648
|
+
import { join as join17 } from "path";
|
|
8643
8649
|
function verificationPlan(cwd) {
|
|
8644
|
-
const packagePath =
|
|
8650
|
+
const packagePath = join17(cwd, "package.json");
|
|
8645
8651
|
if (existsSync15(packagePath)) {
|
|
8646
8652
|
try {
|
|
8647
8653
|
const doc = JSON.parse(readFileSync19(packagePath, "utf8"));
|
|
8648
|
-
const manager = doc.packageManager?.split("@")[0] ?? (existsSync15(
|
|
8654
|
+
const manager = doc.packageManager?.split("@")[0] ?? (existsSync15(join17(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync15(join17(cwd, "yarn.lock")) ? "yarn" : "npm");
|
|
8649
8655
|
const cli = ["pnpm", "yarn", "bun", "npm"].includes(manager) ? manager : "npm";
|
|
8650
8656
|
const checks = [];
|
|
8651
8657
|
if (typeof doc.scripts?.typecheck === "string") checks.push({ label: "Typecheck", command: `${cli} run typecheck` });
|
|
@@ -8655,8 +8661,8 @@ function verificationPlan(cwd) {
|
|
|
8655
8661
|
return [];
|
|
8656
8662
|
}
|
|
8657
8663
|
}
|
|
8658
|
-
if (existsSync15(
|
|
8659
|
-
if (existsSync15(
|
|
8664
|
+
if (existsSync15(join17(cwd, "Cargo.toml"))) return [{ label: "Tests", command: "cargo test" }];
|
|
8665
|
+
if (existsSync15(join17(cwd, "go.mod"))) return [{ label: "Tests", command: "go test ./..." }];
|
|
8660
8666
|
return [];
|
|
8661
8667
|
}
|
|
8662
8668
|
async function runVerification(cwd, run = (command, dir) => runShell(command, dir)) {
|
|
@@ -8675,7 +8681,7 @@ ${formatShell(check.command, result)}`)
|
|
|
8675
8681
|
}
|
|
8676
8682
|
|
|
8677
8683
|
// src/plugins/repl.ts
|
|
8678
|
-
import { join as
|
|
8684
|
+
import { join as join18 } from "path";
|
|
8679
8685
|
|
|
8680
8686
|
// src/image/attach.ts
|
|
8681
8687
|
function attachFailureNotice(failures) {
|
|
@@ -9056,7 +9062,7 @@ async function runRepl(ctx, exit) {
|
|
|
9056
9062
|
void sendWithImages(text, images);
|
|
9057
9063
|
};
|
|
9058
9064
|
const flush = (session) => sessions.flush(session);
|
|
9059
|
-
const skillsDir =
|
|
9065
|
+
const skillsDir = join18(runtimeHome(), "skills");
|
|
9060
9066
|
let skillNames = [];
|
|
9061
9067
|
const refreshSkillNames = () => {
|
|
9062
9068
|
void availableSkillNames(skillsDir, process.cwd()).then((names) => {
|