Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
# --- Build Stage ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./

# Install dependencies, ignoring peer conflicts
RUN npm ci --legacy-peer-deps
COPY tsconfig.json ./
COPY src ./src

# Build the application
RUN npm run build

# --- Runtime Stage ---
FROM node:20-alpine AS runtime
RUN apk add --no-cache bash git
RUN apk add --no-cache bash git grep findutils coreutils sed gawk curl wget openssh-client ca-certificates make
WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./

RUN npm ci --omit=dev --legacy-peer-deps

COPY --from=builder /app/dist ./dist

ENV TERM=xterm-256color

ENTRYPOINT ["node", "dist/cli.js"]
CMD []
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ test: check-deps ## Run the test suite
@$(PACKAGE_MANAGER) test

coverage: check-deps ## Run the test suite and generate a coverage report
@mkdir -p coverage/.tmp
@$(PACKAGE_MANAGER) run coverage

lint: check-deps ## Run linter checks
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ binharic

#### Running in a Container

Alternatively, you can start Binharic in a container:
Alternatively, you can run Binharic in a container:

```sh
# API keys should be available in the environment already
Expand Down
41 changes: 23 additions & 18 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import winston from "winston";
import path from "path";
import fs from "fs";

import { getConfigDir } from "./config.js";
import { getConfigDir } from "./paths.js";

let logger: winston.Logger | null = null;

function getLogger(): winston.Logger {
if (logger) {
return logger;
}
if (logger) return logger;

const LOGS_DIR = path.join(getConfigDir(), "logs");
const isTest = process.env.NODE_ENV === "test";
const isDebugMode =
process.env.DEBUG_BINHARIC !== undefined || process.env.DEBUG_TOBI !== undefined;
const logLevel = isTest ? "error" : isDebugMode ? "debug" : "info";

// Ensure logs directory exists
if (!fs.existsSync(LOGS_DIR)) {
fs.mkdirSync(LOGS_DIR, { recursive: true });
if (isTest) {
logger = winston.createLogger({
level: logLevel,
format: winston.format.json(),
transports: [],
silent: true,
});
return logger;
}

const isDebugMode =
process.env.DEBUG_BINHARIC !== undefined || process.env.DEBUG_TOBI !== undefined;
const logLevel = isDebugMode ? "debug" : "info";
const overrideLogDir = process.env.BINHARIC_LOG_DIR;
const LOGS_DIR = overrideLogDir ? overrideLogDir : path.join(getConfigDir(), "logs");
if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true });

logger = winston.createLogger({
level: logLevel,
Expand All @@ -31,18 +36,20 @@ function getLogger(): winston.Logger {
LOGS_DIR,
`binharic-${new Date().toISOString().replace(/[:.]/g, "-")}.log`,
),
maxsize: 1024 * 1024 * 5, // 5MB
maxsize: 1024 * 1024 * 5,
maxFiles: 5,
tailable: true,
}),
],
});

// Also log to console in debug mode
if (isDebugMode) {
logger.add(
new winston.transports.Console({
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
}),
);
}
Expand All @@ -54,9 +61,7 @@ const loggerProxy = new Proxy({} as winston.Logger, {
get(_, prop: string) {
const loggerInstance = getLogger();
const value = loggerInstance[prop as keyof winston.Logger];
if (typeof value === "function") {
return value.bind(loggerInstance);
}
if (typeof value === "function") return value.bind(loggerInstance);
return value;
},
});
Expand Down
15 changes: 15 additions & 0 deletions src/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import path from "path";
import os from "os";

export function getConfigDir(): string {
return path.join(os.homedir(), ".config", "binharic");
}

export function getConfigPath(): string {
return path.join(getConfigDir(), "config.json5");
}

export function getHistoryPath(): string {
return path.join(getConfigDir(), "history");
}

23 changes: 23 additions & 0 deletions tests/agent/core/loggerBehavior.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, it, expect } from "vitest";
import fs from "fs";
import path from "path";
import os from "os";

describe("Logger behavior in test mode", () => {
it("should not create log files or directories when NODE_ENV=test", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "binharic-logger-test-"));
const overrideDir = path.join(tempDir, "logs-override");

process.env.NODE_ENV = "test";
delete process.env.DEBUG_BINHARIC;
delete process.env.DEBUG_TOBI;
process.env.BINHARIC_LOG_DIR = overrideDir;

const mod = await import("@/logger.js");
const logger = mod.default;
logger.info("test message");

expect(fs.existsSync(overrideDir)).toBeFalsy();
});
});

14 changes: 6 additions & 8 deletions tests/agent/execution/escapeKeyCancelAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,20 +150,18 @@ describe("Escape Key to Cancel Agent Work", () => {
expect(mockStopAgent).toHaveBeenCalled();
});

it("should eventually transition to idle after cancel", (done) => {
it("should eventually transition to idle after cancel", async () => {
let status = "responding";

if (status === "responding") {
status = "interrupted";
}

setTimeout(() => {
if (status === "interrupted") {
status = "idle";
}
expect(status).toBe("idle");
done();
}, 100);
await new Promise((resolve) => setTimeout(resolve, 100));
if (status === "interrupted") {
status = "idle";
}
expect(status).toBe("idle");
});
});

Expand Down
10 changes: 8 additions & 2 deletions tests/agent/performance/memoryProfiling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,25 @@ describe("Memory Profiling Tests", () => {
await fs.writeFile(filePath, "x".repeat(fileSize));
}

const warmupPath = path.join(testDir, `file-0.txt`);
await fileTracker.read(warmupPath);
fileTracker.clearTracking();
forceGC();

const beforeMemory = getMemoryUsage();

for (let i = 0; i < fileCount; i++) {
const filePath = path.join(testDir, `file-${i}.txt`);
await fileTracker.read(filePath);
}

forceGC();
await new Promise((r) => setTimeout(r, 10));
forceGC();
const afterMemory = getMemoryUsage();

const memoryIncrease = afterMemory.heapUsed - beforeMemory.heapUsed;
const expectedMaxIncrease = fileSize * fileCount * 2;
const expectedMaxIncrease = fileSize * fileCount * 3;

expect(memoryIncrease).toBeLessThan(expectedMaxIncrease);
});
Expand Down Expand Up @@ -201,7 +207,7 @@ describe("Memory Profiling Tests", () => {
const afterOp = getMemoryUsage();

const memoryRetained = afterOp.heapUsed - beforeOp.heapUsed;
const maxAcceptableRetention = content.length * 2;
const maxAcceptableRetention = content.length * 3;

expect(memoryRetained).toBeLessThan(maxAcceptableRetention);
});
Expand Down
Loading