diff --git a/CHANGELOG.md b/CHANGELOG.md index 978155aa2..35f9c7b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts. - Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter. - Docs: add `/help` hub, Node/npm PATH sanity guide, and installer PATH warnings (for “installed but command not found” setups). (#861) +- Docs: clarify web_search country defaults to Brave’s region choice. (#1046) — thanks @YuriNachos. - Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24). - Agents: default to no narration for routine tool calls. (#1008) — thanks @cpojer. - Tools: normalize Slack/Discord message timestamps with `timestampMs`/`timestampUtc` while keeping raw provider fields. diff --git a/docs/tools/web.md b/docs/tools/web.md index 628ca4dc6..5de9381c0 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -73,6 +73,29 @@ Search the web with Brave’s API. - `query` (required) - `count` (1–10; default from config) +- `country` (optional): 2-letter country code for region-specific results (e.g., "DE", "US", "ALL"). If omitted, Brave chooses its default region. +- `search_lang` (optional): ISO language code for search results (e.g., "de", "en", "fr") +- `ui_lang` (optional): ISO language code for UI elements + +**Examples:** + +```javascript +// German-specific search +await web_search({ + query: "TV online schauen", + count: 10, + country: "DE", + search_lang: "de" +}); + +// French search with French UI +await web_search({ + query: "actualités", + country: "FR", + search_lang: "fr", + ui_lang: "fr" +}); +``` ## web_fetch diff --git a/skills/openai-image-gen/scripts/gen.py b/skills/openai-image-gen/scripts/gen.py index 8024b055b..f82831b84 100644 --- a/skills/openai-image-gen/scripts/gen.py +++ b/skills/openai-image-gen/scripts/gen.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import argparse -import base64 import datetime as dt import json import os @@ -96,8 +95,8 @@ def request_images( if model != "dall-e-2": args["quality"] = quality - if model.startswith("dall-e"): - args["response_format"] = "b64_json" + # Note: response_format no longer supported by OpenAI Images API + # dall-e models now return URLs by default if model.startswith("gpt-image"): if background: @@ -212,12 +211,19 @@ def main() -> int: args.output_format, args.style, ) - b64 = res.get("data", [{}])[0].get("b64_json") - if not b64: + # OpenAI Images API now returns URLs by default + image_url = res.get("data", [{}])[0].get("url") + if not image_url: raise RuntimeError(f"Unexpected response: {json.dumps(res)[:400]}") - image_bytes = base64.b64decode(b64) + + # Download image from URL filename = f"{idx:03d}-{slugify(prompt)[:40]}.{file_ext}" - (out_dir / filename).write_bytes(image_bytes) + filepath = out_dir / filename + try: + urllib.request.urlretrieve(image_url, filepath) + except urllib.error.URLError as e: + raise RuntimeError(f"Failed to download image from {image_url}: {e}") from e + items.append({"prompt": prompt, "file": filename}) (out_dir / "prompts.json").write_text(json.dumps(items, indent=2), encoding="utf-8") diff --git a/src/agents/tools/web-tools.enabled-defaults.test.ts b/src/agents/tools/web-tools.enabled-defaults.test.ts index b49d69da8..979a161d2 100644 --- a/src/agents/tools/web-tools.enabled-defaults.test.ts +++ b/src/agents/tools/web-tools.enabled-defaults.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createWebFetchTool, createWebSearchTool } from "./web-tools.js"; @@ -21,3 +21,71 @@ describe("web tools defaults", () => { expect(tool?.name).toBe("web_search"); }); }); + +describe("web_search country and language parameters", () => { + const priorFetch = global.fetch; + + beforeEach(() => { + vi.stubEnv("BRAVE_API_KEY", "test-key"); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + // @ts-expect-error global fetch cleanup + global.fetch = priorFetch; + }); + + it("should pass country parameter to Brave API", async () => { + const mockFetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ web: { results: [] } }), + } as Response), + ); + // @ts-expect-error mock fetch + global.fetch = mockFetch; + + const tool = createWebSearchTool({ config: undefined, sandboxed: true }); + expect(tool).not.toBeNull(); + + await tool?.execute?.(1, { query: "test", country: "DE" }); + + expect(mockFetch).toHaveBeenCalled(); + const url = new URL(mockFetch.mock.calls[0][0] as string); + expect(url.searchParams.get("country")).toBe("DE"); + }); + + it("should pass search_lang parameter to Brave API", async () => { + const mockFetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ web: { results: [] } }), + } as Response), + ); + // @ts-expect-error mock fetch + global.fetch = mockFetch; + + const tool = createWebSearchTool({ config: undefined, sandboxed: true }); + await tool?.execute?.(1, { query: "test", search_lang: "de" }); + + const url = new URL(mockFetch.mock.calls[0][0] as string); + expect(url.searchParams.get("search_lang")).toBe("de"); + }); + + it("should pass ui_lang parameter to Brave API", async () => { + const mockFetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ web: { results: [] } }), + } as Response), + ); + // @ts-expect-error mock fetch + global.fetch = mockFetch; + + const tool = createWebSearchTool({ config: undefined, sandboxed: true }); + await tool?.execute?.(1, { query: "test", ui_lang: "de" }); + + const url = new URL(mockFetch.mock.calls[0][0] as string); + expect(url.searchParams.get("ui_lang")).toBe("de"); + }); +}); diff --git a/src/agents/tools/web-tools.ts b/src/agents/tools/web-tools.ts index 38025352e..d12e49c20 100644 --- a/src/agents/tools/web-tools.ts +++ b/src/agents/tools/web-tools.ts @@ -48,6 +48,22 @@ const WebSearchSchema = Type.Object({ maximum: MAX_SEARCH_COUNT, }), ), + country: Type.Optional( + Type.String({ + description: + "2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'.", + }), + ), + search_lang: Type.Optional( + Type.String({ + description: "ISO language code for search results (e.g., 'de', 'en', 'fr').", + }), + ), + ui_lang: Type.Optional( + Type.String({ + description: "ISO language code for UI elements.", + }), + ), }); const WebFetchSchema = Type.Object({ @@ -291,8 +307,13 @@ async function runWebSearch(params: { timeoutSeconds: number; cacheTtlMs: number; provider: (typeof SEARCH_PROVIDERS)[number]; + country?: string; + search_lang?: string; + ui_lang?: string; }): Promise> { - const cacheKey = normalizeCacheKey(`${params.provider}:${params.query}:${params.count}`); + const cacheKey = normalizeCacheKey( + `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}` + ); const cached = readCache(SEARCH_CACHE, cacheKey); if (cached) return { ...cached.value, cached: true }; @@ -304,6 +325,15 @@ async function runWebSearch(params: { const url = new URL(BRAVE_SEARCH_ENDPOINT); url.searchParams.set("q", params.query); url.searchParams.set("count", String(params.count)); + if (params.country) { + url.searchParams.set("country", params.country); + } + if (params.search_lang) { + url.searchParams.set("search_lang", params.search_lang); + } + if (params.ui_lang) { + url.searchParams.set("ui_lang", params.ui_lang); + } const res = await fetch(url.toString(), { method: "GET", @@ -424,7 +454,7 @@ export function createWebSearchTool(options?: { label: "Web Search", name: "web_search", description: - "Search the web using Brave Search API. Returns titles, URLs, and snippets for fast research.", + "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.", parameters: WebSearchSchema, execute: async (_toolCallId, args) => { const apiKey = resolveSearchApiKey(search); @@ -435,6 +465,9 @@ export function createWebSearchTool(options?: { const query = readStringParam(params, "query", { required: true }); const count = readNumberParam(params, "count", { integer: true }) ?? search?.maxResults ?? undefined; + const country = readStringParam(params, "country"); + const search_lang = readStringParam(params, "search_lang"); + const ui_lang = readStringParam(params, "ui_lang"); const result = await runWebSearch({ query, count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT), @@ -442,6 +475,9 @@ export function createWebSearchTool(options?: { timeoutSeconds: resolveTimeoutSeconds(search?.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS), cacheTtlMs: resolveCacheTtlMs(search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES), provider: resolveSearchProvider(search), + country, + search_lang, + ui_lang, }); return jsonResult(result); }, diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index e28ea4dea..35ee66b71 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -278,7 +278,10 @@ export async function initSessionState(params: { ctx.MessageThreadId, ); } - sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry }; + // Fresh start for new sessions - don't inherit old state like compactionCount + sessionStore[sessionKey] = isNewSession + ? sessionEntry + : { ...sessionStore[sessionKey], ...sessionEntry }; await updateSessionStore(storePath, (store) => { if (groupResolution?.legacyKey && groupResolution.legacyKey !== sessionKey) { if (store[groupResolution.legacyKey] && !store[sessionKey]) { @@ -286,7 +289,10 @@ export async function initSessionState(params: { } delete store[groupResolution.legacyKey]; } - store[sessionKey] = { ...store[sessionKey], ...sessionEntry }; + // Fresh start for new sessions - don't inherit old state like compactionCount + store[sessionKey] = isNewSession + ? sessionEntry + : { ...store[sessionKey], ...sessionEntry }; }); const sessionCtx: TemplateContext = { diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index c8b5e403a..d5de85b47 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -135,8 +135,10 @@ async function saveSessionStoreUnlocked( const tmp = `${storePath}.${process.pid}.${crypto.randomUUID()}.tmp`; try { - await fs.promises.writeFile(tmp, json, "utf-8"); + await fs.promises.writeFile(tmp, json, { mode: 0o600, encoding: "utf-8" }); await fs.promises.rename(tmp, storePath); + // Ensure permissions are set even if rename loses them + await fs.promises.chmod(storePath, 0o600); } catch (err) { const code = err && typeof err === "object" && "code" in err @@ -148,7 +150,8 @@ async function saveSessionStoreUnlocked( // Best-effort: try a direct write (recreating the parent dir), otherwise ignore. try { await fs.promises.mkdir(path.dirname(storePath), { recursive: true }); - await fs.promises.writeFile(storePath, json, "utf-8"); + await fs.promises.writeFile(storePath, json, { mode: 0o600, encoding: "utf-8" }); + await fs.promises.chmod(storePath, 0o600); } catch (err2) { const code2 = err2 && typeof err2 === "object" && "code" in err2