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
4 changes: 2 additions & 2 deletions apps/docs/src/components/landing/Capabilities.astro
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ const capabilities: Capability[] = [
href: "/loop/gate-floor/",
},
{
title: "Free web access",
body: "Local web_fetch + web_search — no API key, fully on-machine.",
title: "Keyless web research",
body: "Package metadata, docs, search, fetch, and local browser reads — no vendor API key.",
href: "/integrations/web-tools/",
},
];
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/agent/model-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ One approved task can involve many agent cycles until the gate passes or tsforge
| Navigation | `search`, `symbol_search`, `find_references`, `type_at`, `diagnostics`, `rename_symbol`, `move_file`, `organize_imports` | existing-code repos |
| Git context | `git_context` | existing-code repos (read-only: diff/log/blame/show to scope a change); `TSFORGE_NO_GIT_TOOL=1` to withhold |
| Web | `scaffold_web`, `scaffold_ui`, `scaffold_routes`, `add_dependency` | web builds |
| Web access | `web_fetch`, `web_search` | when `TSFORGE_WEB=1` (free, local — no API key) |
| Web research | `package_info`, `package_docs`, `web_fetch`, `web_search`, `web_browse` | when `TSFORGE_WEB=1` (no required API keys or paid browser/search service) |
| Control | `yield_status` | end turn with a summary |

On greenfield specs, navigation tools are often withheld so the model focuses on creating files instead of exploring an empty tree. See [TypeScript language server](/lsp/typescript-server/).
Expand Down
25 changes: 19 additions & 6 deletions apps/docs/src/content/docs/integrations/web-tools.mdx
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
---
title: Web access (free, local)
description: "Opt-in web_fetch + web_search tools — no API key, no paid service. Fully local extraction, DuckDuckGo by default, SearXNG for full privacy."
title: Web research (no API keys)
description: "Opt-in web_fetch, web_search, package_info, package_docs, and web_browse tools — no paid search/browser API, no required service key."
---

Set `TSFORGE_WEB=1` to give the agent two read-only web tools. They're **free and fully local** — no API key, no paid search service, nothing leaves your machine except the fetch itself. Off by default, so a run without the flag has no network reach beyond your model endpoint.
Set `TSFORGE_WEB=1` to give the agent read-only internet research tools. They're built for **no required API keys and no paid vendor coupling**: npm metadata comes from the configured registry, search defaults to DuckDuckGo's keyless HTML endpoint, pages are extracted locally, and browser rendering uses local Playwright/Chromium when available. Off by default, so a run without the flag has no network reach beyond your model endpoint.

```bash
TSFORGE_WEB=1 tsforge "update the deprecated API call — check the library's current docs"
```

## The tools

- **`package_info`** — read current npm package metadata from the configured registry: latest dist-tag, versions, deprecation status, peer deps, homepage, repository, and dependency names.
- **`package_docs`** — read package docs without a docs API: local `node_modules` README/package metadata/types first, then the npm registry README when needed.
- **`web_fetch`** — retrieve a URL and return readable markdown. The page is fetched and the content extracted **on-machine** (no third-party reader API), so you get clean text without shipping the URL to a service.
- **`web_search`** — discover sources for a query. Defaults to DuckDuckGo's keyless HTML endpoint; point it at a self-hosted [SearXNG](https://docs.searxng.org/) instance for full privacy and control:
- **`web_browse`** — open a public URL in local headless Chromium via Playwright and return rendered visible text, final URL, title, and links. Use it for JavaScript-rendered docs that `web_fetch` cannot see. No hosted browser service.
- **`web_search`** — discover sources for a query. It returns public result URLs only, supports `recency` (`day`, `month`, `year`), `domains` for official-site scoping, and `maxResults` up to 20. Defaults to DuckDuckGo's keyless HTML endpoint.

