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
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"engines": {
"node": ">=20.0.0"
},
"version": "2.19.0",
"version": "2.19.1",
"dependencies": {
"@mariozechner/jiti": "^2.6.5",
"@dreb/coding-agent": "*",
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/agent-core",
"version": "2.19.0",
"version": "2.19.1",
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/ai",
"version": "2.19.0",
"version": "2.19.1",
"description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module",
"main": "./dist/index.js",
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ dreb --thinking high "Solve this complex problem"
| `DREB_SEARXNG_URL` | Base URL for SearXNG backend (default: `http://localhost:8888`) |
| `DREB_BRAVE_API_KEY` | API key for Brave search backend |
| `DREB_WEB_SEARCH_RATE_LIMIT_MS` | Minimum delay between web searches in milliseconds (default: `10000`) |
| `DREB_DEBUG` | Show debug-level messages in the TUI chat feed (default: suppressed) |
| `VISUAL`, `EDITOR` | External editor for Ctrl+G |

---
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/coding-agent",
"version": "2.19.0",
"version": "2.19.1",
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module",
"drebConfig": {
Expand Down
5 changes: 3 additions & 2 deletions packages/coding-agent/src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import type { ThinkingLevel } from "@dreb/agent-core";
import chalk from "chalk";
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config.js";
import { log } from "../core/logger.js";
import { allTools, type ToolName } from "../core/tools/index.js";

export type Mode = "text" | "json" | "rpc";
Expand Down Expand Up @@ -109,7 +110,7 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
if (name in allTools) {
validTools.push(name as ToolName);
} else {
console.error(
log.warn(
chalk.yellow(`Warning: Unknown tool "${name}". Valid tools: ${Object.keys(allTools).join(", ")}`),
);
}
Expand All @@ -120,7 +121,7 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
if (isValidThinkingLevel(level)) {
result.thinking = level;
} else {
console.error(
log.warn(
chalk.yellow(
`Warning: Invalid thinking level "${level}". Valid values: ${VALID_THINKING_LEVELS.join(", ")}`,
),
Expand Down
5 changes: 3 additions & 2 deletions packages/coding-agent/src/cli/file-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { access, readFile, stat } from "node:fs/promises";
import type { ImageContent } from "@dreb/ai";
import chalk from "chalk";
import { resolve } from "path";
import { log } from "../core/logger.js";
import { resolveReadPath } from "../core/tools/path-utils.js";
import { formatDimensionNote, resizeImage } from "../utils/image-resize.js";
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.js";
Expand Down Expand Up @@ -35,7 +36,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process
await access(absolutePath);
} catch {
// File does not exist — print a user-friendly error and exit
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
log.error(chalk.red(`Error: File not found: ${absolutePath}`));
process.exit(1);
}

Expand Down Expand Up @@ -91,7 +92,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process
text += `<file name="${absolutePath}">\n${content}\n</file>\n`;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.error(chalk.red(`Error: Could not read file ${absolutePath}: ${message}`));
log.error(chalk.red(`Error: Could not read file ${absolutePath}: ${message}`));
process.exit(1);
}
}
Expand Down
11 changes: 6 additions & 5 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
} from "./extensions/index.js";
import { checkScriptContent, extractScriptPaths, isForbiddenCommand } from "./forbidden-commands.js";
import { type GitRepoState, getGitRepoState } from "./git-repo-state.js";
import { log } from "./logger.js";
import type { BashExecutionMessage, CustomMessage } from "./messages.js";
import type { ModelRegistry } from "./model-registry.js";
import { PerformanceTracker } from "./performance-tracker.js";
Expand Down Expand Up @@ -646,7 +647,7 @@ export class AgentSession {
this._emit({ type: "message_start", message });
this._emit({ type: "message_end", message });
} catch (err) {
console.error(
log.warn(
`[subagent] Failed to deliver cancellation message for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}`,
);
}
Expand All @@ -662,13 +663,13 @@ export class AgentSession {
} else {
// Fallback: if streaming started between the isStreaming check and this call, deliver as follow-up
this.agent.prompt(message).catch((promptErr) => {
console.error(
log.warn(
`[subagent] prompt() failed for background agent ${agentId}: ${promptErr instanceof Error ? promptErr.message : String(promptErr)}`,
);
try {
this.agent.followUp(message);
} catch (followUpErr) {
console.error(
log.error(
`[subagent] followUp() also failed for background agent ${agentId}: ${followUpErr instanceof Error ? followUpErr.message : String(followUpErr)}. Background result lost.`,
);
}
Expand All @@ -684,7 +685,7 @@ export class AgentSession {
success: result.exitCode === 0,
});
} catch (emitErr) {
console.error(
log.warn(
`[subagent] background_agent_end emit failed: ${emitErr instanceof Error ? emitErr.message : String(emitErr)}`,
);
}
Expand Down Expand Up @@ -1484,7 +1485,7 @@ export class AgentSession {

const skill = this.resourceLoader.getSkills().skills.find((s) => s.name === skillName);
if (!skill) {
console.error(`Unknown skill "${skillName}" — no skill found with that name`);
log.warn(`Unknown skill "${skillName}" — no skill found with that name`);
return text;
}

Expand Down
5 changes: 3 additions & 2 deletions packages/coding-agent/src/core/buddy/buddy-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* and Telegram (activity gating + reaction budget) can use different strategies.
*/

import { log } from "../logger.js";
import { type BuddyManager, checkOllama } from "./buddy-manager.js";
import type { BuddyState } from "./buddy-types.js";

Expand Down Expand Up @@ -212,7 +213,7 @@ export class BuddyController {
} catch (err) {
if (!thinkingEnded) this.callbacks.onThinkingEnd();
this.removeContextEntry(marker);
console.error("[buddy] triggerReaction failed:", err instanceof Error ? err.message : err);
log.debug(`[buddy] triggerReaction failed: ${err instanceof Error ? err.message : String(err)}`);
}
}

Expand Down Expand Up @@ -247,7 +248,7 @@ export class BuddyController {
} catch (err) {
if (!thinkingEnded) this.callbacks.onThinkingEnd();
this.removeContextEntry(marker);
console.error("[buddy] handleNameCall failed:", err instanceof Error ? err.message : err);
log.debug(`[buddy] handleNameCall failed: ${err instanceof Error ? err.message : String(err)}`);
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/coding-agent/src/core/event-bus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventEmitter } from "node:events";
import { log } from "./logger.js";

export interface EventBus {
emit(channel: string, data: unknown): void;
Expand All @@ -20,7 +21,7 @@ export function createEventBus(): EventBusController {
try {
await handler(data);
} catch (err) {
console.error(`Event handler error (${channel}):`, err);
log.warn(`Event handler error (${channel}): ${err instanceof Error ? err.message : String(err)}`);
}
};
emitter.on(channel, safeHandler);
Expand Down
57 changes: 57 additions & 0 deletions packages/coding-agent/src/core/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Structured logger that routes messages appropriately based on mode.
*
* In interactive TUI mode (when stderr is taken over):
* - debug: suppressed unless DREB_DEBUG=1
* - warn/error: routed through writeIntercepted with level info → displayed in TUI feed
*
* In non-interactive modes (JSON, RPC, print, or before TUI starts):
* - All levels write to real stderr (the diagnostic side-channel)
*/

import { isStderrTakenOver, writeIntercepted, writeRawStderr } from "./stderr-guard.js";

export type LogLevel = "debug" | "warn" | "error";

const isDebugEnabled = (): boolean => process.env.DREB_DEBUG === "1";

export const log = {
/**
* Debug-level message. Suppressed in interactive mode unless DREB_DEBUG=1.
* Always writes to stderr in non-interactive modes.
*/
debug(message: string): void {
if (isStderrTakenOver()) {
if (isDebugEnabled()) {
writeIntercepted(message, "debug");
}
// Otherwise silently suppressed
} else {
writeRawStderr(`${message}\n`);
}
},

/**
* Warning-level message. Always displayed to the user.
* In TUI: shown in chat feed as warning. In non-interactive: written to stderr.
*/
warn(message: string): void {
if (isStderrTakenOver()) {
writeIntercepted(message, "warn");
} else {
writeRawStderr(`${message}\n`);
}
},

/**
* Error-level message. Always displayed to the user.
* In TUI: shown in chat feed as error. In non-interactive: written to stderr.
*/
error(message: string): void {
if (isStderrTakenOver()) {
writeIntercepted(message, "error");
} else {
writeRawStderr(`${message}\n`);
}
},
};
5 changes: 3 additions & 2 deletions packages/coding-agent/src/core/model-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import chalk from "chalk";
import { minimatch } from "minimatch";
import { isValidThinkingLevel } from "../cli/args.js";
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
import { log } from "./logger.js";
import type { ModelRegistry } from "./model-registry.js";

/** Default model IDs for each known provider */
Expand Down Expand Up @@ -483,7 +484,7 @@ export async function findInitialModel(options: {
modelRegistry,
});
if (resolved.error) {
console.error(chalk.red(resolved.error));
log.error(chalk.red(resolved.error));
process.exit(1);
}
if (resolved.model) {
Expand Down Expand Up @@ -559,7 +560,7 @@ export async function restoreModelFromSession(
const reason = !restoredModel ? "model no longer exists" : "no API key available";

if (shouldPrintMessages) {
console.error(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));
log.warn(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));
}

// If we already have a model, use it as fallback
Expand Down
27 changes: 25 additions & 2 deletions packages/coding-agent/src/core/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import ignore from "ignore";
import { minimatch } from "minimatch";
import { CONFIG_DIR_NAME } from "../config.js";
import { type GitSource, parseGitUrl } from "../utils/git.js";
import { log } from "./logger.js";
import { isStdoutTakenOver } from "./output-guard.js";
import type { PackageSource, SettingsManager } from "./settings-manager.js";
import { isStderrTakenOver } from "./stderr-guard.js";

const NETWORK_TIMEOUT_MS = 10000;
const UPDATE_CHECK_CONCURRENCY = 4;
Expand Down Expand Up @@ -2157,13 +2159,34 @@ export class DefaultPackageManager implements PackageManager {

private runCommand(command: string, args: string[], options?: { cwd?: string }): Promise<void> {
return new Promise((resolvePromise, reject) => {
const stderrTaken = isStderrTakenOver();
const stdoutTaken = isStdoutTakenOver();
// When TUI owns the terminal, pipe stdout/stderr to avoid corrupting the display.
// When stdout is taken over (non-interactive pipe mode), route to fd 2 (stderr) as before.
// Otherwise inherit for normal terminal output.
const stdio: import("node:child_process").StdioOptions = stderrTaken
? ["ignore", "pipe", "pipe"]
: stdoutTaken
? ["ignore", 2, 2]
: "inherit";
const child = spawn(command, args, {
cwd: options?.cwd,
stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit",
stdio,
shell: process.platform === "win32",
});
if (stderrTaken) {
child.stdout?.on("data", (chunk: Buffer) => {
log.debug(chunk.toString());
});
child.stderr?.on("data", (chunk: Buffer) => {
log.warn(chunk.toString());
});
}
child.on("error", reject);
child.on("exit", (code) => {
// Use "close" instead of "exit" to ensure all piped stdio data
// has been delivered before we resolve/reject. "exit" can fire while
// buffered data events are still pending in the pipe.
child.on("close", (code) => {
if (code === 0) {
resolvePromise();
} else {
Expand Down
Loading
Loading