@brownie-labs/brownie 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/dist/index.js +408 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,20 +121,21 @@ Configuration commands persist to `.brownie/settings.json` and apply live — th
|
|
|
121
121
|
|
|
122
122
|
Without a TTY (systemd, Docker, CI, piping) brownie skips the dashboard, starts the agents immediately, and prints structured line logs to stdout — human-readable by default, NDJSON with `--log-format json` for log aggregators. A running worker is controlled from a second shell over a local control socket:
|
|
123
123
|
|
|
124
|
-
| Command | Effect
|
|
125
|
-
| --------------------------------------------------------- |
|
|
126
|
-
| `brownie` | start the worker (TUI in a terminal, headless without one)
|
|
127
|
-
| `brownie --headless [--log-format json]` | force headless mode even in a terminal
|
|
128
|
-
| `brownie init --monitor-prompt <f> --executor-prompt <f>` | non-interactive setup for servers (cloud-init, Ansible)
|
|
129
|
-
| `brownie status [--json]` | live status of the running worker (doubles as a health check)
|
|
130
|
-
| `brownie pause [monitor\|executor]` | graceful pause, same as `/pause` in the TUI
|
|
131
|
-
| `brownie resume [monitor\|executor]` | resume paused agents
|
|
124
|
+
| Command | Effect |
|
|
125
|
+
| --------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
126
|
+
| `brownie` | start the worker (TUI in a terminal, headless without one) |
|
|
127
|
+
| `brownie --headless [--log-format json]` | force headless mode even in a terminal |
|
|
128
|
+
| `brownie init --monitor-prompt <f> --executor-prompt <f>` | non-interactive setup for servers (cloud-init, Ansible) |
|
|
129
|
+
| `brownie status [--json]` | live status of the running worker (doubles as a health check) |
|
|
130
|
+
| `brownie pause [monitor\|executor]` | graceful pause, same as `/pause` in the TUI |
|
|
131
|
+
| `brownie resume [monitor\|executor]` | resume paused agents |
|
|
132
|
+
| `brownie update [--check]` | update to the newest published version (auto-updates in the background too) |
|
|
132
133
|
|
|
133
134
|
A second `brownie` in the same project refuses to start while one is already running. The full server story — the NDJSON event schema, a DigitalOcean/systemd runbook, authentication without a browser, and the reference `Dockerfile` + `docker-compose.yml`: [docs/deployment.md](https://github.com/brownie-labs/brownie/blob/main/docs/deployment.md).
|
|
134
135
|
|
|
135
136
|
## Configuration
|
|
136
137
|
|
|
137
|
-
Like Claude Code's `.claude/`, all per-project state lives in `.brownie/` inside the directory you run brownie from: `settings.json`, the two project prompts, and runtime data (tasks, memory, logs — gitignored automatically). The settings file is a
|
|
138
|
+
Like Claude Code's `.claude/`, all per-project state lives in `.brownie/` inside the directory you run brownie from: `settings.json`, the two project prompts, and runtime data (tasks, memory, logs — gitignored automatically). The settings file is a strictly validated JSON where every section is optional — change it with the slash commands above or by hand:
|
|
138
139
|
|
|
139
140
|
```json
|
|
140
141
|
{
|
package/dist/index.js
CHANGED
|
@@ -171,11 +171,12 @@ var logger = createConsola({
|
|
|
171
171
|
// src/paths.ts
|
|
172
172
|
import { createHash } from "node:crypto";
|
|
173
173
|
import { readFileSync } from "node:fs";
|
|
174
|
-
import { tmpdir } from "node:os";
|
|
174
|
+
import { homedir, tmpdir } from "node:os";
|
|
175
175
|
import { dirname, join, resolve } from "node:path";
|
|
176
176
|
import { fileURLToPath } from "node:url";
|
|
177
177
|
import { z } from "zod";
|
|
178
178
|
var BROWNIE_DIR_NAME = ".brownie";
|
|
179
|
+
var FALLBACK_PACKAGE_NAME = "@brownie-labs/brownie";
|
|
179
180
|
function projectPaths(projectDir = process.cwd()) {
|
|
180
181
|
const brownieDir = join(projectDir, BROWNIE_DIR_NAME);
|
|
181
182
|
const promptsDir = join(brownieDir, "prompts");
|
|
@@ -202,15 +203,26 @@ function controlSocketPath(projectDir = process.cwd()) {
|
|
|
202
203
|
}
|
|
203
204
|
var packageRootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
204
205
|
var packagePromptsDir = join(packageRootDir, "prompts");
|
|
205
|
-
var
|
|
206
|
-
|
|
206
|
+
var globalBrownieDir = join(homedir(), BROWNIE_DIR_NAME);
|
|
207
|
+
var globalConfigFile = join(globalBrownieDir, "config.json");
|
|
208
|
+
var packageManifestSchema = z.object({
|
|
209
|
+
name: z.string().default(FALLBACK_PACKAGE_NAME),
|
|
210
|
+
version: z.string().default("unknown")
|
|
211
|
+
});
|
|
212
|
+
function readPackageManifest() {
|
|
207
213
|
try {
|
|
208
214
|
const raw = readFileSync(join(packageRootDir, "package.json"), "utf8");
|
|
209
|
-
return packageManifestSchema.parse(JSON.parse(raw))
|
|
215
|
+
return packageManifestSchema.parse(JSON.parse(raw));
|
|
210
216
|
} catch {
|
|
211
|
-
return "unknown";
|
|
217
|
+
return { name: FALLBACK_PACKAGE_NAME, version: "unknown" };
|
|
212
218
|
}
|
|
213
219
|
}
|
|
220
|
+
function packageVersion() {
|
|
221
|
+
return readPackageManifest().version;
|
|
222
|
+
}
|
|
223
|
+
function packageName() {
|
|
224
|
+
return readPackageManifest().name;
|
|
225
|
+
}
|
|
214
226
|
function systemPromptFiles(dir = packagePromptsDir) {
|
|
215
227
|
return {
|
|
216
228
|
monitor: join(dir, "monitor.system.md"),
|
|
@@ -2071,6 +2083,31 @@ async function runExecutorLoop(config, store, waker, reporter, summarizer, contr
|
|
|
2071
2083
|
}
|
|
2072
2084
|
}
|
|
2073
2085
|
|
|
2086
|
+
// src/global-config.ts
|
|
2087
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
2088
|
+
import { z as z5 } from "zod";
|
|
2089
|
+
var globalConfigSchema = z5.object({
|
|
2090
|
+
autoUpdate: z5.boolean().default(true)
|
|
2091
|
+
}).strict();
|
|
2092
|
+
var DEFAULT_GLOBAL_CONFIG = globalConfigSchema.parse({});
|
|
2093
|
+
async function loadGlobalConfig(file = globalConfigFile) {
|
|
2094
|
+
let raw;
|
|
2095
|
+
try {
|
|
2096
|
+
raw = await readFile5(file, "utf8");
|
|
2097
|
+
} catch {
|
|
2098
|
+
return DEFAULT_GLOBAL_CONFIG;
|
|
2099
|
+
}
|
|
2100
|
+
try {
|
|
2101
|
+
return globalConfigSchema.parse(JSON.parse(raw));
|
|
2102
|
+
} catch {
|
|
2103
|
+
return DEFAULT_GLOBAL_CONFIG;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
function isAutoUpdaterDisabled(env = process.env) {
|
|
2107
|
+
const value = env.BROWNIE_DISABLE_AUTOUPDATER?.trim().toLowerCase();
|
|
2108
|
+
return value === "1" || value === "true";
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2074
2111
|
// src/headless/events.ts
|
|
2075
2112
|
function compactFields(fields) {
|
|
2076
2113
|
const compacted = {};
|
|
@@ -2316,13 +2353,13 @@ function teeReporter(primary, secondary) {
|
|
|
2316
2353
|
}
|
|
2317
2354
|
|
|
2318
2355
|
// src/memory/summarizer.ts
|
|
2319
|
-
import { readFile as
|
|
2356
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
2320
2357
|
|
|
2321
2358
|
// src/memory/summary.ts
|
|
2322
|
-
import { z as
|
|
2359
|
+
import { z as z7 } from "zod";
|
|
2323
2360
|
|
|
2324
2361
|
// src/report.ts
|
|
2325
|
-
import { z as
|
|
2362
|
+
import { z as z6 } from "zod";
|
|
2326
2363
|
var TASK_REPORT_JSON_SCHEMA = JSON.stringify({
|
|
2327
2364
|
type: "object",
|
|
2328
2365
|
properties: {
|
|
@@ -2350,12 +2387,12 @@ var TASK_REPORT_JSON_SCHEMA = JSON.stringify({
|
|
|
2350
2387
|
required: ["tasks"],
|
|
2351
2388
|
additionalProperties: false
|
|
2352
2389
|
});
|
|
2353
|
-
var taskReportSchema =
|
|
2354
|
-
tasks:
|
|
2355
|
-
|
|
2356
|
-
id:
|
|
2357
|
-
title:
|
|
2358
|
-
description:
|
|
2390
|
+
var taskReportSchema = z6.object({
|
|
2391
|
+
tasks: z6.array(
|
|
2392
|
+
z6.object({
|
|
2393
|
+
id: z6.string().trim().min(1),
|
|
2394
|
+
title: z6.string().trim().min(1),
|
|
2395
|
+
description: z6.string().trim().default("")
|
|
2359
2396
|
})
|
|
2360
2397
|
)
|
|
2361
2398
|
});
|
|
@@ -2446,9 +2483,9 @@ var SUMMARY_JSON_SCHEMA = JSON.stringify({
|
|
|
2446
2483
|
required: ["headline", "summary"],
|
|
2447
2484
|
additionalProperties: false
|
|
2448
2485
|
});
|
|
2449
|
-
var summarySchema =
|
|
2450
|
-
headline:
|
|
2451
|
-
summary:
|
|
2486
|
+
var summarySchema = z7.object({
|
|
2487
|
+
headline: z7.string().trim().min(1),
|
|
2488
|
+
summary: z7.string().trim().min(1)
|
|
2452
2489
|
});
|
|
2453
2490
|
function parseSummary(resultText) {
|
|
2454
2491
|
const candidate = lastJsonBlock(resultText) ?? resultText.trim();
|
|
@@ -2526,7 +2563,7 @@ var SessionSummarizer = class {
|
|
|
2526
2563
|
finished(false, "executor session log file not found");
|
|
2527
2564
|
return;
|
|
2528
2565
|
}
|
|
2529
|
-
const systemPrompt = await
|
|
2566
|
+
const systemPrompt = await readFile6(summarizer.systemPromptPath, "utf8");
|
|
2530
2567
|
const sessionResult = await runSession(
|
|
2531
2568
|
{
|
|
2532
2569
|
command: this.deps.command,
|
|
@@ -2582,7 +2619,7 @@ var SessionSummarizer = class {
|
|
|
2582
2619
|
};
|
|
2583
2620
|
|
|
2584
2621
|
// src/monitor.ts
|
|
2585
|
-
import { readFile as
|
|
2622
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
2586
2623
|
async function runMonitorLoop(config, store, waker, reporter, controller, limitGate, signal) {
|
|
2587
2624
|
const { monitor } = config;
|
|
2588
2625
|
const aborted = () => signal.aborted;
|
|
@@ -2608,8 +2645,8 @@ async function runMonitorLoop(config, store, waker, reporter, controller, limitG
|
|
|
2608
2645
|
reporter.cycleStarted(cycle);
|
|
2609
2646
|
try {
|
|
2610
2647
|
const [prompt, systemPrompt] = await Promise.all([
|
|
2611
|
-
|
|
2612
|
-
|
|
2648
|
+
readFile7(monitor.promptPath, "utf8"),
|
|
2649
|
+
readFile7(monitor.systemPromptPath, "utf8")
|
|
2613
2650
|
]);
|
|
2614
2651
|
const result = await runSession(
|
|
2615
2652
|
{
|
|
@@ -2778,11 +2815,11 @@ ${details}`);
|
|
|
2778
2815
|
}
|
|
2779
2816
|
|
|
2780
2817
|
// src/prompt-files.ts
|
|
2781
|
-
import { readFile as
|
|
2818
|
+
import { readFile as readFile8, rename, writeFile as writeFile2 } from "node:fs/promises";
|
|
2782
2819
|
var PROMPT_AGENTS = ["monitor", "executor"];
|
|
2783
2820
|
function createPromptFileAccess(paths) {
|
|
2784
2821
|
return {
|
|
2785
|
-
read: async (agent) => (await
|
|
2822
|
+
read: async (agent) => (await readFile8(paths[agent], "utf8")).trimEnd(),
|
|
2786
2823
|
write: async (agent, content) => {
|
|
2787
2824
|
const path = paths[agent];
|
|
2788
2825
|
const tmpPath = `${path}.tmp`;
|
|
@@ -2868,11 +2905,11 @@ function teeSession(reporter, extra) {
|
|
|
2868
2905
|
}
|
|
2869
2906
|
|
|
2870
2907
|
// src/settings-file.ts
|
|
2871
|
-
import { readFile as
|
|
2908
|
+
import { readFile as readFile9, rename as rename2, writeFile as writeFile3 } from "node:fs/promises";
|
|
2872
2909
|
async function readRawSettings(settingsFile) {
|
|
2873
2910
|
let raw;
|
|
2874
2911
|
try {
|
|
2875
|
-
raw = await
|
|
2912
|
+
raw = await readFile9(settingsFile, "utf8");
|
|
2876
2913
|
} catch (err) {
|
|
2877
2914
|
throw new Error(
|
|
2878
2915
|
`settings file missing: ${settingsFile} \u2014 run brownie in an interactive terminal to complete setup`,
|
|
@@ -3044,6 +3081,7 @@ var WorkerStatusStore = class {
|
|
|
3044
3081
|
listeners = /* @__PURE__ */ new Set();
|
|
3045
3082
|
notifyTimer = null;
|
|
3046
3083
|
shutdownSignal;
|
|
3084
|
+
updateStatus;
|
|
3047
3085
|
tasks = [];
|
|
3048
3086
|
snapshot;
|
|
3049
3087
|
stats = {
|
|
@@ -3176,6 +3214,10 @@ var WorkerStatusStore = class {
|
|
|
3176
3214
|
this.shutdownSignal = signalName;
|
|
3177
3215
|
this.markDirty();
|
|
3178
3216
|
}
|
|
3217
|
+
setUpdateStatus(info) {
|
|
3218
|
+
this.updateStatus = info;
|
|
3219
|
+
this.markDirty();
|
|
3220
|
+
}
|
|
3179
3221
|
subscribe = (listener) => {
|
|
3180
3222
|
this.listeners.add(listener);
|
|
3181
3223
|
return () => {
|
|
@@ -3321,7 +3363,8 @@ var WorkerStatusStore = class {
|
|
|
3321
3363
|
monitor: this.buildPanel(this.monitorState),
|
|
3322
3364
|
executor: this.buildPanel(this.executorState),
|
|
3323
3365
|
tasks: this.tasks,
|
|
3324
|
-
stats: { ...this.stats }
|
|
3366
|
+
stats: { ...this.stats },
|
|
3367
|
+
update: this.updateStatus
|
|
3325
3368
|
};
|
|
3326
3369
|
}
|
|
3327
3370
|
buildPanel(state) {
|
|
@@ -3346,22 +3389,22 @@ var WorkerStatusStore = class {
|
|
|
3346
3389
|
};
|
|
3347
3390
|
|
|
3348
3391
|
// src/tasks.ts
|
|
3349
|
-
import { mkdir as mkdir2, readFile as
|
|
3392
|
+
import { mkdir as mkdir2, readFile as readFile10, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
|
|
3350
3393
|
import { dirname as dirname3 } from "node:path";
|
|
3351
|
-
import { z as
|
|
3352
|
-
var taskSchema =
|
|
3353
|
-
id:
|
|
3354
|
-
title:
|
|
3355
|
-
description:
|
|
3356
|
-
status:
|
|
3357
|
-
attempts:
|
|
3358
|
-
createdAt:
|
|
3359
|
-
updatedAt:
|
|
3360
|
-
error:
|
|
3394
|
+
import { z as z8 } from "zod";
|
|
3395
|
+
var taskSchema = z8.object({
|
|
3396
|
+
id: z8.string().min(1),
|
|
3397
|
+
title: z8.string(),
|
|
3398
|
+
description: z8.string(),
|
|
3399
|
+
status: z8.enum(["pending", "in_progress", "done", "failed", "cancelled"]),
|
|
3400
|
+
attempts: z8.number().int().nonnegative().default(0),
|
|
3401
|
+
createdAt: z8.string(),
|
|
3402
|
+
updatedAt: z8.string(),
|
|
3403
|
+
error: z8.string().optional()
|
|
3361
3404
|
});
|
|
3362
|
-
var storeFileSchema =
|
|
3363
|
-
version:
|
|
3364
|
-
tasks:
|
|
3405
|
+
var storeFileSchema = z8.object({
|
|
3406
|
+
version: z8.literal(1),
|
|
3407
|
+
tasks: z8.array(taskSchema)
|
|
3365
3408
|
});
|
|
3366
3409
|
function corruptStoreError(path, reason) {
|
|
3367
3410
|
return new Error(
|
|
@@ -3382,7 +3425,7 @@ var TaskStore = class _TaskStore {
|
|
|
3382
3425
|
await mkdir2(dirname3(path), { recursive: true });
|
|
3383
3426
|
let raw;
|
|
3384
3427
|
try {
|
|
3385
|
-
raw = await
|
|
3428
|
+
raw = await readFile10(path, "utf8");
|
|
3386
3429
|
} catch (err) {
|
|
3387
3430
|
if (err.code !== "ENOENT") throw err;
|
|
3388
3431
|
}
|
|
@@ -3543,6 +3586,245 @@ var TaskStore = class _TaskStore {
|
|
|
3543
3586
|
}
|
|
3544
3587
|
};
|
|
3545
3588
|
|
|
3589
|
+
// src/update/version.ts
|
|
3590
|
+
var SEMVER_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3591
|
+
function parseVersion(raw) {
|
|
3592
|
+
const match = SEMVER_PATTERN.exec(raw.trim());
|
|
3593
|
+
if (match === null) return null;
|
|
3594
|
+
const [, major, minor, patch, prerelease] = match;
|
|
3595
|
+
return {
|
|
3596
|
+
major: Number(major),
|
|
3597
|
+
minor: Number(minor),
|
|
3598
|
+
patch: Number(patch),
|
|
3599
|
+
prerelease: prerelease ?? null
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
function comparePrerelease(a, b) {
|
|
3603
|
+
if (a === b) return 0;
|
|
3604
|
+
if (a === null) return 1;
|
|
3605
|
+
if (b === null) return -1;
|
|
3606
|
+
const aParts = a.split(".");
|
|
3607
|
+
const bParts = b.split(".");
|
|
3608
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i += 1) {
|
|
3609
|
+
const aPart = aParts[i];
|
|
3610
|
+
const bPart = bParts[i];
|
|
3611
|
+
if (aPart === void 0) return -1;
|
|
3612
|
+
if (bPart === void 0) return 1;
|
|
3613
|
+
const aNum = Number(aPart);
|
|
3614
|
+
const bNum = Number(bPart);
|
|
3615
|
+
const aIsNum = Number.isInteger(aNum);
|
|
3616
|
+
const bIsNum = Number.isInteger(bNum);
|
|
3617
|
+
if (aIsNum && bIsNum) {
|
|
3618
|
+
if (aNum !== bNum) return aNum < bNum ? -1 : 1;
|
|
3619
|
+
} else if (aIsNum !== bIsNum) {
|
|
3620
|
+
return aIsNum ? -1 : 1;
|
|
3621
|
+
} else if (aPart !== bPart) {
|
|
3622
|
+
return aPart < bPart ? -1 : 1;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return 0;
|
|
3626
|
+
}
|
|
3627
|
+
function compareVersions(a, b) {
|
|
3628
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
3629
|
+
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
3630
|
+
}
|
|
3631
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
3632
|
+
}
|
|
3633
|
+
function isNewer(current, latest) {
|
|
3634
|
+
const currentVersion = parseVersion(current);
|
|
3635
|
+
const latestVersion = parseVersion(latest);
|
|
3636
|
+
if (currentVersion === null || latestVersion === null) return false;
|
|
3637
|
+
return compareVersions(currentVersion, latestVersion) < 0;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
// src/update/auto-update.ts
|
|
3641
|
+
var UPDATE_CHECK_INTERVAL_MS = 30 * 6e4;
|
|
3642
|
+
async function runAutoUpdateLoop(options) {
|
|
3643
|
+
const {
|
|
3644
|
+
globalConfig,
|
|
3645
|
+
deps,
|
|
3646
|
+
setUpdateStatus,
|
|
3647
|
+
emit,
|
|
3648
|
+
signal,
|
|
3649
|
+
intervalMs = UPDATE_CHECK_INTERVAL_MS,
|
|
3650
|
+
initialDelayMs = 0,
|
|
3651
|
+
env = process.env
|
|
3652
|
+
} = options;
|
|
3653
|
+
if (isAutoUpdaterDisabled(env)) return;
|
|
3654
|
+
const method = deps.detect();
|
|
3655
|
+
if (method === "unknown") return;
|
|
3656
|
+
if (initialDelayMs > 0) await sleep(initialDelayMs, signal);
|
|
3657
|
+
let lastActioned;
|
|
3658
|
+
while (!signal.aborted) {
|
|
3659
|
+
try {
|
|
3660
|
+
const latest = await deps.fetchLatest(deps.name);
|
|
3661
|
+
if (latest !== null && latest !== lastActioned && isNewer(deps.current, latest)) {
|
|
3662
|
+
lastActioned = latest;
|
|
3663
|
+
await handleNewVersion({
|
|
3664
|
+
globalConfig,
|
|
3665
|
+
deps,
|
|
3666
|
+
method,
|
|
3667
|
+
latest,
|
|
3668
|
+
setUpdateStatus,
|
|
3669
|
+
emit
|
|
3670
|
+
});
|
|
3671
|
+
}
|
|
3672
|
+
} catch {
|
|
3673
|
+
}
|
|
3674
|
+
await sleep(intervalMs, signal);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
async function handleNewVersion(args) {
|
|
3678
|
+
const { globalConfig, deps, method, latest, setUpdateStatus, emit } = args;
|
|
3679
|
+
const from = deps.current;
|
|
3680
|
+
if (!globalConfig.autoUpdate) {
|
|
3681
|
+
setUpdateStatus({ state: "available", from, to: latest });
|
|
3682
|
+
emit?.({
|
|
3683
|
+
level: "info",
|
|
3684
|
+
event: "update.available",
|
|
3685
|
+
fields: { from, to: latest }
|
|
3686
|
+
});
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
const result = await deps.install(method, deps.name);
|
|
3690
|
+
if (result.ok) {
|
|
3691
|
+
setUpdateStatus({ state: "installed", from, to: latest });
|
|
3692
|
+
emit?.({
|
|
3693
|
+
level: "info",
|
|
3694
|
+
event: "update.installed",
|
|
3695
|
+
fields: { from, to: latest }
|
|
3696
|
+
});
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
setUpdateStatus({ state: "available", from, to: latest, installError: result.output });
|
|
3700
|
+
emit?.({
|
|
3701
|
+
level: "warn",
|
|
3702
|
+
event: "update.available",
|
|
3703
|
+
fields: { from, to: latest, installError: result.output }
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
// src/update/install.ts
|
|
3708
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3709
|
+
var KILL_GRACE_MS2 = 5e3;
|
|
3710
|
+
var DEFAULT_INSTALL_TIMEOUT_MS = 12e4;
|
|
3711
|
+
function detectInstallMethod(rootDir = packageRootDir) {
|
|
3712
|
+
const normalized = rootDir.replaceAll("\\", "/").toLowerCase();
|
|
3713
|
+
if (!normalized.includes("/node_modules/") && !normalized.endsWith("/node_modules")) {
|
|
3714
|
+
return "unknown";
|
|
3715
|
+
}
|
|
3716
|
+
if (normalized.includes("/pnpm/") || normalized.includes("/.pnpm/")) return "pnpm";
|
|
3717
|
+
if (normalized.includes("/yarn/") || normalized.includes("/.yarn/")) return "yarn";
|
|
3718
|
+
if (normalized.includes("/.bun/") || normalized.includes("/bun/")) return "bun";
|
|
3719
|
+
return "npm";
|
|
3720
|
+
}
|
|
3721
|
+
function installCommand(method, name) {
|
|
3722
|
+
const target = `${name}@latest`;
|
|
3723
|
+
switch (method) {
|
|
3724
|
+
case "npm":
|
|
3725
|
+
return { command: "npm", args: ["install", "-g", target] };
|
|
3726
|
+
case "pnpm":
|
|
3727
|
+
return { command: "pnpm", args: ["add", "-g", target] };
|
|
3728
|
+
case "yarn":
|
|
3729
|
+
return { command: "yarn", args: ["global", "add", target] };
|
|
3730
|
+
case "bun":
|
|
3731
|
+
return { command: "bun", args: ["add", "-g", target] };
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
function runInstall(method, name, options = {}) {
|
|
3735
|
+
const { command, args } = installCommand(method, name);
|
|
3736
|
+
const { env = process.env, timeoutMs = DEFAULT_INSTALL_TIMEOUT_MS } = options;
|
|
3737
|
+
return new Promise((resolvePromise) => {
|
|
3738
|
+
const child = spawn2(command, args, { env, stdio: ["ignore", "pipe", "pipe"] });
|
|
3739
|
+
const chunks = [];
|
|
3740
|
+
let settled = false;
|
|
3741
|
+
const settle = (result) => {
|
|
3742
|
+
if (settled) return;
|
|
3743
|
+
settled = true;
|
|
3744
|
+
clearTimeout(timer);
|
|
3745
|
+
resolvePromise(result);
|
|
3746
|
+
};
|
|
3747
|
+
const timer = setTimeout(() => {
|
|
3748
|
+
chunks.push(`
|
|
3749
|
+
Install timed out after ${String(timeoutMs)}ms`);
|
|
3750
|
+
child.kill("SIGTERM");
|
|
3751
|
+
setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS2).unref();
|
|
3752
|
+
}, timeoutMs);
|
|
3753
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk.toString()));
|
|
3754
|
+
child.stderr.on("data", (chunk) => chunks.push(chunk.toString()));
|
|
3755
|
+
child.on("error", (err) => {
|
|
3756
|
+
chunks.push(`Failed to start "${command}": ${err.message}`);
|
|
3757
|
+
settle({ ok: false, output: chunks.join("").trim() });
|
|
3758
|
+
});
|
|
3759
|
+
child.on("close", (code) => {
|
|
3760
|
+
settle({ ok: code === 0, output: chunks.join("").trim() });
|
|
3761
|
+
});
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// src/update/registry.ts
|
|
3766
|
+
import { z as z9 } from "zod";
|
|
3767
|
+
var REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
3768
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
3769
|
+
var distTagSchema = z9.object({ version: z9.string() });
|
|
3770
|
+
async function fetchLatestVersion(name, options = {}) {
|
|
3771
|
+
const {
|
|
3772
|
+
fetchImpl = fetch,
|
|
3773
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
3774
|
+
registryBaseUrl = REGISTRY_BASE_URL
|
|
3775
|
+
} = options;
|
|
3776
|
+
const url = `${registryBaseUrl}/${name}/latest`;
|
|
3777
|
+
try {
|
|
3778
|
+
const response = await fetchImpl(url, {
|
|
3779
|
+
headers: { accept: "application/json" },
|
|
3780
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3781
|
+
});
|
|
3782
|
+
if (!response.ok) return null;
|
|
3783
|
+
const parsed = distTagSchema.safeParse(await response.json());
|
|
3784
|
+
return parsed.success ? parsed.data.version : null;
|
|
3785
|
+
} catch {
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
// src/update/updater.ts
|
|
3791
|
+
function defaultUpdateDeps(overrides = {}) {
|
|
3792
|
+
return {
|
|
3793
|
+
name: packageName(),
|
|
3794
|
+
current: packageVersion(),
|
|
3795
|
+
fetchLatest: (name) => fetchLatestVersion(name),
|
|
3796
|
+
detect: () => detectInstallMethod(),
|
|
3797
|
+
install: (method, name) => runInstall(method, name),
|
|
3798
|
+
...overrides
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
async function checkForUpdate(deps) {
|
|
3802
|
+
const latest = await deps.fetchLatest(deps.name);
|
|
3803
|
+
return {
|
|
3804
|
+
current: deps.current,
|
|
3805
|
+
latest,
|
|
3806
|
+
newer: latest !== null && isNewer(deps.current, latest),
|
|
3807
|
+
method: deps.detect()
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3810
|
+
async function performUpdate(deps, options) {
|
|
3811
|
+
const check2 = await checkForUpdate(deps);
|
|
3812
|
+
const base = { from: check2.current, method: check2.method };
|
|
3813
|
+
if (check2.latest === null) return { ...base, status: "unreachable" };
|
|
3814
|
+
if (!check2.newer) return { ...base, status: "up-to-date", to: check2.latest };
|
|
3815
|
+
if (!options.install) return { ...base, status: "available", to: check2.latest };
|
|
3816
|
+
if (check2.method === "unknown") {
|
|
3817
|
+
return { ...base, status: "unmanaged", to: check2.latest };
|
|
3818
|
+
}
|
|
3819
|
+
const result = await deps.install(check2.method, deps.name);
|
|
3820
|
+
return {
|
|
3821
|
+
...base,
|
|
3822
|
+
status: result.ok ? "updated" : "failed",
|
|
3823
|
+
to: check2.latest,
|
|
3824
|
+
output: result.output
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3546
3828
|
// src/ui/mount.tsx
|
|
3547
3829
|
import { render as render2 } from "ink";
|
|
3548
3830
|
|
|
@@ -4101,11 +4383,11 @@ async function dispatchCommand(line, ctx) {
|
|
|
4101
4383
|
}
|
|
4102
4384
|
|
|
4103
4385
|
// src/ui/header.tsx
|
|
4104
|
-
import { homedir } from "node:os";
|
|
4386
|
+
import { homedir as homedir2 } from "node:os";
|
|
4105
4387
|
import { Box as Box5, Text as Text5 } from "ink";
|
|
4106
4388
|
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
4107
4389
|
function shortenPath(path) {
|
|
4108
|
-
const home =
|
|
4390
|
+
const home = homedir2();
|
|
4109
4391
|
return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
4110
4392
|
}
|
|
4111
4393
|
function HeaderLine({ left, right }) {
|
|
@@ -4155,7 +4437,8 @@ function Header({ config, version, status, now }) {
|
|
|
4155
4437
|
right: `${config.executor.model} \xB7 ${config.executor.effort}`
|
|
4156
4438
|
}
|
|
4157
4439
|
),
|
|
4158
|
-
/* @__PURE__ */ jsx6(Text5, { dimColor: true, wrap: "truncate-end", children: formatHeaderStats(status.stats, status.tasks, now - status.startedAt) })
|
|
4440
|
+
/* @__PURE__ */ jsx6(Text5, { dimColor: true, wrap: "truncate-end", children: formatHeaderStats(status.stats, status.tasks, now - status.startedAt) }),
|
|
4441
|
+
status.update === void 0 ? null : /* @__PURE__ */ jsx6(Text5, { color: theme.accent, wrap: "truncate-end", children: status.update.state === "installed" ? `\u2B06 updated to v${status.update.to} \u2014 restart to apply` : `\u2B06 update available v${status.update.from} \u2192 v${status.update.to} \u2014 run "brownie update"` })
|
|
4159
4442
|
]
|
|
4160
4443
|
}
|
|
4161
4444
|
);
|
|
@@ -4931,9 +5214,10 @@ function App({
|
|
|
4931
5214
|
const inputHeight = interactive && !editing ? INPUT_HEIGHT : 0;
|
|
4932
5215
|
const menuHeight = menuOpen ? Math.min(suggestionList.length, SUGGESTION_WINDOW) : 0;
|
|
4933
5216
|
const shutdownHeight = status.shutdownSignal === void 0 ? 0 : 1;
|
|
5217
|
+
const headerHeight = HEADER_HEIGHT + (status.update === void 0 ? 0 : 1);
|
|
4934
5218
|
const contentHeight = Math.max(
|
|
4935
5219
|
6,
|
|
4936
|
-
rows -
|
|
5220
|
+
rows - headerHeight - inputHeight - menuHeight - hintHeight - noticeHeight - shutdownHeight
|
|
4937
5221
|
);
|
|
4938
5222
|
const scrollTarget = view.kind === "dashboard" ? focusedPanel : view.kind === "monitor" || view.kind === "executor" ? view.kind : null;
|
|
4939
5223
|
useInput2(
|
|
@@ -5302,6 +5586,7 @@ async function startWorker(options = {}) {
|
|
|
5302
5586
|
projectDir: config.cwd
|
|
5303
5587
|
}
|
|
5304
5588
|
});
|
|
5589
|
+
const globalConfig = await loadGlobalConfig();
|
|
5305
5590
|
let loopError;
|
|
5306
5591
|
try {
|
|
5307
5592
|
await Promise.all([
|
|
@@ -5323,7 +5608,14 @@ async function startWorker(options = {}) {
|
|
|
5323
5608
|
executorControl,
|
|
5324
5609
|
limitGate,
|
|
5325
5610
|
signal
|
|
5326
|
-
)
|
|
5611
|
+
),
|
|
5612
|
+
runAutoUpdateLoop({
|
|
5613
|
+
globalConfig,
|
|
5614
|
+
deps: defaultUpdateDeps(),
|
|
5615
|
+
setUpdateStatus: (info) => status.setUpdateStatus(info),
|
|
5616
|
+
emit: headlessEmit,
|
|
5617
|
+
signal
|
|
5618
|
+
})
|
|
5327
5619
|
]);
|
|
5328
5620
|
} catch (err) {
|
|
5329
5621
|
loopError = err;
|
|
@@ -5354,7 +5646,7 @@ async function runBrownie(options = {}) {
|
|
|
5354
5646
|
const [positional] = options.positionals ?? [];
|
|
5355
5647
|
if (positional !== void 0) {
|
|
5356
5648
|
logger.error(
|
|
5357
|
-
`Unknown command "${positional}" \u2014 available commands: init, status, pause, resume, mcp; run plain brownie to start the worker.`
|
|
5649
|
+
`Unknown command "${positional}" \u2014 available commands: init, status, pause, resume, update, mcp; run plain brownie to start the worker.`
|
|
5358
5650
|
);
|
|
5359
5651
|
process.exitCode = 1;
|
|
5360
5652
|
return;
|
|
@@ -5415,6 +5707,72 @@ var mcpCommand = defineCommand5({
|
|
|
5415
5707
|
}
|
|
5416
5708
|
});
|
|
5417
5709
|
|
|
5710
|
+
// src/update-command.ts
|
|
5711
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
5712
|
+
async function runUpdate(options = {}) {
|
|
5713
|
+
const deps = options.deps ?? defaultUpdateDeps();
|
|
5714
|
+
const install = options.check !== true;
|
|
5715
|
+
const outcome = await performUpdate(deps, { install });
|
|
5716
|
+
switch (outcome.status) {
|
|
5717
|
+
case "unreachable":
|
|
5718
|
+
logger.error("Could not reach the npm registry to check for updates.");
|
|
5719
|
+
process.exitCode = 1;
|
|
5720
|
+
break;
|
|
5721
|
+
case "up-to-date":
|
|
5722
|
+
logger.success(`brownie is up to date (${outcome.from}).`);
|
|
5723
|
+
break;
|
|
5724
|
+
case "available":
|
|
5725
|
+
logger.info(`Update available: ${outcome.from} \u2192 ${outcome.to ?? "?"}.`);
|
|
5726
|
+
logger.info('Run "brownie update" to install it.');
|
|
5727
|
+
break;
|
|
5728
|
+
case "unmanaged": {
|
|
5729
|
+
const manual = installCommand("npm", deps.name);
|
|
5730
|
+
logger.warn(
|
|
5731
|
+
`Update available: ${outcome.from} \u2192 ${outcome.to ?? "?"}, but the install location is not a global package manager install. Install it manually:`
|
|
5732
|
+
);
|
|
5733
|
+
logger.info(` ${manual.command} ${manual.args.join(" ")}`);
|
|
5734
|
+
process.exitCode = 1;
|
|
5735
|
+
break;
|
|
5736
|
+
}
|
|
5737
|
+
case "updated":
|
|
5738
|
+
logger.success(
|
|
5739
|
+
`Updated brownie ${outcome.from} \u2192 ${outcome.to ?? "?"}. Restart to apply.`
|
|
5740
|
+
);
|
|
5741
|
+
break;
|
|
5742
|
+
case "failed":
|
|
5743
|
+
logger.error(
|
|
5744
|
+
`Failed to update brownie ${outcome.from} \u2192 ${outcome.to ?? "?"} via ${outcome.method}.`
|
|
5745
|
+
);
|
|
5746
|
+
if (outcome.output) logger.error(outcome.output);
|
|
5747
|
+
process.exitCode = 1;
|
|
5748
|
+
break;
|
|
5749
|
+
}
|
|
5750
|
+
await reportAutoUpdateState();
|
|
5751
|
+
}
|
|
5752
|
+
async function reportAutoUpdateState() {
|
|
5753
|
+
if (isAutoUpdaterDisabled()) {
|
|
5754
|
+
logger.info("Auto-update is disabled (BROWNIE_DISABLE_AUTOUPDATER).");
|
|
5755
|
+
return;
|
|
5756
|
+
}
|
|
5757
|
+
const config = await loadGlobalConfig();
|
|
5758
|
+
logger.info(
|
|
5759
|
+
`Auto-update is ${config.autoUpdate ? "on" : "off (notify only)"} \u2014 configure it in ${globalConfigFile}.`
|
|
5760
|
+
);
|
|
5761
|
+
}
|
|
5762
|
+
var updateCommand = defineCommand6({
|
|
5763
|
+
meta: {
|
|
5764
|
+
name: "update",
|
|
5765
|
+
description: "Check npm for a newer brownie and install it."
|
|
5766
|
+
},
|
|
5767
|
+
args: {
|
|
5768
|
+
check: {
|
|
5769
|
+
type: "boolean",
|
|
5770
|
+
description: "Only check for a newer version without installing it"
|
|
5771
|
+
}
|
|
5772
|
+
},
|
|
5773
|
+
run: ({ args }) => runUpdate({ check: args.check })
|
|
5774
|
+
});
|
|
5775
|
+
|
|
5418
5776
|
// src/index.ts
|
|
5419
5777
|
var rawArgs = process.argv.slice(2);
|
|
5420
5778
|
var [first, ...rest] = rawArgs;
|
|
@@ -5434,6 +5792,9 @@ switch (first) {
|
|
|
5434
5792
|
case "resume":
|
|
5435
5793
|
void runMain(resumeCommand, { rawArgs: rest });
|
|
5436
5794
|
break;
|
|
5795
|
+
case "update":
|
|
5796
|
+
void runMain(updateCommand, { rawArgs: rest });
|
|
5797
|
+
break;
|
|
5437
5798
|
default:
|
|
5438
5799
|
void runMain(mainCommand, { rawArgs });
|
|
5439
5800
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brownie-labs/brownie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Autonomous two-agent Claude Code worker: a monitor that finds tasks, an executor that completes them, and long-term memory in between",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|