SearXNG is **not bundled or started by tsforge**. If you run your own SearXNG service (Docker, Compose, or any deployment), point `TSFORGE_SEARXNG_URL` at it for full privacy/control:

```bash
TSFORGE_WEB=1 TSFORGE_SEARXNG_URL=http://localhost:8888 tsforge
```

For current TypeScript/library work, ask the agent to search the official host first, then fetch the source it picks:

```text
Check the current TanStack Query docs before changing this hook. Use domain-scoped web search if needed.
```

## Why opt-in

The tools are read-only and offline-safe, but web access is still more reach than the agent has by default — so it's a deliberate flag, not an always-on capability. Under a policy mode that denies `network` (e.g. `ci`), the tools are unavailable even with the flag set. See [Permissions & policy](/guardrails/policy/).

| Env var | Default | Effect |
| --- | --- | --- |
| `TSFORGE_WEB` | off | enable `web_fetch` + `web_search` (`=1`) |
| `TSFORGE_SEARXNG_URL` | unset | route `web_search` to a self-hosted SearXNG instance |
| `TSFORGE_WEB` | off | enable keyless research tools (`=1`) |
| `TSFORGE_NPM_REGISTRY` | npm registry | registry used by `package_info` / `package_docs` |
| `TSFORGE_SEARXNG_URL` | unset | route `web_search` to a SearXNG instance you already run |
| `TSFORGE_WEB_SEARCH_BACKEND` | auto | `duckduckgo` or `searxng`; `searxng` fails closed if no SearXNG URL is set |

