@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.
Files changed (3) hide show
  1. package/README.md +10 -9
  2. package/dist/index.js +408 -47
  3. 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 zod-validated JSON where every section is optional — change it with the slash commands above or by hand:
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 packageManifestSchema = z.object({ version: z.string() });
206
- function packageVersion() {
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)).version;
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 readFile5 } from "node:fs/promises";
2356
+ import { readFile as readFile6 } from "node:fs/promises";
2320
2357
 
2321
2358
  // src/memory/summary.ts
2322
- import { z as z6 } from "zod";
2359
+ import { z as z7 } from "zod";
2323
2360
 
2324
2361
  // src/report.ts
2325
- import { z as z5 } from "zod";
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 = z5.object({
2354
- tasks: z5.array(
2355
- z5.object({
2356
- id: z5.string().trim().min(1),
2357
- title: z5.string().trim().min(1),
2358
- description: z5.string().trim().default("")
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 = z6.object({
2450
- headline: z6.string().trim().min(1),
2451
- summary: z6.string().trim().min(1)
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 readFile5(summarizer.systemPromptPath, "utf8");
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 readFile6 } from "node:fs/promises";
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
- readFile6(monitor.promptPath, "utf8"),
2612
- readFile6(monitor.systemPromptPath, "utf8")
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 readFile7, rename, writeFile as writeFile2 } from "node:fs/promises";
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 readFile7(paths[agent], "utf8")).trimEnd(),
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 readFile8, rename as rename2, writeFile as writeFile3 } from "node:fs/promises";
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 readFile8(settingsFile, "utf8");
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 readFile9, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
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 z7 } from "zod";
3352
- var taskSchema = z7.object({
3353
- id: z7.string().min(1),
3354
- title: z7.string(),
3355
- description: z7.string(),
3356
- status: z7.enum(["pending", "in_progress", "done", "failed", "cancelled"]),
3357
- attempts: z7.number().int().nonnegative().default(0),
3358
- createdAt: z7.string(),
3359
- updatedAt: z7.string(),
3360
- error: z7.string().optional()
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 = z7.object({
3363
- version: z7.literal(1),
3364
- tasks: z7.array(taskSchema)
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 readFile9(path, "utf8");
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 = homedir();
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 - HEADER_HEIGHT - inputHeight - menuHeight - hintHeight - noticeHeight - shutdownHeight
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.1.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",