→ [Environment variables](/reference/flags/) · [MCP servers](/integrations/mcp/) · [Permissions & policy](/guardrails/policy/)
10 changes: 6 additions & 4 deletions apps/docs/src/content/docs/reference/flags.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ description: Canonical list of every TSFORGE_* environment variable.
| `TSFORGE_FORCE_TOOLS` | off | force tool_choice required (`=1`) |
| `TSFORGE_SIMPLICITY` | off | shortest-solution guidance, scratch non-web (`=1`) |
| `TSFORGE_TDD` | ON (`≠ "0"`) | test-first guidance + `test-sibling-required` is an error on changed logic files (`=0` to opt out) |
| `TSFORGE_WEB` | off | free, local web access — the `web_fetch` + `web_search` tools (`=1`) |
| `TSFORGE_WEB` | off | keyless web/package research tools (`=1`) |
| `TSFORGE_CONTRACT` | off | experimental per-feature [contract negotiation](/loop/greenfield/) in greenfield builds (`=1`) |
| `TSFORGE_NO_UPDATE_CHECK` | off | silence the startup "update available" check (`=1`) |
| `TSFORGE_NO_GIT_TOOL` | off | withhold the `git_context` tool (`=1`) |
Expand All @@ -33,12 +33,14 @@ Offered only when there is existing code to inspect (greenfield scratch builds h

## Web access

Opt-in, free, and fully local — no API key, no paid service. `TSFORGE_WEB=1` adds two read-only tools: `web_fetch` (retrieve a URL → readable markdown, extracted on-machine) and `web_search` (discover sources). Search defaults to DuckDuckGo's keyless HTML endpoint; point at a self-hosted [SearXNG](https://docs.searxng.org/) instance for full privacy/control. Full guide: [Web access](/integrations/web-tools/).
Opt-in, free, and no required service keys. `TSFORGE_WEB=1` adds read-only research tools: `package_info`, `package_docs`, `web_fetch`, `web_search`, and `web_browse`. Search defaults to DuckDuckGo's keyless HTML endpoint. SearXNG is not bundled; set `TSFORGE_SEARXNG_URL` only when you already run a SearXNG service. Full guide: [Web access](/integrations/web-tools/).

| Variable | Default | Toggles |
| --- | --- | --- |
| `TSFORGE_WEB` | off | enable `web_fetch` + `web_search` (`=1`) |
| `TSFORGE_SEARXNG_URL` | unset | route `web_search` to a self-hosted SearXNG instance (e.g. `http://localhost:8888`) |
| `TSFORGE_WEB` | off | enable keyless web/package research tools (`=1`) |
| `TSFORGE_NPM_REGISTRY` | npm registry | registry used by `package_info` / `package_docs` |
| `TSFORGE_SEARXNG_URL` | unset | route `web_search` to a SearXNG instance you already run (e.g. `http://localhost:8888`) |
| `TSFORGE_WEB_SEARCH_BACKEND` | auto | `duckduckgo` or `searxng`; `searxng` fails closed if no SearXNG URL is set |

## Opt-in gate oracles

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/reference/roadmap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ TypeScript coding harness for web projects — `packages/core` for the loop and
**The agent surface**
- **Functional review** (`tsforge review`, gate-aware) · **workspace map** (`tsforge map`) · **pre-edit scout** · **declarative recipes** (`tsforge run`) · **plan mode** · **cross-session memory**.
- **Permissions & policy** — 6 modes, deny-first config rules, and a run **ledger** summarized by `tsforge trace`. See [Permissions & policy](/guardrails/policy/) · [Trace a run](/observability/trace/).
- **Free, local web access** — opt-in `web_fetch` + `web_search`, no API key. See [Web access](/integrations/web-tools/).
- **Keyless web research** — opt-in package metadata/docs, search, fetch, and local browser reads with no required API key. See [Web access](/integrations/web-tools/).
- **Web scaffolding** — Vite + React + shadcn + TanStack, ending in a real browser smoke test. See [Web scaffolding](/scaffold/web/).

**Models & observability**
Expand Down
107 changes: 106 additions & 1 deletion packages/core/src/agent/agent.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ export const TOOL_NAME = {
scaffoldRoutes: "scaffold_routes",
scaffoldWeb: "scaffold_web",
addDependency: "add_dependency",
packageInfo: "package_info",
packageDocs: "package_docs",
webFetch: "web_fetch",
webSearch: "web_search",
webBrowse: "web_browse",
yieldStatus: "yield_status",
} as const;

Expand All @@ -41,11 +44,14 @@ export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
// git_context only inspects history/diffs — no workspace mutation — so it is a
// plan-mode tool too (scope a review/fix while planning, before any edit).
TOOL_NAME.gitContext,
TOOL_NAME.packageInfo,
TOOL_NAME.packageDocs,
// Web tools are read-only (no workspace mutation), so they're usable in plan
// mode too — research while planning. Network egress here is structured and
// opt-in (TSFORGE_WEB), unlike the raw `run` curl path plan mode blocks.
TOOL_NAME.webFetch,
TOOL_NAME.webSearch,
TOOL_NAME.webBrowse,
]);

/** The model's own decision to start a from-scratch WEB app: scaffolds the stack
Expand Down Expand Up @@ -254,17 +260,116 @@ export const WEB_SEARCH_TOOL = {
function: {
name: TOOL_NAME.webSearch,
description:
"Search the web and get back ranked result titles, URLs, and snippets. Use it to DISCOVER sources when you don't already have a URL, then `web_fetch` the most relevant one. Free and keyless — DuckDuckGo by default, or a self-hosted SearXNG instance via the TSFORGE_SEARXNG_URL env var.",
"Search the web and get back ranked public result titles, URLs, and snippets. Use it to DISCOVER current sources when you don't already have a URL, then `web_fetch` the most relevant one. Supports `recency` for fresh docs/news, `domains` for official-site scoping, and `maxResults` for broader source discovery. Free and keyless — DuckDuckGo by default, or a user-run SearXNG instance via TSFORGE_SEARXNG_URL. Set TSFORGE_WEB_SEARCH_BACKEND=searxng to fail closed instead of falling back to DuckDuckGo.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "the search query" },
recency: {
type: "string",
enum: ["day", "month", "year"],
description:
"optional freshness window for fast-moving topics and current docs",
},
domains: {
type: "array",
items: { type: "string" },
description:
"optional public hostnames to search within, e.g. ['typescriptlang.org', 'nodejs.org']",
},
maxResults: {
type: "number",
description:
"optional result cap (default 8, maximum 20) when comparing multiple sources",
},
},
required: ["query"],
},
},
};

export const WEB_BROWSE_TOOL = {
type: "function",
function: {
name: TOOL_NAME.webBrowse,
description:
"Open a public URL in a local headless Chromium browser via Playwright and return rendered visible text, final URL, title, and links. Use it when docs/sites require JavaScript or when web_fetch misses content. No hosted browser service or API key.",
parameters: {
type: "object",
properties: {
url: {
type: "string",
description: "absolute http(s) URL to open in the local browser",
},
waitMs: {
type: "number",
description:
"optional extra wait after DOMContentLoaded for client-rendered docs (default 750, max 10000)",
},
maxChars: {
type: "number",
description: "optional cap on returned visible text (default 10000)",
},
},
required: ["url"],
},
},
};

export const PACKAGE_INFO_TOOL = {
type: "function",
function: {
name: TOOL_NAME.packageInfo,
description:
"Read current npm package metadata from the configured npm registry with no API key: latest dist-tag, versions, deprecation, peer deps, homepage, repository, and dependency names. Use before installing or coding against a package API.",
parameters: {
type: "object",
properties: {
package: {
type: "string",
description:
"one npm package name, optionally @versioned, e.g. 'zod', 'react@19', '@tanstack/react-query'",
},
maxChars: {
type: "number",
description: "optional cap on returned characters (default 12000)",
},
},
required: ["package"],
},
},
};

export const PACKAGE_DOCS_TOOL = {
type: "function",
function: {
name: TOOL_NAME.packageDocs,
description:
"Read package documentation with no paid service: local node_modules README/package.json/types first, then npm registry README when needed. Use this for version-aware package docs before guessing APIs.",
parameters: {
type: "object",
properties: {
package: {
type: "string",
description:
"one npm package name, optionally @versioned, e.g. 'zod@4' or '@tanstack/react-query'",
},
source: {
type: "string",
enum: ["auto", "local", "registry"],
description:
"auto prefers installed local docs, local refuses network, registry uses npm metadata",
},
maxChars: {
type: "number",
description: "optional cap on returned characters (default 12000)",
},
},
required: ["package"],
},
},
};

/**
* Semantic + search tools backed by the in-process TypeScript LanguageService
* (+ ripgrep). Read-only tools (find_references, type_at, symbol_search,
Expand Down
32 changes: 29 additions & 3 deletions packages/core/src/loop/prompt/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ export function buildSystem(conventions: IConventions): string {
* don't thread conventions; the dynamic path uses {@link buildSystem}. */
export const SYSTEM = buildSystem(DEFAULT_CONVENTIONS);

export function buildWebResearchGuidance(): string {
return [
"WEB RESEARCH — keyless internet/package tools are enabled:",
" • Package version/install question? Use `package_info` for npm registry",
" dist-tags, versions, deprecations, peer deps, homepage, and repo.",
" • Package API/docs question? Use `package_docs` first; it reads installed",
" node_modules docs/types before falling back to the npm registry README.",
" • Unknown public source? Use `web_search`, preferring official hosts with",
" `domains` and `recency` (`day`/`month`/`year`) for fast-moving topics.",
" • Known static page? Use `web_fetch`. JS-rendered docs/site? Use",
" `web_browse`, which opens the URL in local Chromium via Playwright.",
" • Do not guess APIs from memory when the user asks for latest/current info.",
].join("\n");
}

/** Appended to SYSTEM for from-scratch, NON-web utility builds when the simplicity
* flag is on. Pushes the model toward the shortest correct solution — the axis the
* gate is blind to (it checks correctness, never concision). Carve-outs keep it
Expand Down Expand Up @@ -104,6 +119,10 @@ export function buildSystemPrompt(
const webish = stack !== undefined && isWebStack(stack);
const blocks: string[] = [buildSystem(conventions)];

if (flags.webTools()) {
blocks.push(buildWebResearchGuidance());
}

// Simplicity: from-scratch, non-web only (an A/B-gated concision push).
if (flags.simplicity() && !hasExistingCode && !webish) {
blocks.push(buildScratchSimplicityGuidance(conventions));
Expand All @@ -128,8 +147,7 @@ export function buildSystemPrompt(
export function buildChatSystem(conventions: IConventions): string {
const naming = interfaceNamingPhrase(conventions);
const namingPart = naming === null ? "" : `${naming}; `;

return [
const lines = [
"You are tsforge, an expert TypeScript coding assistant. You are launched inside a repository, but NOT every request is about that repository. The user talks to you; you help by answering, and by inspecting/changing code with your tools.",
"Tools: `read` (inspect a file), `run` (execute any shell command — `ls`, `rg`, tests, `tsc`), `edit` (replace an exact, unique snippet), `create` (a new file).",
"File paths are RELATIVE to the workspace root: use `tsconfig.json` or `src/app.ts` — never an absolute path, and never repeat the workspace folder in the path.",
Expand All @@ -138,7 +156,15 @@ export function buildChatSystem(conventions: IConventions): string {
"Be decisive, not exhaustive. When you do investigate, a few targeted reads beat reading everything — as soon as you can answer or act, STOP calling tools and reply.",
"For a QUESTION about the repo, investigate briefly then give a concise, concrete answer (cite specific files/symbols; offer your top few recommendations, not a survey). For a CHANGE, make it with `edit`/`create`, verify by `run`ning the tests or `tsc`, then briefly state what you did.",
`When you write code, use strict TypeScript: ${namingPart}\`===\`; no \`var\`; never the non-null \`!\` (guard index access: \`const x = arr[i]; if (x === undefined) {…}\`); no \`any\`/\`as\` (type parameters); explicit boolean conditions.`,
].join("\n");
];

if (flags.webTools()) {
lines.push(
"Web tools are enabled: use `package_info` for latest npm metadata, `package_docs` for package APIs, `web_search` to discover current sources, `web_fetch` for static pages, and `web_browse` for JS-rendered pages. Prefer official sources; use `domains`, `recency`, and `maxResults` when searching."
);
}

return lines.join("\n");
}

/** Default-conventions interactive prompt (back-compat constant). */
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/loop/tools/execute-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { doScaffoldWeb } from "./scaffold-web";
import { doAddDependency } from "./add-dependency";
import { doWebFetch } from "./web-fetch";
import { doWebSearch } from "./web-search";
import { doWebBrowse } from "./web-browse";
import { doPackageInfo, doPackageDocs } from "./package-info";
import { reject, type IToolContext } from "./tool-context";
import {
classifyAction,
Expand Down Expand Up @@ -45,8 +47,11 @@ const HANDLERS: Record<ToolName, ToolHandler> = {
[TOOL_NAME.scaffoldRoutes]: doScaffoldRoutes,
[TOOL_NAME.scaffoldWeb]: doScaffoldWeb,
[TOOL_NAME.addDependency]: doAddDependency,
[TOOL_NAME.packageInfo]: doPackageInfo,
[TOOL_NAME.packageDocs]: doPackageDocs,
[TOOL_NAME.webFetch]: doWebFetch,
[TOOL_NAME.webSearch]: doWebSearch,
[TOOL_NAME.webBrowse]: doWebBrowse,
// yield_status is intercepted by the Session BEFORE tool dispatch (it ends the
// turn); this handler only fires if one slips through with other calls.
[TOOL_NAME.yieldStatus]: () =>
Expand Down
Loading
Loading