diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47e2550435..a5e4e828a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,7 +75,7 @@ jobs: - name: Configure git identity run: | - git config --global user.email "bot@code.redrob.ai" + git config --global user.email "44862514+savagemanage@users.noreply.github.com" git config --global user.name "redrob" - name: Run engine tests @@ -109,7 +109,7 @@ jobs: - name: Configure git identity run: | - git config --global user.email "bot@code.redrob.ai" + git config --global user.email "44862514+savagemanage@users.noreply.github.com" git config --global user.name "redrob" # Not sharded: 933 tests in ~22s. Splitting it would cost more in runner setup than @@ -122,6 +122,12 @@ jobs: working-directory: packages/client run: bun run check:generated + # The JSON Schemas the Console serves (config.json, tui.json, theme.json) are committed under + # packages/redrob/schema and regenerated from the live Effect schemas; a stale copy fails here. + - name: Check generated schemas + working-directory: packages/redrob + run: bun run check:generated + httpapi: name: httpapi (linux) runs-on: ubuntu-latest @@ -148,7 +154,7 @@ jobs: - name: Configure git identity run: | - git config --global user.email "bot@code.redrob.ai" + git config --global user.email "44862514+savagemanage@users.noreply.github.com" git config --global user.name "redrob" # Its own job rather than a step after the tests: ~1m38s that has no reason to sit @@ -224,7 +230,7 @@ jobs: - name: Configure git identity run: | - git config --global user.email "bot@code.redrob.ai" + git config --global user.email "44862514+savagemanage@users.noreply.github.com" git config --global user.name "redrob" - name: Run engine tests @@ -259,7 +265,7 @@ jobs: - name: Configure git identity run: | - git config --global user.email "bot@code.redrob.ai" + git config --global user.email "44862514+savagemanage@users.noreply.github.com" git config --global user.name "redrob" - name: Run core tests diff --git a/.redrob/redrob.jsonc b/.redrob/redrob.jsonc index b0a73724ca..aaf8b6c1a7 100644 --- a/.redrob/redrob.jsonc +++ b/.redrob/redrob.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/config.json", + "$schema": "https://console.redrob.ai/code/config.json", "provider": {}, "permission": {}, "references": { diff --git a/github/README.md b/github/README.md index aaaec38de1..cd6a1d0837 100644 --- a/github/README.md +++ b/github/README.md @@ -56,12 +56,11 @@ Run the following command in the terminal from your GitHub repo: Redrob Code github install ``` -This will walk you through installing the GitHub app, creating the workflow, and setting up secrets. +This will walk you through creating the workflow and setting up secrets. The agent acts with the repository's own `GITHUB_TOKEN`; there is no GitHub App to install. ### Manual Setup -1. Install the GitHub app https://github.com/apps/redrob-agent. Make sure it is installed on the target repository. -2. Add the following workflow file to `.github/workflows/redrob.yml` in your repo. Set the appropriate `model` and required API keys in `env`. +1. Add the following workflow file to `.github/workflows/redrob.yml` in your repo. Set the appropriate `model` and required API keys in `env`. ```yml name: Redrob Code @@ -79,7 +78,9 @@ This will walk you through installing the GitHub app, creating the workflow, and contains(github.event.comment.body, '/redrob') runs-on: ubuntu-latest permissions: - id-token: write + contents: write + pull-requests: write + issues: write steps: - name: Checkout repository uses: actions/checkout@v6 @@ -91,13 +92,14 @@ This will walk you through installing the GitHub app, creating the workflow, and uses: redrob-labs/redrob-code/github@latest env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: model: anthropic/claude-sonnet-4-20250514 - use_github_token: true + token: ${{ secrets.GITHUB_TOKEN }} ``` -3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys. + `token` defaults to the workflow's own `GITHUB_TOKEN`; pass a personal access token instead when the agent must act as a user (for example to trigger other workflows with its pushes). + +2. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys. ## Support @@ -127,7 +129,7 @@ To test locally: - `MODEL`: The model used by Redrob Code. Same as the `MODEL` defined in the GitHub workflow. - `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow. - `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment. - - `MOCK_TOKEN`: A GitHub personal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens). + - `MOCK_TOKEN`: A GitHub personal access token. The agent acts with it directly (comments, pushes) and uses it to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens). - `MOCK_EVENT`: Mock GitHub event payload (see templates below). - `/path/to/redrob`: Path to your cloned Redrob Code repo. `bun /path/to/redrob/github/index.ts` runs your local version of `Redrob Code`. diff --git a/github/action.yml b/github/action.yml index a41d4170b9..b26281ee3e 100644 --- a/github/action.yml +++ b/github/action.yml @@ -21,10 +21,10 @@ inputs: description: "Custom prompt to override the default prompt" required: false - use_github_token: - description: "Use GITHUB_TOKEN directly instead of Redrob Code App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var." + token: + description: "GitHub token the agent acts with: the workflow's own GITHUB_TOKEN (the default) or a personal access token with contents, pull-requests and issues write access. There is no GitHub App or token exchange; this token is the only credential." required: false - default: "false" + default: ${{ github.token }} mentions: description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/redrob,/rr,/opencode,/oc' -- the upstream phrases are kept for compatibility" @@ -34,10 +34,6 @@ inputs: description: "Model variant for provider-specific reasoning effort (e.g., high, max, minimal)" required: false - oidc_base_url: - description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.code.redrob.ai" - required: false - runs: using: "composite" steps: @@ -73,7 +69,6 @@ runs: AGENT: ${{ inputs.agent }} SHARE: ${{ inputs.share }} PROMPT: ${{ inputs.prompt }} - USE_GITHUB_TOKEN: ${{ inputs.use_github_token }} + GITHUB_TOKEN: ${{ inputs.token }} MENTIONS: ${{ inputs.mentions }} VARIANT: ${{ inputs.variant }} - OIDC_BASE_URL: ${{ inputs.oidc_base_url }} diff --git a/github/index.ts b/github/index.ts index 09f1b15812..564bb196ae 100644 --- a/github/index.ts +++ b/github/index.ts @@ -144,7 +144,8 @@ let octoGraph: typeof graphql let commentId: number let gitConfig: string let session: { id: string; title: string; version: string } -let shareId: string | undefined +// The share service answers with the public URL of a shared session; nothing here constructs one. +let shareUrl: string | undefined let exitCode = 0 type PromptFiles = Awaited>["promptFiles"] @@ -153,7 +154,7 @@ try { assertPayloadKeyword() await assertRedrobConnected() - accessToken = await getAccessToken() + accessToken = getAccessToken() octoRest = new Octokit({ auth: accessToken }) octoGraph = graphql.defaults({ headers: { authorization: `token ${accessToken}` }, @@ -170,15 +171,15 @@ try { const repoData = await fetchRepo() session = await client.session.create().then((r) => r.data) await subscribeSessionEvents() - shareId = await (async () => { + shareUrl = await (async () => { if (useEnvShare() === false) return if (!useEnvShare() && repoData.data.private) return - await client.session.share({ path: session }) - return session.id.slice(-8) + const shared = await client.session.share({ path: session }) + return shared.data.share?.url })() console.log("Redrob Code session", session.id) - if (shareId) { - console.log("Share link:", `${useShareUrl()}/s/${shareId}`) + if (shareUrl) { + console.log("Share link:", shareUrl) } // Handle 3 cases @@ -196,7 +197,7 @@ try { const summary = await summarize(response) await pushToLocalBranch(summary) } - const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`)) + const hasShared = prData.comments.nodes.some((c) => shareUrl !== undefined && c.body.includes(shareUrl)) await updateComment(`${response}${footer({ image: !hasShared })}`) } // Fork PR @@ -208,7 +209,7 @@ try { const summary = await summarize(response) await pushToForkBranch(summary, prData) } - const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`)) + const hasShared = prData.comments.nodes.some((c) => shareUrl !== undefined && c.body.includes(shareUrl)) await updateComment(`${response}${footer({ image: !hasShared })}`) } } @@ -248,7 +249,6 @@ try { } finally { server.close() await restoreGitConfig() - await revokeAppToken() } process.exit(exitCode) @@ -386,42 +386,24 @@ function useIssueId() { return payload.issue.number } -function useShareUrl() { - return isMock() ? "https://dev.code.redrob.ai" : "https://code.redrob.ai" -} - -async function getAccessToken() { - const { repo } = useContext() +// The action authenticates with a token the workflow hands it: `${{ secrets.GITHUB_TOKEN }}` or a +// PAT. There is no Redrob GitHub App and no token-exchange service behind it, so a run without a +// token cannot do anything useful and stops here with the fix spelled out. +function getAccessToken() { const envToken = useEnvGithubToken() if (envToken) return envToken - - let response if (isMock()) { - response = await fetch("https://api.code.redrob.ai/exchange_github_app_token_with_pat", { - method: "POST", - headers: { - Authorization: `Bearer ${useEnvMock().mockToken}`, - }, - body: JSON.stringify({ owner: repo.owner, repo: repo.repo }), - }) - } else { - const oidcToken = await core.getIDToken("opencode-github-action") - response = await fetch("https://api.code.redrob.ai/exchange_github_app_token", { - method: "POST", - headers: { - Authorization: `Bearer ${oidcToken}`, - }, - }) + const { mockToken } = useEnvMock() + if (mockToken) return mockToken } - - if (!response.ok) { - const responseJson = (await response.json()) as { error?: string } - throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`) - } - - const responseJson = (await response.json()) as { token: string } - return responseJson.token + throw new Error( + [ + "No GitHub token was provided.", + "Pass one to the action: `with: { token: ${{ secrets.GITHUB_TOKEN }} }` (or a personal access token with", + "`contents: write`, `pull-requests: write` and `issues: write`).", + ].join(" "), + ) } async function createComment() { @@ -674,7 +656,7 @@ async function chat(text: string, files: PromptFiles = []) { return match.text } -async function configureGit(appToken: string) { +async function configureGit(token: string) { // Do not change git config when running locally if (isMock()) return @@ -683,7 +665,7 @@ async function configureGit(appToken: string) { const ret = await $`git config --local --get ${config}` gitConfig = ret.stdout.toString().trim() - const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64") + const newCredentials = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64") await $`git config --local --unset-all ${config}` await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"` @@ -875,16 +857,17 @@ function footer(opts?: { image?: boolean }) { const { providerID, modelID } = useEnvModel() const image = (() => { - if (!shareId) return "" + if (!shareUrl) return "" if (!opts?.image) return "" const titleAlt = encodeURIComponent(session.title.substring(0, 50)) const title64 = Buffer.from(session.title.substring(0, 700), "utf8").toString("base64") + const shareId = shareUrl.split("/").pop() - return `${titleAlt}\n` + return `${titleAlt}\n` })() - const shareUrl = shareId ? `[Redrob Code session](${useShareUrl()}/s/${shareId})  |  ` : "" - return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})` + const shareLink = shareUrl ? `[Redrob Code session](${shareUrl})  |  ` : "" + return `\n\n${image}${shareLink}[github run](${useEnvRunUrl()})` } async function fetchRepo() { @@ -1107,16 +1090,3 @@ function buildPromptDataForPR(pr: GitHubPullRequest) { ].join("\n") } -async function revokeAppToken() { - if (!accessToken) return - console.log("Revoking app token...") - - await fetch("https://api.github.com/installation/token", { - method: "DELETE", - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - }) -} diff --git a/packages/core/src/links.ts b/packages/core/src/links.ts new file mode 100644 index 0000000000..69d179a87d --- /dev/null +++ b/packages/core/src/links.ts @@ -0,0 +1,22 @@ +// Leaf module holding every public Redrob Code URL that ships in the binary. Dependency-free +// on purpose so core, the CLI and the TUI can all import it without pulling in anything heavier. +// +// The old "code." host never existed. Everything user-facing lives under the Console: +// the product page, the docs, the install script, and the JSON Schemas that editors fetch from a +// config file's `$schema` field. Keep the literals here so a dead link cannot creep back in one +// file at a time; JSON assets (packages/redrob/config.json, the TUI theme files) repeat the +// schema URLs as literals because JSON cannot import, and `redrob generate` regenerates the schema +// documents at `packages/redrob/schema/*.json` with matching `$id`s. + +export const CONSOLE_HOME_URL = "https://console.redrob.ai" +export const CODE_HOME_URL = `${CONSOLE_HOME_URL}/code` +export const CODE_DOCS_URL = `${CONSOLE_HOME_URL}/docs` +export const CODE_INSTALL_URL = `${CODE_HOME_URL}/install.sh` +export const CODE_INSTALL_COMMAND = `curl -fsSL ${CODE_INSTALL_URL} | sh` + +/** `$schema` for redrob.json / redrob.jsonc (generated from ConfigV1.Info). */ +export const CONFIG_SCHEMA_URL = `${CODE_HOME_URL}/config.json` +/** `$schema` for tui.json (generated from TuiConfig.Info). */ +export const TUI_SCHEMA_URL = `${CODE_HOME_URL}/tui.json` +/** `$schema` for a TUI theme file (hand-written from the ThemeJson type). */ +export const THEME_SCHEMA_URL = `${CODE_HOME_URL}/theme.json` diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts index 8eb9126621..6ecded3b17 100644 --- a/packages/core/src/plugin/provider/kilo.ts +++ b/packages/core/src/plugin/provider/kilo.ts @@ -11,8 +11,8 @@ export const KiloPlugin = define({ if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue evt.provider.update(item.provider.id, (provider) => { - provider.request.headers["HTTP-Referer"] = "https://code.redrob.ai/" - provider.request.headers["X-Title"] = "redrob" + provider.request.headers["HTTP-Referer"] = "https://console.redrob.ai/code" + provider.request.headers["X-Title"] = "Redrob Code" }) } }), diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts index ba3902aa53..935cb2c43a 100644 --- a/packages/core/src/plugin/provider/llmgateway.ts +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -15,8 +15,8 @@ export const LLMGatewayPlugin = define({ if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue evt.provider.update(item.provider.id, (provider) => { - provider.request.headers["HTTP-Referer"] = "https://code.redrob.ai/" - provider.request.headers["X-Title"] = "redrob" + provider.request.headers["HTTP-Referer"] = "https://console.redrob.ai/code" + provider.request.headers["X-Title"] = "Redrob Code" provider.request.headers["X-Source"] = "redrob" }) } diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts index b530e7be3e..2d73f963f2 100644 --- a/packages/core/src/plugin/provider/nvidia.ts +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -11,8 +11,8 @@ export const NvidiaPlugin = define({ if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue evt.provider.update(item.provider.id, (provider) => { - provider.request.headers["HTTP-Referer"] = "https://code.redrob.ai/" - provider.request.headers["X-Title"] = "redrob" + provider.request.headers["HTTP-Referer"] = "https://console.redrob.ai/code" + provider.request.headers["X-Title"] = "Redrob Code" provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "RedrobCode" }) } diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts index cb60ef0b40..cad9361ef3 100644 --- a/packages/core/src/plugin/provider/openrouter.ts +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -11,8 +11,8 @@ export const OpenRouterPlugin = define({ if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue evt.provider.update(item.provider.id, (provider) => { - provider.request.headers["HTTP-Referer"] = "https://code.redrob.ai/" - provider.request.headers["X-Title"] = "redrob" + provider.request.headers["HTTP-Referer"] = "https://console.redrob.ai/code" + provider.request.headers["X-Title"] = "Redrob Code" }) for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) { if (!item.models.has(modelID)) continue diff --git a/packages/core/src/plugin/provider/vercel.ts b/packages/core/src/plugin/provider/vercel.ts index 4a27c3837c..feefecf31b 100644 --- a/packages/core/src/plugin/provider/vercel.ts +++ b/packages/core/src/plugin/provider/vercel.ts @@ -10,7 +10,7 @@ export const VercelPlugin = define({ if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/vercel") continue evt.provider.update(item.provider.id, (provider) => { - provider.request.headers["http-referer"] = "https://code.redrob.ai/" + provider.request.headers["http-referer"] = "https://console.redrob.ai/code" provider.request.headers["x-title"] = "redrob" }) } diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts index fc4b61caa7..1ece569bf8 100644 --- a/packages/core/src/plugin/provider/zenmux.ts +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -11,8 +11,8 @@ export const ZenmuxPlugin = define({ if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue evt.provider.update(item.provider.id, (provider) => { - provider.request.headers["HTTP-Referer"] ??= "https://code.redrob.ai/" - provider.request.headers["X-Title"] ??= "redrob" + provider.request.headers["HTTP-Referer"] ??= "https://console.redrob.ai/code" + provider.request.headers["X-Title"] ??= "Redrob Code" }) } }), diff --git a/packages/core/src/plugin/skill/customize-redrob.md b/packages/core/src/plugin/skill/customize-redrob.md index ee49b3be1d..62e032d118 100644 --- a/packages/core/src/plugin/skill/customize-redrob.md +++ b/packages/core/src/plugin/skill/customize-redrob.md @@ -16,7 +16,7 @@ is wrong. The shapes below cover the common surface area, but they are a The authoritative list of every config option — with field types, enums, defaults, and descriptions — lives in the published JSON Schema: -**** +**** If a field is not documented in this skill, or you need to confirm an exact shape before writing config, **fetch that URL and read the schema directly** @@ -24,7 +24,7 @@ rather than guessing. redrob hard-fails on invalid config, so the cost of a wrong shape is a broken startup. Independently, every `redrob.json` should declare -`"$schema": "https://code.redrob.ai/config.json"` so the user's editor catches +`"$schema": "https://console.redrob.ai/code/config.json"` so the user's editor catches mistakes as they type. ## Applying changes @@ -61,7 +61,7 @@ Every field is optional. ```json { - "$schema": "https://code.redrob.ai/config.json", + "$schema": "https://console.redrob.ai/code/config.json", "username": "string", "model": "provider/model-id", "small_model": "provider/model-id", @@ -433,7 +433,7 @@ When a user's config is broken and redrob won't start, these env vars help: and start from globals only. Run from the project directory, redrob loads, the user edits the broken file, then they restart without the flag. - `REDROB_CONFIG=/path/to/file.json`: load an additional explicit config. -- `REDROB_CONFIG_CONTENT='{"$schema":"https://code.redrob.ai/config.json"}'`: +- `REDROB_CONFIG_CONTENT='{"$schema":"https://console.redrob.ai/code/config.json"}'`: inject inline JSON as a final local-scope merge. - `REDROB_DISABLE_DEFAULT_PLUGINS=1`: skip default plugins. - `REDROB_PURE=1`: skip external plugins entirely. @@ -445,7 +445,7 @@ When a user's config is broken and redrob won't start, these env vars help: - Validate against the schema before writing. If you are unsure of a field's exact shape, or the field is not covered in this skill, fetch - `https://code.redrob.ai/config.json` and read the schema rather than guessing. + `https://console.redrob.ai/code/config.json` and read the schema rather than guessing. - Preserve `$schema` and any existing fields the user did not ask to change. - For agent, command, skill, and plugin definitions, prefer creating new files in the correct location over inlining everything in `redrob.json`. diff --git a/packages/core/src/plugin/skill/skill-writer.md b/packages/core/src/plugin/skill/skill-writer.md index 40ee892f4b..358b505759 100644 --- a/packages/core/src/plugin/skill/skill-writer.md +++ b/packages/core/src/plugin/skill/skill-writer.md @@ -67,7 +67,7 @@ the file and the reason. A malformed frontmatter block drops the whole document. name: release-notes description: Use when drafting release notes from a tag range, or when the user asks what changed between two versions. slash: true -icon: https://code.redrob.ai/icon/release-notes.svg +icon: https://console.redrob.ai/code/icon/release-notes.svg autoInject: keywords: - release notes diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index 8b1527a20b..972597005e 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -39,7 +39,7 @@ export const Info = Schema.Struct({ description: "Server configuration for redrob serve and web commands", }), command: Schema.optional(Schema.Record(Schema.String, ConfigCommandV1.Info)).annotate({ - description: "Command configuration, see https://code.redrob.ai/docs/commands", + description: "Command configuration, see https://console.redrob.ai/docs/commands", }), skills: Schema.optional(ConfigSkillsV1.Info).annotate({ description: "Additional skill folder paths" }), references: Schema.optional(ConfigReference.Info).annotate({ @@ -106,7 +106,7 @@ export const Info = Schema.Struct({ }), [Schema.Record(Schema.String, ConfigAgentV1.Info)], ), - ).annotate({ description: "Agent configuration, see https://code.redrob.ai/docs/agents" }), + ).annotate({ description: "Agent configuration, see https://console.redrob.ai/docs/agents" }), provider: Schema.optional(Schema.Record(Schema.String, ConfigProviderV1.Info)).annotate({ description: "Custom provider configurations and model overrides", }), diff --git a/packages/core/test/skill/skill-writer.test.ts b/packages/core/test/skill/skill-writer.test.ts index fdd0272ef9..1c79fc3c46 100644 --- a/packages/core/test/skill/skill-writer.test.ts +++ b/packages/core/test/skill/skill-writer.test.ts @@ -82,7 +82,7 @@ describe("K-5 the skill that writes skills", () => { "Use when drafting release notes from a tag range, or when the user asks what changed between two versions.", ) expect(skill.slash).toBe(true) - expect(skill.icon).toBe("https://code.redrob.ai/icon/release-notes.svg") + expect(skill.icon).toBe("https://console.redrob.ai/code/icon/release-notes.svg") expect(skill.autoInject).toBeDefined() expect(skill.autoInject!.keywords).toEqual(["release notes", "changelog"]) expect(skill.autoInject!.url).toEqual(["github.com/*/releases/**"]) diff --git a/packages/docs/docs.json b/packages/docs/docs.json index e0606abfbe..0e51602148 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -30,7 +30,7 @@ { "group": "Getting started", "pages": ["index", "quickstart", "development"], - "openapi": "https://code.redrob.ai/openapi.json" + "openapi": "https://console.redrob.ai/code/openapi.json" } ] } diff --git a/packages/redrob/config.json b/packages/redrob/config.json index 03f6236148..7bf646aa7c 100644 --- a/packages/redrob/config.json +++ b/packages/redrob/config.json @@ -1,3 +1,3 @@ { - "$schema": "https://code.redrob.ai/config.json" + "$schema": "https://console.redrob.ai/code/config.json" } \ No newline at end of file diff --git a/packages/redrob/package.json b/packages/redrob/package.json index 1e4b2ea726..76081db17c 100644 --- a/packages/redrob/package.json +++ b/packages/redrob/package.json @@ -12,6 +12,8 @@ "bench:test": "bun run script/bench-test-suite.ts", "profile:test": "bun run script/profile-test-files.ts", "build": "bun run script/build.ts", + "generate:schema": "bun run script/schema.ts", + "check:generated": "bun run generate:schema && git diff --exit-code -- schema", "dev": "bun run --conditions=browser ./src/index.ts", "dev:temporary": "bun run --conditions=browser ./src/temporary.ts" }, diff --git a/packages/redrob/schema/config.json b/packages/redrob/schema/config.json new file mode 100644 index 0000000000..21c515721f --- /dev/null +++ b/packages/redrob/schema/config.json @@ -0,0 +1,1313 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://console.redrob.ai/code/config.json", + "$ref": "#/$defs/Config", + "$defs": { + "LogLevel": { + "type": "string", + "enum": [ + "DEBUG", + "INFO", + "WARN", + "ERROR" + ], + "description": "Log level" + }, + "ServerConfig": { + "type": "object", + "properties": { + "port": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Port to listen on" + }, + "hostname": { + "type": "string", + "description": "Hostname to listen on" + }, + "mdns": { + "type": "boolean", + "description": "Enable mDNS service discovery" + }, + "mdnsDomain": { + "type": "string", + "description": "Custom domain name for mDNS service (default: redrob.local)" + }, + "cors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional domains to allow for CORS" + } + }, + "additionalProperties": false + }, + "ConfigV2.Reference.Git": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": [ + "repository" + ], + "additionalProperties": false + }, + "ConfigV2.Reference.Local": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + "PermissionActionConfig": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "PermissionObjectConfig": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/PermissionActionConfig" + } + }, + "PermissionRuleConfig": { + "anyOf": [ + { + "$ref": "#/$defs/PermissionActionConfig" + }, + { + "$ref": "#/$defs/PermissionObjectConfig" + } + ] + }, + "PermissionConfig": { + "anyOf": [ + { + "$ref": "#/$defs/PermissionActionConfig" + }, + { + "type": "object", + "properties": { + "read": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "edit": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "glob": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "grep": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "list": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "bash": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "task": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "external_directory": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "todowrite": { + "$ref": "#/$defs/PermissionActionConfig" + }, + "question": { + "$ref": "#/$defs/PermissionActionConfig" + }, + "webfetch": { + "$ref": "#/$defs/PermissionActionConfig" + }, + "websearch": { + "$ref": "#/$defs/PermissionActionConfig" + }, + "lsp": { + "$ref": "#/$defs/PermissionRuleConfig" + }, + "doom_loop": { + "$ref": "#/$defs/PermissionActionConfig" + }, + "skill": { + "$ref": "#/$defs/PermissionRuleConfig" + } + }, + "additionalProperties": { + "$ref": "#/$defs/PermissionRuleConfig" + } + } + ] + }, + "AgentConfig": { + "type": "object", + "properties": { + "model": { + "type": "string", + "$ref": "https://models.dev/model-schema.json#/$defs/Model" + }, + "variant": { + "type": "string", + "description": "Default model variant for this agent (applies only when using the agent's configured model)." + }, + "temperature": { + "type": "number" + }, + "top_p": { + "type": "number" + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "additionalProperties": { + "type": "boolean" + }, + "description": "@deprecated Use 'permission' field instead" + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string", + "description": "Description of when to use the agent" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "hidden": { + "type": "boolean", + "description": "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)" + }, + "options": { + "type": "object" + }, + "color": { + "anyOf": [ + { + "pattern": "^#[0-9a-fA-F]{6}$", + "type": "string" + }, + { + "type": "string", + "enum": [ + "primary", + "secondary", + "accent", + "success", + "warning", + "error", + "info" + ] + } + ], + "description": "Hex color code (e.g., #FF5733) or theme color (e.g., primary)" + }, + "steps": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum number of agentic iterations before forcing text-only response" + }, + "maxSteps": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "@deprecated Use 'steps' field instead." + }, + "permission": { + "$ref": "#/$defs/PermissionConfig" + } + } + }, + "ProviderConfig": { + "type": "object", + "properties": { + "api": { + "type": "string" + }, + "name": { + "type": "string" + }, + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "npm": { + "type": "string" + }, + "whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "options": { + "type": "object", + "properties": { + "apiKey": { + "type": "string" + }, + "baseURL": { + "type": "string" + }, + "enterpriseUrl": { + "type": "string", + "description": "GitHub Enterprise URL for copilot authentication" + }, + "setCacheKey": { + "type": "boolean", + "description": "Enable promptCacheKey for this provider (default false)" + }, + "timeout": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991 + }, + { + "type": "boolean", + "enum": [ + false + ] + } + ], + "description": "Timeout in milliseconds for full requests to this provider. Set to false to disable timeout." + }, + "headerTimeout": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991 + }, + { + "type": "boolean", + "enum": [ + false + ] + } + ], + "description": "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout." + }, + "chunkTimeout": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted." + } + } + }, + "models": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "family": { + "type": "string" + }, + "release_date": { + "type": "string" + }, + "attachment": { + "type": "boolean" + }, + "reasoning": { + "type": "boolean" + }, + "temperature": { + "type": "boolean" + }, + "tool_call": { + "type": "boolean" + }, + "interleaved": { + "anyOf": [ + { + "type": "boolean" + }, + { + "anyOf": [ + { + "type": "string", + "enum": [ + "reasoning", + "reasoning_content", + "reasoning_text" + ] + }, + { + "type": "string" + } + ] + }, + { + "type": "object", + "properties": { + "field": { + "anyOf": [ + { + "type": "string", + "enum": [ + "reasoning", + "reasoning_content", + "reasoning_text" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + ] + }, + "cost": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache_read": { + "type": "number" + }, + "cache_write": { + "type": "number" + }, + "context_over_200k": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache_read": { + "type": "number" + }, + "cache_write": { + "type": "number" + } + }, + "required": [ + "input", + "output" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output" + ], + "additionalProperties": false + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "number" + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + } + }, + "required": [ + "context", + "output" + ], + "additionalProperties": false + }, + "modalities": { + "type": "object", + "properties": { + "input": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] + } + }, + "output": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] + } + } + }, + "additionalProperties": false + }, + "experimental": { + "type": "boolean" + }, + "status": { + "type": "string", + "enum": [ + "alpha", + "beta", + "deprecated", + "active" + ] + }, + "provider": { + "type": "object", + "properties": { + "npm": { + "type": "string" + }, + "api": { + "type": "string" + } + }, + "additionalProperties": false + }, + "options": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "variants": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "description": "Disable this variant for the model" + } + } + }, + "description": "Variant-specific configuration" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "McpLocalConfig": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "local" + ], + "description": "Type of MCP server connection" + }, + "command": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Command and arguments to run the MCP server" + }, + "cwd": { + "type": "string", + "description": "Working directory for the MCP server process. Relative paths resolve from the workspace directory." + }, + "environment": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables to set when running the MCP server" + }, + "enabled": { + "type": "boolean", + "description": "Enable or disable the MCP server on startup" + }, + "timeout": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified." + } + }, + "required": [ + "type", + "command" + ], + "additionalProperties": false + }, + "McpOAuthConfig": { + "type": "object", + "properties": { + "clientId": { + "type": "string", + "description": "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted." + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (if required by the authorization server)" + }, + "scope": { + "type": "string", + "description": "OAuth scopes to request during authorization" + }, + "callbackPort": { + "minimum": 1, + "maximum": 65535, + "type": "integer", + "description": "Port for the local OAuth callback server (default: 19876). Shorthand for redirectUri when only the port needs changing. Ignored if redirectUri is set." + }, + "redirectUri": { + "type": "string", + "description": "OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)." + } + }, + "additionalProperties": false + }, + "McpRemoteConfig": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "remote" + ], + "description": "Type of MCP server connection" + }, + "url": { + "type": "string", + "description": "URL of the remote MCP server" + }, + "enabled": { + "type": "boolean", + "description": "Enable or disable the MCP server on startup" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Headers to send with the request" + }, + "oauth": { + "anyOf": [ + { + "$ref": "#/$defs/McpOAuthConfig" + }, + { + "type": "boolean", + "enum": [ + false + ] + } + ], + "description": "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection." + }, + "timeout": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified." + } + }, + "required": [ + "type", + "url" + ], + "additionalProperties": false + }, + "LayoutConfig": { + "type": "string", + "enum": [ + "auto", + "stretch" + ] + }, + "ImageAttachmentConfig": { + "type": "object", + "properties": { + "auto_resize": { + "type": "boolean", + "description": "Resize images before sending them to the model when they exceed configured limits (default: true)" + }, + "max_width": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum image width before resizing or rejecting the attachment (default: 2000)" + }, + "max_height": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum image height before resizing or rejecting the attachment (default: 2000)" + }, + "max_base64_bytes": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum base64 payload bytes for an image attachment (default: 5242880)" + } + }, + "additionalProperties": false + }, + "AttachmentConfig": { + "type": "object", + "properties": { + "image": { + "$ref": "#/$defs/ImageAttachmentConfig", + "description": "Image attachment configuration" + } + }, + "additionalProperties": false + }, + "Policy.Effect": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + }, + "ConfigV2.Experimental.Policy": { + "type": "object", + "properties": { + "action": { + "anyOf": [ + { + "anyOf": [ + { + "type": "string", + "enum": [ + "provider.use" + ] + } + ] + } + ] + }, + "effect": { + "$ref": "#/$defs/Policy.Effect" + }, + "resource": { + "type": "string" + } + }, + "required": [ + "action", + "effect", + "resource" + ], + "additionalProperties": false + }, + "Config": { + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "JSON schema reference for configuration validation" + }, + "shell": { + "type": "string", + "description": "Default shell to use for terminal and bash tool" + }, + "logLevel": { + "$ref": "#/$defs/LogLevel", + "description": "Log level" + }, + "server": { + "$ref": "#/$defs/ServerConfig", + "description": "Server configuration for redrob serve and web commands" + }, + "command": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "template": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "string", + "$ref": "https://models.dev/model-schema.json#/$defs/Model" + }, + "variant": { + "type": "string" + }, + "subtask": { + "type": "boolean" + } + }, + "required": [ + "template" + ], + "additionalProperties": false + }, + "description": "Command configuration, see https://console.redrob.ai/docs/commands" + }, + "skills": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional paths to skill folders" + }, + "urls": { + "type": "array", + "items": { + "type": "string" + }, + "description": "URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)" + } + }, + "additionalProperties": false, + "description": "Additional skill folder paths" + }, + "references": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/ConfigV2.Reference.Git" + }, + { + "$ref": "#/$defs/ConfigV2.Reference.Local" + } + ] + }, + "description": "Named git or local directory references" + }, + "reference": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/ConfigV2.Reference.Git" + }, + { + "$ref": "#/$defs/ConfigV2.Reference.Local" + } + ] + }, + "description": "@deprecated Use 'references' field instead. Named git or local directory references" + }, + "watcher": { + "type": "object", + "properties": { + "ignore": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "snapshot": { + "type": "boolean", + "description": "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true." + }, + "plugin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "object" + } + ], + "maxItems": 2, + "minItems": 2 + } + ] + } + }, + "share": { + "type": "string", + "enum": [ + "manual", + "auto", + "disabled" + ], + "description": "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing" + }, + "autoshare": { + "type": "boolean", + "description": "@deprecated Use 'share' field instead. Share newly created sessions automatically" + }, + "autoupdate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "notify" + ] + } + ], + "description": "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications" + }, + "disabled_providers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Disable providers that are loaded automatically" + }, + "enabled_providers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "When set, ONLY these providers will be enabled. All other providers will be ignored" + }, + "model": { + "type": "string", + "description": "Model to use in the format of provider/model, eg anthropic/claude-2", + "$ref": "https://models.dev/model-schema.json#/$defs/Model" + }, + "small_model": { + "type": "string", + "description": "Small model to use for tasks like title generation in the format of provider/model", + "$ref": "https://models.dev/model-schema.json#/$defs/Model" + }, + "default_agent": { + "type": "string", + "description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid." + }, + "subagent_depth": { + "minimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum subagent nesting depth. Defaults to 1, which prevents subagents from launching subagents." + }, + "username": { + "type": "string", + "description": "Custom username to display in conversations instead of system username" + }, + "mode": { + "type": "object", + "properties": { + "build": { + "$ref": "#/$defs/AgentConfig" + }, + "plan": { + "$ref": "#/$defs/AgentConfig" + } + }, + "additionalProperties": { + "$ref": "#/$defs/AgentConfig" + }, + "description": "@deprecated Use `agent` field instead." + }, + "agent": { + "type": "object", + "properties": { + "plan": { + "$ref": "#/$defs/AgentConfig" + }, + "build": { + "$ref": "#/$defs/AgentConfig" + }, + "general": { + "$ref": "#/$defs/AgentConfig" + }, + "explore": { + "$ref": "#/$defs/AgentConfig" + }, + "title": { + "$ref": "#/$defs/AgentConfig" + }, + "summary": { + "$ref": "#/$defs/AgentConfig" + }, + "compaction": { + "$ref": "#/$defs/AgentConfig" + } + }, + "additionalProperties": { + "$ref": "#/$defs/AgentConfig" + }, + "description": "Agent configuration, see https://console.redrob.ai/docs/agents" + }, + "provider": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ProviderConfig" + }, + "description": "Custom provider configurations and model overrides" + }, + "mcp": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/$defs/McpLocalConfig" + }, + { + "$ref": "#/$defs/McpRemoteConfig" + } + ] + }, + { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "additionalProperties": false + } + ] + }, + "description": "MCP (Model Context Protocol) server configurations" + }, + "formatter": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean" + }, + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "environment": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "extensions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } + ], + "description": "Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides." + }, + "lsp": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "disabled" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "extensions": { + "type": "array", + "items": { + "type": "string" + } + }, + "disabled": { + "type": "boolean" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "initialization": { + "type": "object" + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + ] + } + } + ], + "description": "Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides." + }, + "instructions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional instruction files or patterns to include" + }, + "layout": { + "$ref": "#/$defs/LayoutConfig", + "description": "@deprecated Always uses stretch layout." + }, + "permission": { + "$ref": "#/$defs/PermissionConfig" + }, + "tools": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "attachment": { + "$ref": "#/$defs/AttachmentConfig", + "description": "Attachment processing configuration, including image size limits and resizing behavior" + }, + "enterprise": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Enterprise URL" + } + }, + "additionalProperties": false + }, + "tool_output": { + "type": "object", + "properties": { + "max_lines": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)" + }, + "max_bytes": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)" + } + }, + "additionalProperties": false, + "description": "Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned." + }, + "compaction": { + "type": "object", + "properties": { + "auto": { + "type": "boolean", + "description": "Compact automatically when the context fills. Default TRUE - set false to turn it off. It was false, which meant an untouched workspace never compacted while the desktop app displayed the setting as on." + }, + "prune": { + "type": "boolean", + "description": "Enable pruning of old tool outputs (default: false)" + }, + "tail_turns": { + "minimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction. By default retention is limited only by the preserved token budget." + }, + "preserve_recent_tokens": { + "minimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Maximum number of tokens from recent turns to preserve verbatim after compaction" + }, + "reserved": { + "minimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Token buffer for compaction. Leaves enough window to avoid overflow during compaction." + }, + "threshold": { + "minimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "How full the context may get before compacting, as a percentage of the window (1-100, default 70). Scales with the model, where `reserved` does not: a 20,000-token reserve is most of a small window and 2% of a 1,000,000-token one, which put the trigger at 98% and made the turn that crossed it the turn that failed. An explicit `reserved` still wins over this." + }, + "maxTurnInputCostUsd": { + "type": "number", + "description": "The most one turn's input may cost, in USD, before compacting (default 0.50). Applies alongside `threshold`, whichever comes first, because a percentage scales with the window and money does not: 70% of a 1,000,000-token window at $3.15 per million is about $2.16 of input on every later turn. Set 0 to disable and be governed by the percentage alone. Ignored for a model with no published input price, and `reserved` still wins over both." + } + }, + "additionalProperties": false + }, + "experimental": { + "type": "object", + "properties": { + "disable_paste_summary": { + "type": "boolean" + }, + "batch_tool": { + "type": "boolean", + "description": "Enable the batch tool" + }, + "openTelemetry": { + "type": "boolean", + "description": "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)" + }, + "primary_tools": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tools that should only be available to primary agents." + }, + "continue_loop_on_deny": { + "type": "boolean", + "description": "Continue the agent loop when a tool call is denied" + }, + "mcp_timeout": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Timeout in milliseconds for model context protocol (MCP) requests" + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/$defs/ConfigV2.Experimental.Policy" + }, + "description": "Policy statements applied to supported resources, such as provider access" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "allowComments": true, + "allowTrailingCommas": true +} diff --git a/packages/redrob/schema/theme.json b/packages/redrob/schema/theme.json new file mode 100644 index 0000000000..a27112ef32 --- /dev/null +++ b/packages/redrob/schema/theme.json @@ -0,0 +1,305 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://console.redrob.ai/code/theme.json", + "title": "Redrob Code TUI theme", + "description": "A colour theme for the Redrob Code terminal interface. `defs` holds named colours; `theme` maps every interface role to a colour, a `defs` name, another role, or a dark/light pair.", + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "defs": { + "type": "object", + "description": "Named colours referenced from `theme` (or from other defs).", + "additionalProperties": { + "$ref": "#/$defs/HexOrRef" + } + }, + "theme": { + "type": "object", + "properties": { + "primary": { + "$ref": "#/$defs/ColorValue" + }, + "secondary": { + "$ref": "#/$defs/ColorValue" + }, + "accent": { + "$ref": "#/$defs/ColorValue" + }, + "error": { + "$ref": "#/$defs/ColorValue" + }, + "warning": { + "$ref": "#/$defs/ColorValue" + }, + "success": { + "$ref": "#/$defs/ColorValue" + }, + "info": { + "$ref": "#/$defs/ColorValue" + }, + "text": { + "$ref": "#/$defs/ColorValue" + }, + "textMuted": { + "$ref": "#/$defs/ColorValue" + }, + "background": { + "$ref": "#/$defs/ColorValue" + }, + "backgroundPanel": { + "$ref": "#/$defs/ColorValue" + }, + "backgroundElement": { + "$ref": "#/$defs/ColorValue" + }, + "border": { + "$ref": "#/$defs/ColorValue" + }, + "borderActive": { + "$ref": "#/$defs/ColorValue" + }, + "borderSubtle": { + "$ref": "#/$defs/ColorValue" + }, + "diffAdded": { + "$ref": "#/$defs/ColorValue" + }, + "diffRemoved": { + "$ref": "#/$defs/ColorValue" + }, + "diffContext": { + "$ref": "#/$defs/ColorValue" + }, + "diffHunkHeader": { + "$ref": "#/$defs/ColorValue" + }, + "diffHighlightAdded": { + "$ref": "#/$defs/ColorValue" + }, + "diffHighlightRemoved": { + "$ref": "#/$defs/ColorValue" + }, + "diffAddedBg": { + "$ref": "#/$defs/ColorValue" + }, + "diffRemovedBg": { + "$ref": "#/$defs/ColorValue" + }, + "diffContextBg": { + "$ref": "#/$defs/ColorValue" + }, + "diffLineNumber": { + "$ref": "#/$defs/ColorValue" + }, + "diffAddedLineNumberBg": { + "$ref": "#/$defs/ColorValue" + }, + "diffRemovedLineNumberBg": { + "$ref": "#/$defs/ColorValue" + }, + "markdownText": { + "$ref": "#/$defs/ColorValue" + }, + "markdownHeading": { + "$ref": "#/$defs/ColorValue" + }, + "markdownLink": { + "$ref": "#/$defs/ColorValue" + }, + "markdownLinkText": { + "$ref": "#/$defs/ColorValue" + }, + "markdownCode": { + "$ref": "#/$defs/ColorValue" + }, + "markdownBlockQuote": { + "$ref": "#/$defs/ColorValue" + }, + "markdownEmph": { + "$ref": "#/$defs/ColorValue" + }, + "markdownStrong": { + "$ref": "#/$defs/ColorValue" + }, + "markdownHorizontalRule": { + "$ref": "#/$defs/ColorValue" + }, + "markdownListItem": { + "$ref": "#/$defs/ColorValue" + }, + "markdownListEnumeration": { + "$ref": "#/$defs/ColorValue" + }, + "markdownImage": { + "$ref": "#/$defs/ColorValue" + }, + "markdownImageText": { + "$ref": "#/$defs/ColorValue" + }, + "markdownCodeBlock": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxComment": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxKeyword": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxFunction": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxVariable": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxString": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxNumber": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxType": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxOperator": { + "$ref": "#/$defs/ColorValue" + }, + "syntaxPunctuation": { + "$ref": "#/$defs/ColorValue" + }, + "selectedListItemText": { + "$ref": "#/$defs/ColorValue" + }, + "backgroundMenu": { + "$ref": "#/$defs/ColorValue" + }, + "thinkingOpacity": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Opacity applied to thinking/reasoning text. Defaults to 0.6." + } + }, + "required": [ + "primary", + "secondary", + "accent", + "error", + "warning", + "success", + "info", + "text", + "textMuted", + "background", + "backgroundPanel", + "backgroundElement", + "border", + "borderActive", + "borderSubtle", + "diffAdded", + "diffRemoved", + "diffContext", + "diffHunkHeader", + "diffHighlightAdded", + "diffHighlightRemoved", + "diffAddedBg", + "diffRemovedBg", + "diffContextBg", + "diffLineNumber", + "diffAddedLineNumberBg", + "diffRemovedLineNumberBg", + "markdownText", + "markdownHeading", + "markdownLink", + "markdownLinkText", + "markdownCode", + "markdownBlockQuote", + "markdownEmph", + "markdownStrong", + "markdownHorizontalRule", + "markdownListItem", + "markdownListEnumeration", + "markdownImage", + "markdownImageText", + "markdownCodeBlock", + "syntaxComment", + "syntaxKeyword", + "syntaxFunction", + "syntaxVariable", + "syntaxString", + "syntaxNumber", + "syntaxType", + "syntaxOperator", + "syntaxPunctuation" + ], + "additionalProperties": false + } + }, + "required": [ + "theme" + ], + "additionalProperties": false, + "$defs": { + "HexColor": { + "type": "string", + "pattern": "^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$", + "description": "Hex colour: #RGB, #RGBA, #RRGGBB or #RRGGBBAA, e.g. #8aafff" + }, + "RefName": { + "type": "string", + "description": "The name of an entry in `defs`, the name of another role in `theme`, or `transparent` / `none` for no colour.", + "not": { + "pattern": "^#" + } + }, + "HexOrRef": { + "anyOf": [ + { + "$ref": "#/$defs/HexColor" + }, + { + "$ref": "#/$defs/RefName" + } + ] + }, + "AnsiIndex": { + "type": "integer", + "minimum": 0, + "maximum": 255, + "description": "An ANSI 256-colour palette index." + }, + "Variant": { + "type": "object", + "description": "A colour that differs between dark and light terminals.", + "properties": { + "dark": { + "$ref": "#/$defs/HexOrRef" + }, + "light": { + "$ref": "#/$defs/HexOrRef" + } + }, + "required": [ + "dark", + "light" + ], + "additionalProperties": false + }, + "ColorValue": { + "anyOf": [ + { + "$ref": "#/$defs/HexOrRef" + }, + { + "$ref": "#/$defs/AnsiIndex" + }, + { + "$ref": "#/$defs/Variant" + } + ] + } + }, + "allowComments": true, + "allowTrailingCommas": true +} diff --git a/packages/redrob/schema/tui.json b/packages/redrob/schema/tui.json new file mode 100644 index 0000000000..525136561b --- /dev/null +++ b/packages/redrob/schema/tui.json @@ -0,0 +1,1176 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://console.redrob.ai/code/tui.json", + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "keybinds": { + "type": "object", + "properties": { + "leader": { + "$ref": "#/$defs/KeybindsValue", + "description": "Leader key for keybind combinations" + }, + "app_exit": { + "$ref": "#/$defs/KeybindsValue", + "description": "Exit the application" + }, + "app_debug": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle debug panel" + }, + "app_console": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle console" + }, + "app_heap_snapshot": { + "$ref": "#/$defs/KeybindsValue", + "description": "Write heap snapshot" + }, + "app_toggle_animations": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle animations" + }, + "app_toggle_file_context": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle file context" + }, + "app_toggle_diffwrap": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle diff wrapping" + }, + "app_toggle_paste_summary": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle paste summary" + }, + "app_toggle_session_directory_filter": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle session directory filtering" + }, + "command_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "List available commands" + }, + "help_show": { + "$ref": "#/$defs/KeybindsValue", + "description": "Open help dialog" + }, + "docs_open": { + "$ref": "#/$defs/KeybindsValue", + "description": "Open documentation" + }, + "diff_open": { + "$ref": "#/$defs/KeybindsValue", + "description": "Open diff viewer" + }, + "diff_close": { + "$ref": "#/$defs/KeybindsValue", + "description": "Close diff viewer" + }, + "diff_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle diff viewer item" + }, + "diff_expand": { + "$ref": "#/$defs/KeybindsValue", + "description": "Expand diff viewer item" + }, + "diff_expand_all": { + "$ref": "#/$defs/KeybindsValue", + "description": "Expand all diff viewer folders" + }, + "diff_collapse": { + "$ref": "#/$defs/KeybindsValue", + "description": "Collapse diff viewer item" + }, + "diff_switch_focus": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch diff viewer focus" + }, + "diff_next_hunk": { + "$ref": "#/$defs/KeybindsValue", + "description": "Jump to next diff hunk" + }, + "diff_previous_hunk": { + "$ref": "#/$defs/KeybindsValue", + "description": "Jump to previous diff hunk" + }, + "diff_next_file": { + "$ref": "#/$defs/KeybindsValue", + "description": "Jump to next diff file" + }, + "diff_previous_file": { + "$ref": "#/$defs/KeybindsValue", + "description": "Jump to previous diff file" + }, + "diff_toggle_file_tree": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle diff viewer file tree" + }, + "diff_single_patch": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle single patch view" + }, + "diff_switch_source": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch diff viewer source" + }, + "diff_toggle_view": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle diff viewer split or unified view" + }, + "diff_help": { + "$ref": "#/$defs/KeybindsValue", + "description": "Show more diff viewer shortcuts" + }, + "editor_open": { + "$ref": "#/$defs/KeybindsValue", + "description": "Open external editor" + }, + "language_switch": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch language" + }, + "theme_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "List available themes" + }, + "theme_switch_mode": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch between light and dark theme mode" + }, + "theme_mode_lock": { + "$ref": "#/$defs/KeybindsValue", + "description": "Lock or unlock theme mode" + }, + "sidebar_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle sidebar" + }, + "scrollbar_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle session scrollbar" + }, + "status_view": { + "$ref": "#/$defs/KeybindsValue", + "description": "View status" + }, + "debug_view": { + "$ref": "#/$defs/KeybindsValue", + "description": "View debug info" + }, + "session_export": { + "$ref": "#/$defs/KeybindsValue", + "description": "Export session to editor" + }, + "session_copy": { + "$ref": "#/$defs/KeybindsValue", + "description": "Copy session transcript" + }, + "session_move": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move session" + }, + "session_new": { + "$ref": "#/$defs/KeybindsValue", + "description": "Create a new session" + }, + "session_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "List all sessions" + }, + "session_timeline": { + "$ref": "#/$defs/KeybindsValue", + "description": "Show session timeline" + }, + "session_fork": { + "$ref": "#/$defs/KeybindsValue", + "description": "Fork session from message" + }, + "session_rename": { + "$ref": "#/$defs/KeybindsValue", + "description": "Rename session" + }, + "session_delete": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete session" + }, + "session_share": { + "$ref": "#/$defs/KeybindsValue", + "description": "Share current session" + }, + "session_unshare": { + "$ref": "#/$defs/KeybindsValue", + "description": "Unshare current session" + }, + "session_interrupt": { + "$ref": "#/$defs/KeybindsValue", + "description": "Interrupt current session" + }, + "session_background": { + "$ref": "#/$defs/KeybindsValue", + "description": "Background synchronous subagents" + }, + "session_compact": { + "$ref": "#/$defs/KeybindsValue", + "description": "Compact the session" + }, + "session_toggle_timestamps": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle message timestamps" + }, + "session_toggle_generic_tool_output": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle generic tool output" + }, + "session_queued_prompts": { + "$ref": "#/$defs/KeybindsValue", + "description": "Manage queued prompts" + }, + "session_child_first": { + "$ref": "#/$defs/KeybindsValue", + "description": "Go to first child session" + }, + "session_child_cycle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Go to next child session" + }, + "session_child_cycle_reverse": { + "$ref": "#/$defs/KeybindsValue", + "description": "Go to previous child session" + }, + "session_parent": { + "$ref": "#/$defs/KeybindsValue", + "description": "Go to parent session" + }, + "session_pin_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Pin or unpin session in the session list" + }, + "session_quick_switch_1": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 1" + }, + "session_quick_switch_2": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 2" + }, + "session_quick_switch_3": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 3" + }, + "session_quick_switch_4": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 4" + }, + "session_quick_switch_5": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 5" + }, + "session_quick_switch_6": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 6" + }, + "session_quick_switch_7": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 7" + }, + "session_quick_switch_8": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 8" + }, + "session_quick_switch_9": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch to session in quick slot 9" + }, + "stash_delete": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete stash entry" + }, + "model_provider_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "Open provider list from model dialog" + }, + "model_favorite_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle model favorite status" + }, + "model_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "List available models" + }, + "model_cycle_recent": { + "$ref": "#/$defs/KeybindsValue", + "description": "Next recently used model" + }, + "model_cycle_recent_reverse": { + "$ref": "#/$defs/KeybindsValue", + "description": "Previous recently used model" + }, + "model_cycle_favorite": { + "$ref": "#/$defs/KeybindsValue", + "description": "Next favorite model" + }, + "model_cycle_favorite_reverse": { + "$ref": "#/$defs/KeybindsValue", + "description": "Previous favorite model" + }, + "mcp_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "List MCP servers" + }, + "provider_connect": { + "$ref": "#/$defs/KeybindsValue", + "description": "Connect provider" + }, + "console_org_switch": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch console organization" + }, + "agent_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "List agents" + }, + "agent_cycle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Next agent" + }, + "agent_cycle_reverse": { + "$ref": "#/$defs/KeybindsValue", + "description": "Previous agent" + }, + "variant_cycle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Cycle model variants" + }, + "variant_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "List model variants" + }, + "messages_page_up": { + "$ref": "#/$defs/KeybindsValue", + "description": "Scroll messages up by one page" + }, + "messages_page_down": { + "$ref": "#/$defs/KeybindsValue", + "description": "Scroll messages down by one page" + }, + "messages_line_up": { + "$ref": "#/$defs/KeybindsValue", + "description": "Scroll messages up by one line" + }, + "messages_line_down": { + "$ref": "#/$defs/KeybindsValue", + "description": "Scroll messages down by one line" + }, + "messages_half_page_up": { + "$ref": "#/$defs/KeybindsValue", + "description": "Scroll messages up by half page" + }, + "messages_half_page_down": { + "$ref": "#/$defs/KeybindsValue", + "description": "Scroll messages down by half page" + }, + "messages_first": { + "$ref": "#/$defs/KeybindsValue", + "description": "Navigate to first message" + }, + "messages_last": { + "$ref": "#/$defs/KeybindsValue", + "description": "Navigate to last message" + }, + "messages_next": { + "$ref": "#/$defs/KeybindsValue", + "description": "Navigate to next message" + }, + "messages_previous": { + "$ref": "#/$defs/KeybindsValue", + "description": "Navigate to previous message" + }, + "messages_last_user": { + "$ref": "#/$defs/KeybindsValue", + "description": "Navigate to last user message" + }, + "messages_copy": { + "$ref": "#/$defs/KeybindsValue", + "description": "Copy message" + }, + "messages_undo": { + "$ref": "#/$defs/KeybindsValue", + "description": "Undo message" + }, + "messages_redo": { + "$ref": "#/$defs/KeybindsValue", + "description": "Redo message" + }, + "messages_toggle_conceal": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle code block concealment in messages" + }, + "tool_details": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle tool details visibility" + }, + "display_thinking": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle thinking blocks visibility" + }, + "prompt_submit": { + "$ref": "#/$defs/KeybindsValue", + "description": "Submit prompt" + }, + "prompt_editor_context_clear": { + "$ref": "#/$defs/KeybindsValue", + "description": "Clear editor context" + }, + "prompt_skills": { + "$ref": "#/$defs/KeybindsValue", + "description": "Open skill selector" + }, + "prompt_stash": { + "$ref": "#/$defs/KeybindsValue", + "description": "Stash prompt" + }, + "prompt_stash_pop": { + "$ref": "#/$defs/KeybindsValue", + "description": "Pop stashed prompt" + }, + "prompt_stash_list": { + "$ref": "#/$defs/KeybindsValue", + "description": "List stashed prompts" + }, + "workspace_set": { + "$ref": "#/$defs/KeybindsValue", + "description": "Set workspace" + }, + "input_clear": { + "$ref": "#/$defs/KeybindsValue", + "description": "Clear input field" + }, + "input_paste": { + "$ref": "#/$defs/KeybindsValue", + "description": "Paste from clipboard" + }, + "input_submit": { + "$ref": "#/$defs/KeybindsValue", + "description": "Submit input" + }, + "input_newline": { + "$ref": "#/$defs/KeybindsValue", + "description": "Insert newline in input" + }, + "input_move_left": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move cursor left in input" + }, + "input_move_right": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move cursor right in input" + }, + "input_move_up": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move cursor up in input" + }, + "input_move_down": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move cursor down in input" + }, + "input_select_left": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select left in input" + }, + "input_select_right": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select right in input" + }, + "input_select_up": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select up in input" + }, + "input_select_down": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select down in input" + }, + "input_line_home": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to start of line in input" + }, + "input_line_end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to end of line in input" + }, + "input_select_line_home": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select to start of line in input" + }, + "input_select_line_end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select to end of line in input" + }, + "input_visual_line_home": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to start of visual line in input" + }, + "input_visual_line_end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to end of visual line in input" + }, + "input_select_visual_line_home": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select to start of visual line in input" + }, + "input_select_visual_line_end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select to end of visual line in input" + }, + "input_buffer_home": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to start of buffer in input" + }, + "input_buffer_end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to end of buffer in input" + }, + "input_select_buffer_home": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select to start of buffer in input" + }, + "input_select_buffer_end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select to end of buffer in input" + }, + "input_delete_line": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete line in input" + }, + "input_delete_to_line_end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete to end of line in input" + }, + "input_delete_to_line_start": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete to start of line in input" + }, + "input_backspace": { + "$ref": "#/$defs/KeybindsValue", + "description": "Backspace in input" + }, + "input_delete": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete character in input" + }, + "input_undo": { + "$ref": "#/$defs/KeybindsValue", + "description": "Undo in input" + }, + "input_redo": { + "$ref": "#/$defs/KeybindsValue", + "description": "Redo in input" + }, + "input_word_forward": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move word forward in input" + }, + "input_word_backward": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move word backward in input" + }, + "input_select_word_forward": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select word forward in input" + }, + "input_select_word_backward": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select word backward in input" + }, + "input_delete_word_forward": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete word forward in input" + }, + "input_delete_word_backward": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete word backward in input" + }, + "input_select_all": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select all in input" + }, + "history_previous": { + "$ref": "#/$defs/KeybindsValue", + "description": "Previous history item" + }, + "history_next": { + "$ref": "#/$defs/KeybindsValue", + "description": "Next history item" + }, + "dialog.select.prev": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to previous dialog item" + }, + "dialog.select.next": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to next dialog item" + }, + "dialog.select.page_up": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move up one page in dialog" + }, + "dialog.select.page_down": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move down one page in dialog" + }, + "dialog.select.home": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to first dialog item" + }, + "dialog.select.end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to last dialog item" + }, + "dialog.select.submit": { + "$ref": "#/$defs/KeybindsValue", + "description": "Submit selected dialog item" + }, + "dialog.prompt.submit": { + "$ref": "#/$defs/KeybindsValue", + "description": "Submit dialog prompt" + }, + "dialog.mcp.toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle MCP in MCP dialog" + }, + "dialog.move_session.new": { + "$ref": "#/$defs/KeybindsValue", + "description": "New project copy" + }, + "dialog.move_session.delete": { + "$ref": "#/$defs/KeybindsValue", + "description": "Delete project copy" + }, + "dialog.move_session.refresh": { + "$ref": "#/$defs/KeybindsValue", + "description": "Refresh project copies" + }, + "prompt.autocomplete.prev": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to previous autocomplete item" + }, + "prompt.autocomplete.next": { + "$ref": "#/$defs/KeybindsValue", + "description": "Move to next autocomplete item" + }, + "prompt.autocomplete.hide": { + "$ref": "#/$defs/KeybindsValue", + "description": "Hide autocomplete" + }, + "prompt.autocomplete.select": { + "$ref": "#/$defs/KeybindsValue", + "description": "Select autocomplete item" + }, + "prompt.autocomplete.complete": { + "$ref": "#/$defs/KeybindsValue", + "description": "Complete autocomplete item" + }, + "permission.prompt.fullscreen": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle permission prompt fullscreen" + }, + "plugins.toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle plugin" + }, + "dialog.plugins.install": { + "$ref": "#/$defs/KeybindsValue", + "description": "Install plugin from plugin dialog" + }, + "terminal_suspend": { + "$ref": "#/$defs/KeybindsValue", + "description": "Suspend terminal" + }, + "terminal_title_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle terminal title" + }, + "tips_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle tips on home screen" + }, + "plugin_manager": { + "$ref": "#/$defs/KeybindsValue", + "description": "Open plugin manager dialog" + }, + "plugin_install": { + "$ref": "#/$defs/KeybindsValue", + "description": "Install plugin" + }, + "which_key_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle which-key panel" + }, + "which_key_layout_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Switch which-key layout" + }, + "which_key_pending_toggle": { + "$ref": "#/$defs/KeybindsValue", + "description": "Toggle which-key pending preview" + }, + "which_key_group_previous": { + "$ref": "#/$defs/KeybindsValue", + "description": "Previous which-key group" + }, + "which_key_group_next": { + "$ref": "#/$defs/KeybindsValue", + "description": "Next which-key group" + }, + "which_key_scroll_up": { + "$ref": "#/$defs/KeybindsValue", + "description": "Scroll which-key up" + }, + "which_key_scroll_down": { + "$ref": "#/$defs/KeybindsValue", + "description": "Scroll which-key down" + }, + "which_key_page_up": { + "$ref": "#/$defs/KeybindsValue", + "description": "Page which-key up" + }, + "which_key_page_down": { + "$ref": "#/$defs/KeybindsValue", + "description": "Page which-key down" + }, + "which_key_home": { + "$ref": "#/$defs/KeybindsValue", + "description": "Jump to first which-key binding" + }, + "which_key_end": { + "$ref": "#/$defs/KeybindsValue", + "description": "Jump to last which-key binding" + } + }, + "additionalProperties": false, + "description": "TUI keybinding overrides" + }, + "plugin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "object" + } + ], + "maxItems": 2, + "minItems": 2 + } + ] + } + }, + "plugin_enabled": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "leader_timeout": { + "exclusiveMinimum": 0, + "description": "Leader key timeout in milliseconds", + "type": "integer", + "maximum": 9007199254740991 + }, + "attention": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "notifications": { + "type": "boolean" + }, + "sound": { + "type": "boolean" + }, + "volume": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ], + "allOf": [ + { + "minimum": 0 + }, + { + "maximum": 1 + } + ] + }, + "sound_pack": { + "type": "string" + }, + "sounds": { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "question": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "error": { + "type": "string" + }, + "done": { + "type": "string" + }, + "subagent_done": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Attention notification and sound settings" + }, + "prompt": { + "type": "object", + "properties": { + "max_height": { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991, + "description": "Prompt textarea max height" + }, + "max_width": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer", + "maximum": 9007199254740991 + }, + { + "type": "string", + "enum": [ + "auto" + ] + } + ], + "description": "Home prompt max width: a positive integer for a fixed cap, or 'auto' to scale with terminal width" + } + }, + "additionalProperties": false, + "description": "Prompt size settings" + }, + "scroll_speed": { + "minimum": 0.001, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ], + "description": "TUI scroll speed" + }, + "scroll_acceleration": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable scroll acceleration" + } + }, + "required": [ + "enabled" + ], + "additionalProperties": false, + "description": "Scroll acceleration settings" + }, + "diff_style": { + "type": "string", + "enum": [ + "auto", + "stacked" + ], + "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column" + }, + "cursor": { + "type": "object", + "properties": { + "style": { + "type": "string", + "enum": [ + "block", + "underline", + "line", + "default" + ], + "description": "Cursor shape. Use 'default' to preserve the terminal setting" + }, + "blinking": { + "type": "boolean", + "description": "Whether the cursor blinks. Has no effect when style is 'default'" + } + }, + "additionalProperties": false, + "description": "Terminal cursor settings" + }, + "mouse": { + "type": "boolean", + "description": "Enable or disable mouse capture (default: true)" + } + }, + "additionalProperties": false, + "$defs": { + "KeybindsValue": { + "anyOf": [ + { + "type": "boolean", + "enum": [ + false + ] + }, + { + "type": "string", + "enum": [ + "none" + ] + }, + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "ctrl": { + "type": "boolean" + }, + "shift": { + "type": "boolean" + }, + "meta": { + "type": "boolean" + }, + "super": { + "type": "boolean" + }, + "hyper": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "ctrl": { + "type": "boolean" + }, + "shift": { + "type": "boolean" + }, + "meta": { + "type": "boolean" + }, + "super": { + "type": "boolean" + }, + "hyper": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + ] + }, + "event": { + "type": "string", + "enum": [ + "press", + "release" + ] + }, + "preventDefault": { + "type": "boolean" + }, + "fallthrough": { + "type": "boolean" + } + }, + "required": [ + "key" + ] + } + ] + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "ctrl": { + "type": "boolean" + }, + "shift": { + "type": "boolean" + }, + "meta": { + "type": "boolean" + }, + "super": { + "type": "boolean" + }, + "hyper": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "ctrl": { + "type": "boolean" + }, + "shift": { + "type": "boolean" + }, + "meta": { + "type": "boolean" + }, + "super": { + "type": "boolean" + }, + "hyper": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + ] + }, + "event": { + "type": "string", + "enum": [ + "press", + "release" + ] + }, + "preventDefault": { + "type": "boolean" + }, + "fallthrough": { + "type": "boolean" + } + }, + "required": [ + "key" + ] + } + ] + } + } + ] + } + }, + "allowComments": true, + "allowTrailingCommas": true +} diff --git a/packages/redrob/script/schema.ts b/packages/redrob/script/schema.ts index 7c877a8c89..9c58d017d4 100755 --- a/packages/redrob/script/schema.ts +++ b/packages/redrob/script/schema.ts @@ -1,26 +1,45 @@ #!/usr/bin/env bun -import { Config } from "@/config/config" +/** + * Emits the JSON Schemas that the Console serves and that config files point at via `$schema`: + * + * schema/config.json <- ConfigV1.Info ($id CONFIG_SCHEMA_URL, redrob.json / redrob.jsonc) + * schema/tui.json <- TuiConfig.Info ($id TUI_SCHEMA_URL, tui.json) + * schema/theme.json <- themeJsonSchema ($id THEME_SCHEMA_URL, TUI theme files; hand-written, + * the theme format has no runtime schema) + * + * bun run script/schema.ts write all three into packages/redrob/schema/ + * bun run check:generated same, then `git diff --exit-code -- schema` (CI gate) + * bun run script/schema.ts legacy: write only the config schema to (nix build) + */ + +import path from "path" +import { fileURLToPath } from "url" import { ConfigV1 } from "@redrob-code/core/v1/config/config" +import { CONFIG_SCHEMA_URL, THEME_SCHEMA_URL, TUI_SCHEMA_URL } from "@redrob-code/core/links" import { TuiConfig } from "@redrob-code/tui/config" +import { themeJsonSchema } from "@redrob-code/tui/theme/schema" import { Schema } from "effect" type JsonSchema = Record const MODEL_REF = "https://models.dev/model-schema.json#/$defs/Model" +const OUT_DIR = fileURLToPath(new URL("../schema/", import.meta.url)) -function generateEffect(schema: Schema.Top) { +function generateEffect(schema: Schema.Top, id: string) { const document = Schema.toJsonSchemaDocument(schema) const normalized = normalize({ $schema: "https://json-schema.org/draft/2020-12/schema", + $id: id, ...document.schema, $defs: document.definitions, }) if (!isRecord(normalized)) throw new Error("schema generator produced a non-object schema") const restored = restoreModelRefs(normalized) if (!isRecord(restored)) throw new Error("schema generator produced a non-object schema") - restored.allowComments = true - restored.allowTrailingCommas = true - return restored + const deduped = hoistRepeatedProperties(restored) + deduped.allowComments = true + deduped.allowTrailingCommas = true + return deduped } function normalize(value: unknown): unknown { @@ -61,17 +80,78 @@ function restoreModelRefs(value: unknown, key?: string): unknown { return schema } +/** + * `Schema.toJsonSchemaDocument` inlines a struct's field schema once per field. For a struct such as + * `TuiKeybind` -- ~185 keys that all share one ~1.7KB value schema and differ only in `description` + * -- that is a 1.1MB document. Whenever every property of an object (4 or more of them) has the same + * schema once `description` is stripped, hoist that schema into `$defs/Value` and leave a + * `$ref` plus the per-key description behind. Pure restructuring: the accepted documents are identical. + */ +function hoistRepeatedProperties(root: JsonSchema): JsonSchema { + const defs: JsonSchema = isRecord(root.$defs) ? { ...root.$defs } : {} + + const walk = (value: unknown, key: string): unknown => { + if (Array.isArray(value)) return value.map((item) => walk(item, key)) + if (!isRecord(value)) return value + const schema = Object.fromEntries(Object.entries(value).map(([name, item]) => [name, walk(item, name)])) + const props = schema.properties + if (!isRecord(props)) return schema + const entries = Object.entries(props) + if (entries.length < 4) return schema + const stripped = entries.map(([, item]) => { + if (!isRecord(item)) return undefined + const { description: _, ...rest } = item + return JSON.stringify(rest) + }) + const shape = stripped[0] + if (!shape || shape.length < 200 || stripped.some((item) => item !== shape)) return schema + const name = `${key.charAt(0).toUpperCase()}${key.slice(1)}Value` + if (name in defs && JSON.stringify(defs[name]) !== shape) return schema + defs[name] = JSON.parse(shape) + return { + ...schema, + properties: Object.fromEntries( + entries.map(([field, item]) => { + const description = isRecord(item) ? item.description : undefined + return [field, description === undefined ? { $ref: `#/$defs/${name}` } : { $ref: `#/$defs/${name}`, description }] + }), + ), + } + } + + const { $defs: _, ...rest } = root + const walked = walk(rest, "") + if (!isRecord(walked)) throw new Error("schema generator produced a non-object schema") + return Object.keys(defs).length ? { ...walked, $defs: defs } : walked +} + function isRecord(value: unknown): value is JsonSchema { return typeof value === "object" && value !== null && !Array.isArray(value) } -const configFile = process.argv[2] -const tuiFile = process.argv[3] +const render = (schema: JsonSchema) => `${JSON.stringify(schema, null, 2)}\n` + +const outputs = { + "config.json": () => generateEffect(ConfigV1.Info, CONFIG_SCHEMA_URL), + "tui.json": () => generateEffect(TuiConfig.Info, TUI_SCHEMA_URL), + "theme.json": () => themeJsonSchema(THEME_SCHEMA_URL), +} -console.log(configFile) -await Bun.write(configFile, JSON.stringify(generateEffect(ConfigV1.Info), null, 2)) +const arg = process.argv[2] -if (tuiFile) { - console.log(tuiFile) - await Bun.write(tuiFile, JSON.stringify(generateEffect(TuiConfig.Info), null, 2)) +if (arg) { + // Legacy single-file mode (nix/redrob.nix): the config schema only, to the given path. + console.log(arg) + await Bun.write(arg, render(outputs["config.json"]())) + // Keep the old optional second argument working. + if (process.argv[3]) { + console.log(process.argv[3]) + await Bun.write(process.argv[3], render(outputs["tui.json"]())) + } +} else { + for (const [name, build] of Object.entries(outputs)) { + const file = path.join(OUT_DIR, name) + console.log(path.relative(process.cwd(), file)) + await Bun.write(file, render(build())) + } } diff --git a/packages/redrob/specs/tui-plugins.md b/packages/redrob/specs/tui-plugins.md index 1d6e754b43..2f24dce516 100644 --- a/packages/redrob/specs/tui-plugins.md +++ b/packages/redrob/specs/tui-plugins.md @@ -18,7 +18,7 @@ Example: ```json { - "$schema": "https://code.redrob.ai/tui.json", + "$schema": "https://console.redrob.ai/code/tui.json", "theme": "smoke-theme", "leader_timeout": 2000, "keybinds": { diff --git a/packages/redrob/src/cli/cmd/github.handler.ts b/packages/redrob/src/cli/cmd/github.handler.ts index 3596a3ee09..58af47a3df 100644 --- a/packages/redrob/src/cli/cmd/github.handler.ts +++ b/packages/redrob/src/cli/cmd/github.handler.ts @@ -1,5 +1,4 @@ import path from "path" -import { exec } from "child_process" import { Filesystem } from "@/util/filesystem" import * as prompts from "@clack/prompts" import { map, pipe, sortBy, values } from "remeda" @@ -140,7 +139,9 @@ type IssueQueryResponse = { } } -const AGENT_USERNAME = "redrob-agent[bot]" +// Commits the agent pushes are authored as the Actions bot, which is the identity GITHUB_TOKEN acts as. +const GIT_AUTHOR_NAME = "github-actions[bot]" +const GIT_AUTHOR_EMAIL = "41898282+github-actions[bot]@users.noreply.github.com" const AGENT_REACTION = "eyes" const WORKFLOW_FILE = ".github/workflows/redrob.yml" @@ -165,7 +166,6 @@ export const githubInstall = Effect.fn("Cli.github.install")(function* () { UI.empty() prompts.intro("Install GitHub agent") const app = await getAppInfo() - await installGitHubApp() const providers = await Effect.runPromise(modelsDev.get()).then((p) => { // TODO: add guide for copilot, for now just hide it @@ -200,9 +200,10 @@ export const githubInstall = Effect.fn("Cli.github.install")(function* () { ` 1. Commit the \`${WORKFLOW_FILE}\` file and push`, step2, "", - " 3. Go to a GitHub issue and comment `/oc summarize` to see the agent in action", + " 3. Go to a GitHub issue and comment `/redrob summarize` to see the agent in action", "", - " Learn more about the GitHub agent - https://code.redrob.ai/docs/github/#usage-examples", + " The workflow authenticates with the repository's own GITHUB_TOKEN; no GitHub App is involved.", + " Learn more about the GitHub agent - https://console.redrob.ai/docs/github", ].join("\n"), ) } @@ -277,59 +278,6 @@ export const githubInstall = Effect.fn("Cli.github.install")(function* () { return model } - async function installGitHubApp() { - const s = prompts.spinner() - s.start("Installing GitHub app") - - // Get installation - const installation = await getInstallation() - if (installation) return s.stop("GitHub app already installed") - - // Open browser - const url = "https://github.com/apps/redrob-agent" - const command = - process.platform === "darwin" - ? `open "${url}"` - : process.platform === "win32" - ? `start "" "${url}"` - : `xdg-open "${url}"` - - exec(command, (error) => { - if (error) { - prompts.log.warn(`Could not open browser. Please visit: ${url}`) - } - }) - - // Wait for installation - s.message("Waiting for GitHub app to be installed") - const MAX_RETRIES = 120 - let retries = 0 - do { - const installation = await getInstallation() - if (installation) break - - if (retries > MAX_RETRIES) { - s.stop( - `Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`, - ) - throw new UI.CancelledError() - } - - retries++ - await sleep(1000) - } while (true) // oxlint-disable-line no-constant-condition - - s.stop("Installed GitHub app") - - async function getInstallation() { - return await fetch( - `https://api.code.redrob.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`, - ) - .then((res) => res.json()) - .then((data) => data.installation) - } - } - async function addWorkflowFiles() { const envStr = provider === "amazon-bedrock" @@ -355,10 +303,9 @@ jobs: startsWith(github.event.comment.body, '/redrob') runs-on: ubuntu-latest permissions: - id-token: write - contents: read - pull-requests: read - issues: read + contents: write + pull-requests: write + issues: write steps: - name: Checkout repository uses: actions/checkout@v6 @@ -368,7 +315,8 @@ jobs: - name: Run redrob uses: redrob-labs/redrob-code/github@latest${envStr} with: - model: ${provider}/${model}`, + model: ${provider}/${model} + token: \${{ secrets.GITHUB_TOKEN }}`, ) prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`) @@ -410,7 +358,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: const variant = process.env["VARIANT"] || undefined const runId = normalizeRunId() const share = normalizeShare() - const oidcBaseUrl = normalizeOidcBaseUrl() const { owner, repo } = context.repo // For repo events (schedule, workflow_dispatch), payload has no issue/comment data const payload = context.payload as @@ -430,21 +377,24 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: ? (payload as IssueCommentEvent | IssuesEvent).issue.number : (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number const runUrl = `/${owner}/${repo}/actions/runs/${runId}` - const shareBaseUrl = isMock ? "https://dev.code.redrob.ai" : "https://code.redrob.ai" - let appToken: string + // The token the workflow handed us: GITHUB_TOKEN or a PAT. There is no Redrob GitHub App and + // no token-exchange service, so this is the only credential the agent ever holds. + let githubToken: string let octoRest: Octokit let octoGraph: typeof graphql let gitConfig: string let session: { id: SessionID; title: string; version: string } - let shareId: string | undefined + // The share service returns the public URL; nothing here builds one from a base URL. + let shareUrl: string | undefined + // The reaction this run added, so it can be removed by id instead of by guessing the bot's login. + let reactionId: number | undefined let exitCode = 0 let githubClientReady = false type PromptFiles = Awaited>["promptFiles"] const triggerCommentId = isCommentEvent ? (payload as IssueCommentEvent | PullRequestReviewCommentEvent).comment.id : undefined - const useGithubToken = normalizeUseGithubToken() const commentType = isCommentEvent ? context.eventName === "pull_request_review_comment" ? "pr_review" @@ -472,28 +422,15 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: } try { - if (useGithubToken) { - const githubToken = process.env["GITHUB_TOKEN"] - if (!githubToken) { - throw new Error( - "GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.", - ) - } - appToken = githubToken - } else { - const actionToken = isMock ? args.token! : await getOidcToken() - appToken = await exchangeForAppToken(actionToken) - } - octoRest = new Octokit({ auth: appToken }) + githubToken = resolveGithubToken() + octoRest = new Octokit({ auth: githubToken }) octoGraph = graphql.defaults({ - headers: { authorization: `token ${appToken}` }, + headers: { authorization: `token ${githubToken}` }, }) githubClientReady = true const { userPrompt, promptFiles } = await getUserPrompt() - if (!useGithubToken) { - await configureGit(appToken) - } + await configureGit(githubToken) // Skip permission check and reactions for repo events (no actor to check, no issue to react to) if (isUserEvent) { await assertPermissions() @@ -514,11 +451,11 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: }), ) await subscribeSessionEvents() - shareId = await (async () => { + shareUrl = await (async () => { if (share === false) return if (!share && repoData.data.private) return - await runLocalEffect(sessionShare.share(session.id)) - return session.id.slice(-8) + const shared = await runLocalEffect(sessionShare.share(session.id)) + return shared.url })() console.log("redrob session", session.id) @@ -578,7 +515,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: const summary = await summarize(response) await pushToLocalBranch(summary, uncommittedChanges) } - const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`)) + const hasShared = prData.comments.nodes.some((c) => shareUrl !== undefined && c.body.includes(shareUrl)) await createComment(`${response}${footer({ image: !hasShared })}`) await removeReaction(commentType) } @@ -596,7 +533,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: const summary = await summarize(response) await pushToForkBranch(summary, prData, uncommittedChanges) } - const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`)) + const hasShared = prData.comments.nodes.some((c) => shareUrl !== undefined && c.body.includes(shareUrl)) await createComment(`${response}${footer({ image: !hasShared })}`) await removeReaction(commentType) } @@ -655,10 +592,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: // Also output the clean error message for the action to capture //core.setOutput("prepare_error", e.message); } finally { - if (!useGithubToken) { - await restoreGitConfig() - await revokeAppToken() - } + await restoreGitConfig() } process.exit(exitCode) @@ -687,18 +621,17 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: throw new Error(`Invalid share value: ${value}. Share must be a boolean.`) } - function normalizeUseGithubToken() { - const value = process.env["USE_GITHUB_TOKEN"] - if (!value) return false - if (value === "true") return true - if (value === "false") return false - throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`) - } - - function normalizeOidcBaseUrl(): string { - const value = process.env["OIDC_BASE_URL"] - if (!value) return "https://api.code.redrob.ai" - return value.replace(/\/+$/, "") + function resolveGithubToken() { + const value = args.token || process.env["GITHUB_TOKEN"] + if (value) return value + throw new Error( + [ + "No GitHub token was provided.", + "Set the GITHUB_TOKEN environment variable (the action does this from its `token` input, which", + "defaults to the workflow's own secrets.GITHUB_TOKEN), or pass --token with a personal access token", + "that has `contents: write`, `pull-requests: write` and `issues: write`.", + ].join(" "), + ) } function isIssueCommentEvent( @@ -800,7 +733,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: // Download image const res = await fetch(url, { headers: { - Authorization: `Bearer ${appToken}`, + Authorization: `Bearer ${githubToken}`, Accept: "application/vnd.github.v3+json", }, }) @@ -983,45 +916,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: ) } - async function getOidcToken() { - try { - return await core.getIDToken("redrob-github-action") - } catch (error) { - console.error("Failed to get OIDC token:", error instanceof Error ? error.message : error) - throw new Error( - "Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.", - { cause: error }, - ) - } - } - - async function exchangeForAppToken(token: string) { - const response = token.startsWith("github_pat_") - ? await fetch(`${oidcBaseUrl}/exchange_github_app_token_with_pat`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ owner, repo }), - }) - : await fetch(`${oidcBaseUrl}/exchange_github_app_token`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - if (!response.ok) { - throw new Error( - `App token exchange failed: ${response.status} ${response.statusText} - ${await response.text()}`, - ) - } - - const responseJson = (await response.json()) as { token: string } - return responseJson.token - } - - async function configureGit(appToken: string) { + async function configureGit(token: string) { // Do not change git config when running locally if (isMock) return @@ -1035,11 +930,11 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: await gitRun(["config", "--local", "--unset-all", config]) } - const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64") + const newCredentials = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64") await gitRun(["config", "--local", config, `AUTHORIZATION: basic ${newCredentials}`]) - await gitRun(["config", "--global", "user.name", AGENT_USERNAME]) - await gitRun(["config", "--global", "user.email", `${AGENT_USERNAME}@users.noreply.github.com`]) + await gitRun(["config", "--global", "user.name", GIT_AUTHOR_NAME]) + await gitRun(["config", "--global", "user.email", GIT_AUTHOR_EMAIL]) } async function restoreGitConfig() { @@ -1189,86 +1084,59 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: async function addReaction(commentType?: "issue" | "pr_review") { // Only called for non-schedule events, so triggerCommentId is defined console.log("Adding reaction...") - if (triggerCommentId) { - if (commentType === "pr_review") { - return await octoRest.rest.reactions.createForPullRequestReviewComment({ + const created = await (async () => { + if (triggerCommentId) { + if (commentType === "pr_review") { + return await octoRest.rest.reactions.createForPullRequestReviewComment({ + owner, + repo, + comment_id: triggerCommentId!, + content: AGENT_REACTION, + }) + } + return await octoRest.rest.reactions.createForIssueComment({ owner, repo, comment_id: triggerCommentId!, content: AGENT_REACTION, }) } - return await octoRest.rest.reactions.createForIssueComment({ + return await octoRest.rest.reactions.createForIssue({ owner, repo, - comment_id: triggerCommentId!, + issue_number: issueId!, content: AGENT_REACTION, }) - } - return await octoRest.rest.reactions.createForIssue({ - owner, - repo, - issue_number: issueId!, - content: AGENT_REACTION, - }) + })() + reactionId = created.data.id + return created } async function removeReaction(commentType?: "issue" | "pr_review") { // Only called for non-schedule events, so triggerCommentId is defined console.log("Removing reaction...") + if (reactionId === undefined) return if (triggerCommentId) { if (commentType === "pr_review") { - const reactions = await octoRest.rest.reactions.listForPullRequestReviewComment({ - owner, - repo, - comment_id: triggerCommentId!, - content: AGENT_REACTION, - }) - - const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME) - if (!eyesReaction) return - return await octoRest.rest.reactions.deleteForPullRequestComment({ owner, repo, comment_id: triggerCommentId!, - reaction_id: eyesReaction.id, + reaction_id: reactionId, }) } - - const reactions = await octoRest.rest.reactions.listForIssueComment({ - owner, - repo, - comment_id: triggerCommentId!, - content: AGENT_REACTION, - }) - - const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME) - if (!eyesReaction) return - return await octoRest.rest.reactions.deleteForIssueComment({ owner, repo, comment_id: triggerCommentId!, - reaction_id: eyesReaction.id, + reaction_id: reactionId, }) } - - const reactions = await octoRest.rest.reactions.listForIssue({ - owner, - repo, - issue_number: issueId!, - content: AGENT_REACTION, - }) - - const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME) - if (!eyesReaction) return - await octoRest.rest.reactions.deleteForIssue({ owner, repo, issue_number: issueId!, - reaction_id: eyesReaction.id, + reaction_id: reactionId, }) } @@ -1354,16 +1222,17 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: function footer(opts?: { image?: boolean }) { const image = (() => { - if (!shareId) return "" + if (!shareUrl) return "" if (!opts?.image) return "" const titleAlt = encodeURIComponent(session.title.substring(0, 50)) const title64 = Buffer.from(session.title.substring(0, 700), "utf8").toString("base64") + const shareId = shareUrl.split("/").pop() - return `${titleAlt}\n` + return `${titleAlt}\n` })() - const shareUrl = shareId ? `[redrob session](${shareBaseUrl}/s/${shareId})  |  ` : "" - return `\n\n${image}${shareUrl}[github run](${runUrl})` + const shareLink = shareUrl ? `[redrob session](${shareUrl})  |  ` : "" + return `\n\n${image}${shareLink}[github run](${runUrl})` } async function fetchRepo() { @@ -1590,18 +1459,5 @@ query($owner: String!, $repo: String!, $number: Int!) { "", ].join("\n") } - - async function revokeAppToken() { - if (!appToken) return - - await fetch("https://api.github.com/installation/token", { - method: "DELETE", - headers: { - Authorization: `Bearer ${appToken}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - }) - } }) }) diff --git a/packages/redrob/src/cli/cmd/providers.ts b/packages/redrob/src/cli/cmd/providers.ts index 5131657044..95a0be48d7 100644 --- a/packages/redrob/src/cli/cmd/providers.ts +++ b/packages/redrob/src/cli/cmd/providers.ts @@ -471,7 +471,7 @@ export const ProvidersLoginCommand = effectCmd({ if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) { yield* Prompt.log.info( - "Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://code.redrob.ai/docs/providers/#cloudflare-ai-gateway", + "Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://console.redrob.ai/docs/providers/#cloudflare-ai-gateway", ) } diff --git a/packages/redrob/src/config/config.ts b/packages/redrob/src/config/config.ts index dcdcc78edd..d2f5774fe6 100644 --- a/packages/redrob/src/config/config.ts +++ b/packages/redrob/src/config/config.ts @@ -20,6 +20,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/ import { EffectFlock } from "@redrob-code/core/util/effect-flock" import { containsPath, type InstanceContext } from "../project/instance-context" import { ConfigV1 } from "@redrob-code/core/v1/config/config" +import { CONFIG_SCHEMA_URL } from "@redrob-code/core/links" import { RemoteAuthError } from "@redrob-code/core/v1/config/error" import { ConfigPermissionV1 } from "@redrob-code/core/v1/config/permission" import { ConfigPluginV1 } from "@redrob-code/core/v1/config/plugin" @@ -223,8 +224,8 @@ const layer = Layer.effect( yield* Effect.promise(() => resolveLoadedPlugins(data, options.path)) if (!data.$schema) { - data.$schema = "https://code.redrob.ai/config.json" - const updated = text.replace(/^\s*\{/, '{\n "$schema": "https://code.redrob.ai/config.json",') + data.$schema = CONFIG_SCHEMA_URL + const updated = text.replace(/^\s*\{/, `{\n "$schema": "${CONFIG_SCHEMA_URL}",`) yield* fs.writeFileString(options.path, updated).pipe(Effect.catch(() => Effect.void)) } return data @@ -245,7 +246,7 @@ const layer = Layer.effect( const file = globalConfigFile() if (!existsSync(file)) { yield* fs - .writeWithDirs(file, JSON.stringify({ $schema: "https://code.redrob.ai/config.json" }, null, 2)) + .writeWithDirs(file, JSON.stringify({ $schema: CONFIG_SCHEMA_URL }, null, 2)) .pipe(Effect.catch(() => Effect.void)) } } @@ -260,7 +261,7 @@ const layer = Layer.effect( .then(async (mod) => { const { provider, model, ...rest } = mod.default if (provider && model) result.model = `${provider}/${model}` - result["$schema"] = "https://code.redrob.ai/config.json" + result["$schema"] = CONFIG_SCHEMA_URL result = mergeConfig(result, rest) await fsNode.writeFile(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2)) await fsNode.unlink(legacy) @@ -374,7 +375,7 @@ const layer = Layer.effect( }) : {} const remoteConfig = mergeConfig(isRecord(wellknown.config) ? wellknown.config : {}, fetchedConfig) - if (!remoteConfig.$schema) remoteConfig.$schema = "https://code.redrob.ai/config.json" + if (!remoteConfig.$schema) remoteConfig.$schema = CONFIG_SCHEMA_URL const source = wellknownURL const next = yield* loadConfig( JSON.stringify(remoteConfig), diff --git a/packages/redrob/src/config/tui-migrate.ts b/packages/redrob/src/config/tui-migrate.ts index 37550f7c70..ce8441736f 100644 --- a/packages/redrob/src/config/tui-migrate.ts +++ b/packages/redrob/src/config/tui-migrate.ts @@ -7,8 +7,7 @@ import { Flag } from "@redrob-code/core/flag/flag" import { Global } from "@redrob-code/core/global" import { Filesystem } from "@/util/filesystem" import * as ConfigPaths from "@/config/paths" - -const TUI_SCHEMA_URL = "https://code.redrob.ai/tui.json" +import { TUI_SCHEMA_URL } from "@redrob-code/core/links" const decodeTheme = Schema.decodeUnknownOption(Schema.String) const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown)) diff --git a/packages/redrob/src/mcp/oauth-provider.ts b/packages/redrob/src/mcp/oauth-provider.ts index ccc0baa3ef..944407b2c1 100644 --- a/packages/redrob/src/mcp/oauth-provider.ts +++ b/packages/redrob/src/mcp/oauth-provider.ts @@ -44,7 +44,7 @@ export class McpOAuthProvider implements OAuthClientProvider { return { redirect_uris: [this.redirectUrl], client_name: "RedrobCode", - client_uri: "https://code.redrob.ai", + client_uri: "https://console.redrob.ai/code", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none", diff --git a/packages/redrob/src/provider/provider.ts b/packages/redrob/src/provider/provider.ts index 65eeedf393..b082960473 100644 --- a/packages/redrob/src/provider/provider.ts +++ b/packages/redrob/src/provider/provider.ts @@ -488,8 +488,8 @@ function custom(dep: CustomDep): Record { autoload: false, options: { headers: { - "HTTP-Referer": "https://code.redrob.ai/", - "X-Title": "redrob", + "HTTP-Referer": "https://console.redrob.ai/code", + "X-Title": "Redrob Code", "X-Source": "redrob", }, }, @@ -499,8 +499,8 @@ function custom(dep: CustomDep): Record { autoload: false, options: { headers: { - "HTTP-Referer": "https://code.redrob.ai/", - "X-Title": "redrob", + "HTTP-Referer": "https://console.redrob.ai/code", + "X-Title": "Redrob Code", }, }, }), @@ -509,8 +509,8 @@ function custom(dep: CustomDep): Record { autoload: provider.source === "config", options: { headers: { - "HTTP-Referer": "https://code.redrob.ai/", - "X-Title": "redrob", + "HTTP-Referer": "https://console.redrob.ai/code", + "X-Title": "Redrob Code", "X-BILLING-INVOKE-ORIGIN": "RedrobCode", }, }, @@ -520,7 +520,7 @@ function custom(dep: CustomDep): Record { autoload: false, options: { headers: { - "http-referer": "https://code.redrob.ai/", + "http-referer": "https://console.redrob.ai/code", "x-title": "redrob", }, }, @@ -625,8 +625,8 @@ function custom(dep: CustomDep): Record { autoload: false, options: { headers: { - "HTTP-Referer": "https://code.redrob.ai/", - "X-Title": "redrob", + "HTTP-Referer": "https://console.redrob.ai/code", + "X-Title": "Redrob Code", }, }, }), @@ -915,8 +915,8 @@ function custom(dep: CustomDep): Record { autoload: false, options: { headers: { - "HTTP-Referer": "https://code.redrob.ai/", - "X-Title": "redrob", + "HTTP-Referer": "https://console.redrob.ai/code", + "X-Title": "Redrob Code", }, }, }), diff --git a/packages/redrob/src/session/prompt/anthropic.txt b/packages/redrob/src/session/prompt/anthropic.txt index 569072d09e..df3e2b6b5a 100644 --- a/packages/redrob/src/session/prompt/anthropic.txt +++ b/packages/redrob/src/session/prompt/anthropic.txt @@ -9,7 +9,7 @@ If the user asks for help or wants to give feedback inform them of the following - To give feedback, users should report the issue at https://github.com/redrob-labs/redrob-code -When the user directly asks about Redrob Code (eg. "can Redrob Code do...", "does Redrob Code have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific Redrob Code feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from Redrob Code docs. The list of available docs is available at https://code.redrob.ai/docs +When the user directly asks about Redrob Code (eg. "can Redrob Code do...", "does Redrob Code have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific Redrob Code feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from Redrob Code docs. The list of available docs is available at https://console.redrob.ai/docs # Tone and style - Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked. diff --git a/packages/redrob/src/session/prompt/default.txt b/packages/redrob/src/session/prompt/default.txt index d1e8e914a0..24a1a0e2ff 100644 --- a/packages/redrob/src/session/prompt/default.txt +++ b/packages/redrob/src/session/prompt/default.txt @@ -6,7 +6,7 @@ If the user asks for help or wants to give feedback inform them of the following - /help: Get help with using redrob - To give feedback, users should report the issue at https://github.com/redrob-labs/redrob-code/issues -When the user directly asks about redrob (eg 'can redrob do...', 'does redrob have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from redrob docs at https://code.redrob.ai +When the user directly asks about redrob (eg 'can redrob do...', 'does redrob have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from redrob docs at https://console.redrob.ai/docs # Tone and style You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system). diff --git a/packages/redrob/src/session/prompt/meta.txt b/packages/redrob/src/session/prompt/meta.txt index 4c09df218e..d3c34d2f41 100644 --- a/packages/redrob/src/session/prompt/meta.txt +++ b/packages/redrob/src/session/prompt/meta.txt @@ -62,4 +62,4 @@ Use the instructions below and the tools available to assist the user. # User Help & Feedback - Users can give feedback or report issues at https://github.com/redrob-labs/redrob-code and mention that they are using Meta {{MODEL_NAME}}. -- When users ask directly about Redrob Code (eg. "can Redrob Code do...", "are you able to do...") or its features (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from the Redrob Code docs at https://code.redrob.ai/docs. +- When users ask directly about Redrob Code (eg. "can Redrob Code do...", "are you able to do...") or its features (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from the Redrob Code docs at https://console.redrob.ai/docs. diff --git a/packages/redrob/src/session/retry.ts b/packages/redrob/src/session/retry.ts index 95da7b46a8..371ebc2648 100644 --- a/packages/redrob/src/session/retry.ts +++ b/packages/redrob/src/session/retry.ts @@ -8,7 +8,6 @@ import { isRecord } from "@/util/record" export type Err = ReturnType export const GO_UPSELL_MESSAGE = "Free usage exceeded, subscribe to Go" -export const GO_UPSELL_URL = "https://code.redrob.ai/go" /** * Where a console refusal sends the user. `/limits` shows the tier and its per-minute ceiling; * `/api-keys` is where a key's monthly cap is raised; `/billing` is where the account is topped up. @@ -18,6 +17,8 @@ export const GO_UPSELL_URL = "https://code.redrob.ai/go" export const CONSOLE_LIMITS_URL = "https://console.redrob.ai/limits" export const CONSOLE_KEYS_URL = "https://console.redrob.ai/api-keys" export const CONSOLE_BILLING_URL = "https://console.redrob.ai/billing" +/** Where the legacy "Go" upsell sends the user: there is no Go storefront, the Console billing page is it. */ +export const GO_UPSELL_URL = CONSOLE_BILLING_URL export type RetryReason = | "free_tier_limit" | "account_rate_limit" @@ -233,7 +234,6 @@ export function retryable(error: Err, provider: string) { } if (error.data.responseBody?.includes("GoUsageLimitError")) { const body = parseJSON(error.data.responseBody) - const workspace = str(body?.metadata?.workspace) const limitName = str(body?.metadata?.limitName) const retryAfter = num(error.data.responseHeaders?.["retry-after"]) const resetIn = iife(() => { @@ -251,7 +251,7 @@ export function retryable(error: Err, provider: string) { const message = `${limitName ? `${limitName} usage limit` : "Usage limit"} reached. It will reset in ${resetIn}. To continue using this model now, enable usage from your available balance` - const link = `https://code.redrob.ai/workspace/${workspace}/go` + const link = CONSOLE_LIMITS_URL return { message: `${message} - ${link}`, action: { diff --git a/packages/redrob/test/cli/run/footer.view.test.tsx b/packages/redrob/test/cli/run/footer.view.test.tsx index c04512683f..1f169985ed 100644 --- a/packages/redrob/test/cli/run/footer.view.test.tsx +++ b/packages/redrob/test/cli/run/footer.view.test.tsx @@ -59,7 +59,7 @@ function model(input: { providerID: "opencode", api: { id: "opencode", - url: "https://code.redrob.ai", + url: "https://console.redrob.ai/code", npm: "@ai-sdk/openai-compatible", }, name: input.name, diff --git a/packages/redrob/test/config/config.test.ts b/packages/redrob/test/config/config.test.ts index 3d51a50fed..51a9026618 100644 --- a/packages/redrob/test/config/config.test.ts +++ b/packages/redrob/test/config/config.test.ts @@ -110,7 +110,7 @@ const layer = configLayer() const it = testEffect(layer) const configIt = (options?: Parameters[0]) => testEffect(configLayer(options)) -const schemaConfig = (config: object) => ({ $schema: "https://code.redrob.ai/config.json", ...config }) +const schemaConfig = (config: object) => ({ $schema: "https://console.redrob.ai/code/config.json", ...config }) const provideCurrentInstance = (effect: Effect.Effect, ctx: InstanceContext) => effect.pipe(Effect.provideService(InstanceRef, ctx)) @@ -268,7 +268,7 @@ async function check(map: (dir: string) => string) { await clear() try { await writeConfig(globalTmp.path, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", snapshot: false, }) await withTestInstance({ @@ -314,7 +314,7 @@ it.effect("creates global jsonc config with schema when no global configs exist" yield* Config.use.get().pipe(provideInstanceEffect(dir)) const content = yield* FSUtil.use.readFileString(path.join(dir, "redrob.jsonc")) - expect(content).toContain('"$schema": "https://code.redrob.ai/config.json"') + expect(content).toContain('"$schema": "https://console.redrob.ai/code/config.json"') }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))), ), ) @@ -360,7 +360,7 @@ it.instance("updates config and preserves empty shell sentinel", () => const test = yield* TestInstance yield* writeConfigEffect( test.directory, - { $schema: "https://code.redrob.ai/config.json", shell: "bash" }, + { $schema: "https://console.redrob.ai/code/config.json", shell: "bash" }, "config.json", ) @@ -436,7 +436,7 @@ it.instance("ignores legacy tui keys in redrob config", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", model: "test/model", theme: "legacy", tui: { scroll_speed: 4 }, @@ -456,7 +456,7 @@ it.instance("loads JSONC config file", () => path.join(test.directory, "redrob.jsonc"), `{ // This is a comment - "$schema": "https://code.redrob.ai/config.json", + "$schema": "https://console.redrob.ai/code/config.json", "model": "test/model", "username": "testuser" }`, @@ -473,14 +473,14 @@ it.instance("jsonc overrides json in the same directory", () => yield* writeConfigEffect( test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", model: "base", username: "base", }, "redrob.jsonc", ) yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", model: "override", }) const config = yield* Config.use.get() @@ -496,7 +496,7 @@ it.instance("handles environment variable substitution", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", username: "{env:TEST_VAR}", }) const config = yield* Config.use.get() @@ -533,7 +533,7 @@ it.instance("handles file inclusion substitution", () => const test = yield* TestInstance yield* FSUtil.use.writeWithDirs(path.join(test.directory, "included.txt"), "test-user") yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", username: "{file:included.txt}", }) const config = yield* Config.use.get() @@ -546,7 +546,7 @@ it.instance("handles file inclusion with replacement tokens", () => const test = yield* TestInstance yield* FSUtil.use.writeWithDirs(path.join(test.directory, "included.md"), "const out = await Bun.$`echo hi`") yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", username: "{file:included.md}", }) const config = yield* Config.use.get() @@ -558,7 +558,7 @@ it.instance("validates config schema and throws on invalid values", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", model: 42, }) const exit = yield* Config.use.get().pipe(Effect.exit) @@ -579,7 +579,7 @@ it.instance("handles agent configuration", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test_agent: { model: "test/model", @@ -603,7 +603,7 @@ it.instance("treats agent variant as model-scoped setting (not provider option)" Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test_agent: { model: "openai/gpt-5.2", @@ -627,7 +627,7 @@ it.instance("handles command configuration", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", command: { test_command: { template: "test template", @@ -649,7 +649,7 @@ it.instance("migrates autoshare to share field", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", autoshare: true, }) const config = yield* Config.use.get() @@ -662,7 +662,7 @@ it.instance("migrates mode field to agent field", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", mode: { test_mode: { model: "test/model", @@ -685,7 +685,7 @@ it.instance("accepts the deprecated reference field", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", reference: { local: { path: "../library" }, sdk: { repository: "github.com/example/sdk", branch: "main" }, @@ -1095,7 +1095,7 @@ it.instance("migrates legacy tools config to permissions - allow", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test: { tools: { bash: true, read: true } } }, }) @@ -1111,7 +1111,7 @@ it.instance("migrates legacy tools config to permissions - deny", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test: { tools: { bash: false, webfetch: false } } }, }) @@ -1127,7 +1127,7 @@ it.instance("migrates legacy write tool to edit permission", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test: { tools: { write: true } } }, }) @@ -1143,7 +1143,7 @@ it.instance( "managed settings override user settings", Effect.gen(function* () { yield* writeManagedSettingsEffect({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", model: "managed/model", share: "disabled", }) @@ -1160,7 +1160,7 @@ it.instance( "managed settings override project settings", Effect.gen(function* () { yield* writeManagedSettingsEffect({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", autoupdate: false, disabled_providers: ["openai"], }) @@ -1195,7 +1195,7 @@ it.instance("migrates legacy edit tool to edit permission", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test: { tools: { edit: false } } }, }) @@ -1208,7 +1208,7 @@ it.instance("migrates legacy patch tool to edit permission", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test: { tools: { patch: true } } }, }) @@ -1221,7 +1221,7 @@ it.instance("migrates mixed legacy tools config", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test: { tools: { bash: true, write: true, read: false, webfetch: true } } }, }) @@ -1239,7 +1239,7 @@ it.instance("merges legacy tools with existing permission config", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", agent: { test: { permission: { glob: "allow" }, tools: { bash: true } } }, }) @@ -1257,7 +1257,7 @@ it.instance("permission config preserves user key order", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", permission: { "*": "deny", edit: "ask", @@ -1313,7 +1313,7 @@ it.instance("project config can override MCP server enabled status", () => const test = yield* TestInstance // Simulates a base config (like from remote .well-known) with disabled MCP. yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", mcp: { jira: { type: "remote", @@ -1331,7 +1331,7 @@ it.instance("project config can override MCP server enabled status", () => yield* writeConfigEffect( test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", mcp: { jira: { type: "remote", @@ -1361,7 +1361,7 @@ it.instance("MCP config deep merges preserving base config properties", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", mcp: { myserver: { type: "remote", @@ -1376,7 +1376,7 @@ it.instance("MCP config deep merges preserving base config properties", () => yield* writeConfigEffect( test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", mcp: { myserver: { type: "remote", @@ -1404,7 +1404,7 @@ it.instance("local .redrob config can override MCP from project config", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", mcp: { docs: { type: "remote", @@ -1417,7 +1417,7 @@ it.instance("local .redrob config can override MCP from project config", () => yield* writeConfigEffect( path.join(test.directory, ".redrob"), { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", mcp: { docs: { type: "remote", @@ -1446,7 +1446,7 @@ it.instance("loads app-managed directory config from the state dir, below the pr yield* writeConfigEffect( dir, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", snapshot: false, mcp: { docs: { type: "remote", url: "https://docs.example.com/mcp", enabled: false }, @@ -1456,7 +1456,7 @@ it.instance("loads app-managed directory config from the state dir, below the pr "redrob.json", ) yield* writeConfigEffect(test.directory, { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", mcp: { docs: { type: "remote", url: "https://docs.example.com/mcp", enabled: true } }, }) @@ -1901,7 +1901,7 @@ describe("REDROB_CONFIG_CONTENT token substitution", () => { withProcessEnv( "REDROB_CONFIG_CONTENT", JSON.stringify({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", username: "{env:TEST_CONFIG_VAR}", }), Effect.gen(function* () { @@ -1919,7 +1919,7 @@ describe("REDROB_CONFIG_CONTENT token substitution", () => { yield* withProcessEnv( "REDROB_CONFIG_CONTENT", JSON.stringify({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", username: "{file:./api_key.txt}", }), Effect.gen(function* () { @@ -1967,7 +1967,7 @@ test("parseManagedPlist parses server settings", async () => { ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", server: { hostname: "127.0.0.1", mdns: false }, autoupdate: true, }), @@ -1987,7 +1987,7 @@ test("parseManagedPlist parses permission rules", async () => { ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", permission: { "*": "ask", bash: { "*": "ask", "rm -rf *": "deny", "curl *": "deny" }, @@ -2017,7 +2017,7 @@ test("parseManagedPlist parses enabled_providers", async () => { ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", enabled_providers: ["anthropic", "google"], }), ), @@ -2032,10 +2032,10 @@ test("parseManagedPlist handles empty config", async () => { const config = ConfigParse.schema( ConfigV1.Info, ConfigParse.jsonc( - await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://code.redrob.ai/config.json" })), + await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json" })), "test:mobileconfig", ), "test:mobileconfig", ) - expect(config.$schema).toBe("https://code.redrob.ai/config.json") + expect(config.$schema).toBe("https://console.redrob.ai/code/config.json") }) diff --git a/packages/redrob/test/fixture/fixture.ts b/packages/redrob/test/fixture/fixture.ts index 0b8a32b125..f3f245b13d 100644 --- a/packages/redrob/test/fixture/fixture.ts +++ b/packages/redrob/test/fixture/fixture.ts @@ -96,7 +96,7 @@ export async function tmpdir(options?: TmpDirOptions) { await Bun.write( path.join(dirpath, "redrob.json"), JSON.stringify({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", ...options.config, }), ) @@ -154,7 +154,7 @@ export function tmpdirScoped(options?: { yield* Effect.promise(() => fs.writeFile( path.join(dir, "redrob.json"), - JSON.stringify({ $schema: "https://code.redrob.ai/config.json", ...resolved }), + JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json", ...resolved }), ), ) } diff --git a/packages/redrob/test/installation/installation.test.ts b/packages/redrob/test/installation/installation.test.ts index 45ee43aba2..bd0246c06f 100644 --- a/packages/redrob/test/installation/installation.test.ts +++ b/packages/redrob/test/installation/installation.test.ts @@ -154,7 +154,7 @@ describe("installation", () => { Effect.gen(function* () { yield* Installation.use.upgrade("curl", "9.9.9") expect(scriptCalls).toEqual([Installation.CONSOLE_INSTALL_SCRIPT_URL]) - expect(scriptCalls.some((url) => url.includes("code.redrob.ai/install"))).toBe(false) + expect(scriptCalls.some((url) => !url.startsWith("https://console.redrob.ai/"))).toBe(false) // The name the script reads. `VERSION` is the other installer's variable and this one // ignores it, which is an upgrade that silently reinstalls whatever `latest` is. expect(scriptEnv[0]?.["REDROB_CODE_VERSION"]).toBe("9.9.9") diff --git a/packages/redrob/test/plugin/trigger.test.ts b/packages/redrob/test/plugin/trigger.test.ts index 786372ac09..cea158eb98 100644 --- a/packages/redrob/test/plugin/trigger.test.ts +++ b/packages/redrob/test/plugin/trigger.test.ts @@ -41,7 +41,7 @@ function withProject(source: string, self: Effect.Effect) { path.join(test.directory, "redrob.json"), JSON.stringify( { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", plugin: [pathToFileURL(file).href], }, null, diff --git a/packages/redrob/test/plugin/workspace-adapter.test.ts b/packages/redrob/test/plugin/workspace-adapter.test.ts index 3a119a90b2..5010d074cf 100644 --- a/packages/redrob/test/plugin/workspace-adapter.test.ts +++ b/packages/redrob/test/plugin/workspace-adapter.test.ts @@ -74,7 +74,7 @@ describe("plugin.workspace", () => { path.join(dir, "redrob.json"), JSON.stringify( { - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", plugin: [pathToFileURL(file).href], }, null, diff --git a/packages/redrob/test/project/instance-bootstrap.test.ts b/packages/redrob/test/project/instance-bootstrap.test.ts index 9a80473ef4..7648cb6e11 100644 --- a/packages/redrob/test/project/instance-bootstrap.test.ts +++ b/packages/redrob/test/project/instance-bootstrap.test.ts @@ -52,7 +52,7 @@ const bootstrapFixture = Effect.gen(function* () { Bun.write( path.join(dir, "redrob.json"), JSON.stringify({ - $schema: "https://code.redrob.ai/config.json", + $schema: "https://console.redrob.ai/code/config.json", plugin: [pathToFileURL(pluginFile).href], }), ), diff --git a/packages/redrob/test/provider/gitlab-duo.test.ts b/packages/redrob/test/provider/gitlab-duo.test.ts index 0d66b7d200..48e6d2863b 100644 --- a/packages/redrob/test/provider/gitlab-duo.test.ts +++ b/packages/redrob/test/provider/gitlab-duo.test.ts @@ -19,7 +19,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // }), // ) // }, @@ -43,7 +43,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // provider: { // gitlab: { // options: { @@ -75,7 +75,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // }), // ) // }, @@ -112,7 +112,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // }), // ) // }, @@ -148,7 +148,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // provider: { // gitlab: { // options: { @@ -180,7 +180,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // provider: { // gitlab: { // options: { @@ -210,7 +210,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // }), // ) // }, @@ -236,7 +236,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // provider: { // gitlab: { // options: { @@ -271,7 +271,7 @@ export {} // await Bun.write( // path.join(dir, "redrob.json"), // JSON.stringify({ -// $schema: "https://code.redrob.ai/config.json", +// $schema: "https://console.redrob.ai/code/config.json", // }), // ) // }, @@ -297,7 +297,7 @@ export {} // test("duo-workflow-* model routes through workflowChat", async () => { // await using tmp = await tmpdir({ // init: async (dir) => { -// await Bun.write(path.join(dir, "redrob.json"), JSON.stringify({ $schema: "https://code.redrob.ai/config.json" })) +// await Bun.write(path.join(dir, "redrob.json"), JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json" })) // }, // }) // await withTestInstance({ @@ -345,7 +345,7 @@ export {} // test("duo-chat-* model routes through agenticChat (not workflow)", async () => { // await using tmp = await tmpdir({ // init: async (dir) => { -// await Bun.write(path.join(dir, "redrob.json"), JSON.stringify({ $schema: "https://code.redrob.ai/config.json" })) +// await Bun.write(path.join(dir, "redrob.json"), JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json" })) // }, // }) // await withTestInstance({ @@ -368,7 +368,7 @@ export {} // test("model.options merged with provider.options in getLanguage", async () => { // await using tmp = await tmpdir({ // init: async (dir) => { -// await Bun.write(path.join(dir, "redrob.json"), JSON.stringify({ $schema: "https://code.redrob.ai/config.json" })) +// await Bun.write(path.join(dir, "redrob.json"), JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json" })) // }, // }) // await withTestInstance({ @@ -392,7 +392,7 @@ export {} // test("static duo-chat models always present regardless of discovery", async () => { // await using tmp = await tmpdir({ // init: async (dir) => { -// await Bun.write(path.join(dir, "redrob.json"), JSON.stringify({ $schema: "https://code.redrob.ai/config.json" })) +// await Bun.write(path.join(dir, "redrob.json"), JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json" })) // }, // }) // await withTestInstance({ diff --git a/packages/redrob/test/provider/provider.test.ts b/packages/redrob/test/provider/provider.test.ts index 55f8363426..aa5116349c 100644 --- a/packages/redrob/test/provider/provider.test.ts +++ b/packages/redrob/test/provider/provider.test.ts @@ -1313,8 +1313,8 @@ it.instance( Effect.gen(function* () { const providers = yield* list expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({ - "HTTP-Referer": "https://code.redrob.ai/", - "X-Title": "redrob", + "HTTP-Referer": "https://console.redrob.ai/code", + "X-Title": "Redrob Code", "X-BILLING-INVOKE-ORIGIN": "RedrobCode", }) }), @@ -1326,8 +1326,8 @@ it.instance( Effect.gen(function* () { const providers = yield* list expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({ - "HTTP-Referer": "https://code.redrob.ai/", - "X-Title": "redrob", + "HTTP-Referer": "https://console.redrob.ai/code", + "X-Title": "Redrob Code", "X-BILLING-INVOKE-ORIGIN": "RedrobCode", }) }), diff --git a/packages/redrob/test/provider/transform.test.ts b/packages/redrob/test/provider/transform.test.ts index fd38065475..06bcb67d37 100644 --- a/packages/redrob/test/provider/transform.test.ts +++ b/packages/redrob/test/provider/transform.test.ts @@ -2768,7 +2768,7 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( providerID: "opencode", api: { id: "redrob-test", - url: "https://api.code.redrob.ai", + url: "https://console.redrob.ai/api/backend/v1", npm: "@ai-sdk/openai-compatible", }, } @@ -2802,7 +2802,7 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( providerID: "opencode", api: { id: "redrob-test", - url: "https://api.code.redrob.ai", + url: "https://console.redrob.ai/api/backend/v1", npm: "@ai-sdk/openai-compatible", }, } diff --git a/packages/redrob/test/server/httpapi-cors.test.ts b/packages/redrob/test/server/httpapi-cors.test.ts index cea9bd4054..229d6a440c 100644 --- a/packages/redrob/test/server/httpapi-cors.test.ts +++ b/packages/redrob/test/server/httpapi-cors.test.ts @@ -93,7 +93,7 @@ describe("HttpApi CORS", () => { Effect.gen(function* () { const handler = webHandler() - for (const origin of ["https://app.code.redrob.ai", "https://console.redrob.ai"]) { + for (const origin of ["https://app.redrob.ai", "https://console.redrob.ai"]) { const response = yield* Effect.promise(() => handler( new Request(new URL("/global/config", "http://localhost"), { headers: { origin } }), @@ -108,17 +108,17 @@ describe("HttpApi CORS", () => { it.live("allows a redrob.ai origin only when explicitly configured", () => Effect.gen(function* () { - const handler = webHandler({ cors: ["https://app.code.redrob.ai"] }) + const handler = webHandler({ cors: ["https://app.redrob.ai"] }) const configured = yield* Effect.promise(() => handler( new Request(new URL("/global/config", "http://localhost"), { - headers: { origin: "https://app.code.redrob.ai" }, + headers: { origin: "https://app.redrob.ai" }, }), HttpApiApp.context, ), ) - expect(configured.headers.get("access-control-allow-origin")).toBe("https://app.code.redrob.ai") + expect(configured.headers.get("access-control-allow-origin")).toBe("https://app.redrob.ai") const other = yield* Effect.promise(() => handler( diff --git a/packages/redrob/test/server/httpapi-workspace.test.ts b/packages/redrob/test/server/httpapi-workspace.test.ts index cd287f1cf4..642adcee77 100644 --- a/packages/redrob/test/server/httpapi-workspace.test.ts +++ b/packages/redrob/test/server/httpapi-workspace.test.ts @@ -399,7 +399,7 @@ describe("workspace HttpApi", () => { "content-type": "application/json", "x-redrob-workspace": "internal", }, - body: JSON.stringify({ $schema: "https://code.redrob.ai/config.json" }), + body: JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json" }), }) const responseBody = yield* response.text @@ -416,7 +416,7 @@ describe("workspace HttpApi", () => { "content-type": "application/json", "x-target-auth": "secret", }), - body: JSON.stringify({ $schema: "https://code.redrob.ai/config.json" }), + body: JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json" }), }, ]) expect(forwarded[0]?.headers).not.toHaveProperty("x-redrob-directory") diff --git a/packages/redrob/test/session/llm-native-recorded.test.ts b/packages/redrob/test/session/llm-native-recorded.test.ts index 7e4739e20f..3c33bb5a17 100644 --- a/packages/redrob/test/session/llm-native-recorded.test.ts +++ b/packages/redrob/test/session/llm-native-recorded.test.ts @@ -328,7 +328,7 @@ const writeConfig = (directory: string, scenario: RecordedScenario, model: Model Effect.promise(() => Bun.write( path.join(directory, "redrob.json"), - JSON.stringify({ $schema: "https://code.redrob.ai/config.json", ...scenario.config(model) }), + JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json", ...scenario.config(model) }), ), ) diff --git a/packages/redrob/test/session/prompt.test.ts b/packages/redrob/test/session/prompt.test.ts index 7569f78b61..2c194e633d 100644 --- a/packages/redrob/test/session/prompt.test.ts +++ b/packages/redrob/test/session/prompt.test.ts @@ -311,7 +311,7 @@ const writeText = Effect.fn("test.writeText")(function* (file: string, text: str const writeConfig = Effect.fn("test.writeConfig")(function* (dir: string, config: Partial) { yield* writeText( path.join(dir, "redrob.json"), - JSON.stringify({ $schema: "https://code.redrob.ai/config.json", ...config }), + JSON.stringify({ $schema: "https://console.redrob.ai/code/config.json", ...config }), ) }) diff --git a/packages/redrob/test/session/retry.test.ts b/packages/redrob/test/session/retry.test.ts index 01f7bfd1ac..a8f9dc6d38 100644 --- a/packages/redrob/test/session/retry.test.ts +++ b/packages/redrob/test/session/retry.test.ts @@ -386,7 +386,7 @@ describe("session.retry.retryable", () => { expect(SessionRetry.retryable(error, "redrob-go")).toEqual({ message: - "5 hour usage limit reached. It will reset in 5 hours 23 minutes. To continue using this model now, enable usage from your available balance - https://code.redrob.ai/workspace/wrk_01K6XGM22R6FM8JVABE9XDQXGH/go", + "5 hour usage limit reached. It will reset in 5 hours 23 minutes. To continue using this model now, enable usage from your available balance - https://console.redrob.ai/limits", action: { reason: "account_rate_limit", provider: "redrob-go", @@ -394,7 +394,7 @@ describe("session.retry.retryable", () => { message: "5 hour usage limit reached. It will reset in 5 hours 23 minutes. To continue using this model now, enable usage from your available balance", label: "open settings", - link: "https://code.redrob.ai/workspace/wrk_01K6XGM22R6FM8JVABE9XDQXGH/go", + link: "https://console.redrob.ai/limits", }, }) }) diff --git a/packages/redrob/test/session/schema-decoding.test.ts b/packages/redrob/test/session/schema-decoding.test.ts index cb5959248d..3f27ae802e 100644 --- a/packages/redrob/test/session/schema-decoding.test.ts +++ b/packages/redrob/test/session/schema-decoding.test.ts @@ -240,7 +240,7 @@ describe("SessionStatus.Info", () => { title: "Free limit reached", message: "Subscribe to RedrobCode Go.", label: "subscribe", - link: "https://code.redrob.ai/go", + link: "https://console.redrob.ai/billing", }, next: 500, } diff --git a/packages/redrob/test/tool/fixtures/models-api.json b/packages/redrob/test/tool/fixtures/models-api.json index 1ae783dda4..3f730ab14f 100644 --- a/packages/redrob/test/tool/fixtures/models-api.json +++ b/packages/redrob/test/tool/fixtures/models-api.json @@ -33607,9 +33607,9 @@ "id": "redrob-go", "env": ["REDROB_API_KEY"], "npm": "@ai-sdk/openai-compatible", - "api": "https://code.redrob.ai/go/v1", + "api": "https://console.redrob.ai/api/backend/v1", "name": "Redrob Console Go", - "doc": "https://code.redrob.ai/docs", + "doc": "https://console.redrob.ai/docs", "models": { "deepseek-v4-flash": { "id": "deepseek-v4-flash", @@ -90323,9 +90323,9 @@ "id": "redrob", "env": ["REDROB_API_KEY"], "npm": "@ai-sdk/openai-compatible", - "api": "https://code.redrob.ai/v1", + "api": "https://console.redrob.ai/api/backend/v1", "name": "Redrob Console", - "doc": "https://code.redrob.ai/docs", + "doc": "https://console.redrob.ai/docs", "models": { "ring-2.6-1t-free": { "id": "ring-2.6-1t-free", diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 3ea6999154..a41ed237fe 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1209,7 +1209,7 @@ export type Config = { diff_style?: "auto" | "stacked" } /** - * Command configuration, see https://code.redrob.ai/docs/commands + * Command configuration, see https://console.redrob.ai/docs/commands */ command?: { [key: string]: { @@ -1266,7 +1266,7 @@ export type Config = { [key: string]: AgentConfig | undefined } /** - * Agent configuration, see https://code.redrob.ai/docs/agent + * Agent configuration, see https://console.redrob.ai/docs/agents */ agent?: { plan?: AgentConfig diff --git a/packages/tui/package.json b/packages/tui/package.json index eb39561654..f579b8ffdb 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -22,6 +22,7 @@ "./context/sdk": "./src/context/sdk.tsx", "./context/sync": "./src/context/sync.tsx", "./context/theme": "./src/context/theme.tsx", + "./theme/schema": "./src/theme/schema.ts", "./context/editor": "./src/context/editor.ts", "./context/clipboard": "./src/context/clipboard.tsx", "./attention": "./src/attention.ts", diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index cb12027373..fbd8ef5fed 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -842,7 +842,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi name: "docs.open", title: language.t("command.docs.open"), run: () => { - open("https://code.redrob.ai/docs").catch(() => {}) + open("https://console.redrob.ai/docs").catch(() => {}) dialog.clear() }, category: "System", diff --git a/packages/tui/src/component/dialog-provider.tsx b/packages/tui/src/component/dialog-provider.tsx index 553fdecbe0..de7ab87b43 100644 --- a/packages/tui/src/component/dialog-provider.tsx +++ b/packages/tui/src/component/dialog-provider.tsx @@ -353,7 +353,7 @@ function ApiMethod(props: ApiMethodProps) { {language.t("provider.auth.redrob_go.description")} - {language.t("provider.auth.redrob_go.link", { url: "https://code.redrob.ai/go" })} + {language.t("provider.auth.redrob_go.link", { url: "https://console.redrob.ai/billing" })} ), diff --git a/packages/tui/src/component/dialog-retry-action.tsx b/packages/tui/src/component/dialog-retry-action.tsx index 4efdaeede2..fba5d833f7 100644 --- a/packages/tui/src/component/dialog-retry-action.tsx +++ b/packages/tui/src/component/dialog-retry-action.tsx @@ -8,7 +8,7 @@ import { BgPulse } from "./bg-pulse" import { useBindings } from "../keymap" import { useLanguage } from "../context/language" -const GO_URL = "https://code.redrob.ai/go" +const GO_URL = "https://console.redrob.ai/billing" const PAD_X = 3 const PAD_TOP_OUTER = 1 const FOREGROUND_ALPHA = 186 diff --git a/packages/tui/src/i18n/en.ts b/packages/tui/src/i18n/en.ts index dc989d7d89..530b12af87 100644 --- a/packages/tui/src/i18n/en.ts +++ b/packages/tui/src/i18n/en.ts @@ -803,7 +803,7 @@ export const dict = { "tip.agent_cycle": "Press {{shortcut}} to cycle between Build and Plan agents", "tip.undo": "Use {highlight}/undo{/highlight} to revert the last message and file changes", "tip.redo": "Use {highlight}/redo{/highlight} to restore previously undone messages and file changes", - "tip.share": "Run {highlight}/share{/highlight} to create a public code.redrob.ai link", + "tip.share": "Run {highlight}/share{/highlight} to create a public share link", "tip.drag_drop": "Drag and drop images or PDFs into the terminal as context", "tip.paste_images": "Press {{shortcut}} to paste images from your clipboard into the prompt", "tip.editor": "Use {{command}} to compose messages in your external editor", diff --git a/packages/tui/src/i18n/ko.ts b/packages/tui/src/i18n/ko.ts index 211ab55600..5e89eb0844 100644 --- a/packages/tui/src/i18n/ko.ts +++ b/packages/tui/src/i18n/ko.ts @@ -771,7 +771,7 @@ export const dict: Record = { "tip.agent_cycle": "{{shortcut}} 키를 누르면 Build 에이전트와 Plan 에이전트를 번갈아 사용할 수 있습니다", "tip.undo": "{highlight}/undo{/highlight}로 마지막 메시지와 파일 변경을 되돌립니다", "tip.redo": "{highlight}/redo{/highlight}로 되돌린 메시지와 파일 변경을 복원합니다", - "tip.share": "{highlight}/share{/highlight}를 실행하면 공개 code.redrob.ai 링크가 만들어집니다", + "tip.share": "{highlight}/share{/highlight}를 실행하면 공개 공유 링크가 만들어집니다", "tip.drag_drop": "이미지나 PDF를 터미널로 끌어다 놓으면 컨텍스트로 추가됩니다", "tip.paste_images": "{{shortcut}} 키를 누르면 클립보드의 이미지를 프롬프트에 붙여넣습니다", "tip.editor": "{{command}} 명령으로 외부 편집기에서 메시지를 작성할 수 있습니다", diff --git a/packages/tui/src/theme/assets/aura.json b/packages/tui/src/theme/assets/aura.json index 2ebc0ab08c..70954e6b2d 100644 --- a/packages/tui/src/theme/assets/aura.json +++ b/packages/tui/src/theme/assets/aura.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkBg": "#0f0f0f", "darkBgPanel": "#15141b", diff --git a/packages/tui/src/theme/assets/ayu.json b/packages/tui/src/theme/assets/ayu.json index 060b0b7bad..805a8a17f8 100644 --- a/packages/tui/src/theme/assets/ayu.json +++ b/packages/tui/src/theme/assets/ayu.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkBg": "#0B0E14", "darkBgAlt": "#0D1017", diff --git a/packages/tui/src/theme/assets/carbonfox.json b/packages/tui/src/theme/assets/carbonfox.json index c156b44fc2..ab3294175c 100644 --- a/packages/tui/src/theme/assets/carbonfox.json +++ b/packages/tui/src/theme/assets/carbonfox.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "bg0": "#0d0d0d", "bg1": "#161616", diff --git a/packages/tui/src/theme/assets/catppuccin-frappe.json b/packages/tui/src/theme/assets/catppuccin-frappe.json index 4634d77dcc..dc477965d5 100644 --- a/packages/tui/src/theme/assets/catppuccin-frappe.json +++ b/packages/tui/src/theme/assets/catppuccin-frappe.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "frappeRosewater": "#f2d5cf", "frappeFlamingo": "#eebebe", diff --git a/packages/tui/src/theme/assets/catppuccin-macchiato.json b/packages/tui/src/theme/assets/catppuccin-macchiato.json index 8dc56ae4d4..da424ea3bf 100644 --- a/packages/tui/src/theme/assets/catppuccin-macchiato.json +++ b/packages/tui/src/theme/assets/catppuccin-macchiato.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "macRosewater": "#f4dbd6", "macFlamingo": "#f0c6c6", diff --git a/packages/tui/src/theme/assets/catppuccin.json b/packages/tui/src/theme/assets/catppuccin.json index dd5fbf9b0b..2b1604f3be 100644 --- a/packages/tui/src/theme/assets/catppuccin.json +++ b/packages/tui/src/theme/assets/catppuccin.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "lightRosewater": "#dc8a78", "lightFlamingo": "#dd7878", diff --git a/packages/tui/src/theme/assets/cobalt2.json b/packages/tui/src/theme/assets/cobalt2.json index 951e525700..9fcb1bc09a 100644 --- a/packages/tui/src/theme/assets/cobalt2.json +++ b/packages/tui/src/theme/assets/cobalt2.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "background": "#193549", "backgroundAlt": "#122738", diff --git a/packages/tui/src/theme/assets/cursor.json b/packages/tui/src/theme/assets/cursor.json index 89233c1dac..c97bd856c1 100644 --- a/packages/tui/src/theme/assets/cursor.json +++ b/packages/tui/src/theme/assets/cursor.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkBg": "#181818", "darkPanel": "#141414", diff --git a/packages/tui/src/theme/assets/dracula.json b/packages/tui/src/theme/assets/dracula.json index 428582856d..2b5f3ec972 100644 --- a/packages/tui/src/theme/assets/dracula.json +++ b/packages/tui/src/theme/assets/dracula.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "background": "#282a36", "currentLine": "#44475a", diff --git a/packages/tui/src/theme/assets/everforest.json b/packages/tui/src/theme/assets/everforest.json index e1961fe3e6..fb43a45e69 100644 --- a/packages/tui/src/theme/assets/everforest.json +++ b/packages/tui/src/theme/assets/everforest.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkStep1": "#2d353b", "darkStep2": "#333c43", diff --git a/packages/tui/src/theme/assets/flexoki.json b/packages/tui/src/theme/assets/flexoki.json index b998e6513d..856e45901d 100644 --- a/packages/tui/src/theme/assets/flexoki.json +++ b/packages/tui/src/theme/assets/flexoki.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "black": "#100F0F", "base950": "#1C1B1A", diff --git a/packages/tui/src/theme/assets/github.json b/packages/tui/src/theme/assets/github.json index c25ea4b342..b215b5dba8 100644 --- a/packages/tui/src/theme/assets/github.json +++ b/packages/tui/src/theme/assets/github.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkBg": "#0d1117", "darkBgAlt": "#010409", diff --git a/packages/tui/src/theme/assets/gruvbox.json b/packages/tui/src/theme/assets/gruvbox.json index 126d04c09a..3025d7f552 100644 --- a/packages/tui/src/theme/assets/gruvbox.json +++ b/packages/tui/src/theme/assets/gruvbox.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkBg0": "#282828", "darkBg1": "#3c3836", diff --git a/packages/tui/src/theme/assets/kanagawa.json b/packages/tui/src/theme/assets/kanagawa.json index aa4c27ea17..3037f22de2 100644 --- a/packages/tui/src/theme/assets/kanagawa.json +++ b/packages/tui/src/theme/assets/kanagawa.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "sumiInk0": "#1F1F28", "sumiInk1": "#2A2A37", diff --git a/packages/tui/src/theme/assets/lucent-orng.json b/packages/tui/src/theme/assets/lucent-orng.json index c1040769c7..3a0b188976 100644 --- a/packages/tui/src/theme/assets/lucent-orng.json +++ b/packages/tui/src/theme/assets/lucent-orng.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkStep6": "#3c3c3c", "darkStep11": "#808080", diff --git a/packages/tui/src/theme/assets/material.json b/packages/tui/src/theme/assets/material.json index fd65b25b01..d7d147edcd 100644 --- a/packages/tui/src/theme/assets/material.json +++ b/packages/tui/src/theme/assets/material.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkBg": "#263238", "darkBgAlt": "#1e272c", diff --git a/packages/tui/src/theme/assets/matrix.json b/packages/tui/src/theme/assets/matrix.json index b63ef5f07f..69c7001692 100644 --- a/packages/tui/src/theme/assets/matrix.json +++ b/packages/tui/src/theme/assets/matrix.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "matrixInk0": "#0a0e0a", "matrixInk1": "#0e130d", diff --git a/packages/tui/src/theme/assets/mercury.json b/packages/tui/src/theme/assets/mercury.json index b43dac7277..1d4b57b558 100644 --- a/packages/tui/src/theme/assets/mercury.json +++ b/packages/tui/src/theme/assets/mercury.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "purple-800": "#3442a6", "purple-700": "#465bd1", diff --git a/packages/tui/src/theme/assets/monokai.json b/packages/tui/src/theme/assets/monokai.json index c141aae422..c4e3d6be23 100644 --- a/packages/tui/src/theme/assets/monokai.json +++ b/packages/tui/src/theme/assets/monokai.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "background": "#272822", "backgroundAlt": "#1e1f1c", diff --git a/packages/tui/src/theme/assets/nightowl.json b/packages/tui/src/theme/assets/nightowl.json index 6efe476b81..8dd3306e37 100644 --- a/packages/tui/src/theme/assets/nightowl.json +++ b/packages/tui/src/theme/assets/nightowl.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "nightOwlBg": "#011627", "nightOwlFg": "#d6deeb", diff --git a/packages/tui/src/theme/assets/nord.json b/packages/tui/src/theme/assets/nord.json index 1bb722dcdb..ca90b6efaf 100644 --- a/packages/tui/src/theme/assets/nord.json +++ b/packages/tui/src/theme/assets/nord.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "nord0": "#2E3440", "nord1": "#3B4252", diff --git a/packages/tui/src/theme/assets/one-dark.json b/packages/tui/src/theme/assets/one-dark.json index a7621374b2..7199173bdf 100644 --- a/packages/tui/src/theme/assets/one-dark.json +++ b/packages/tui/src/theme/assets/one-dark.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkBg": "#282c34", "darkBgAlt": "#21252b", diff --git a/packages/tui/src/theme/assets/orng.json b/packages/tui/src/theme/assets/orng.json index bad4a0a21d..3cb6d944c0 100644 --- a/packages/tui/src/theme/assets/orng.json +++ b/packages/tui/src/theme/assets/orng.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkStep1": "#0a0a0a", "darkStep2": "#141414", diff --git a/packages/tui/src/theme/assets/osaka-jade.json b/packages/tui/src/theme/assets/osaka-jade.json index 370d7852e5..11879afff0 100644 --- a/packages/tui/src/theme/assets/osaka-jade.json +++ b/packages/tui/src/theme/assets/osaka-jade.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkBg0": "#111c18", "darkBg1": "#1a2520", diff --git a/packages/tui/src/theme/assets/palenight.json b/packages/tui/src/theme/assets/palenight.json index 345870986b..03756450f3 100644 --- a/packages/tui/src/theme/assets/palenight.json +++ b/packages/tui/src/theme/assets/palenight.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "background": "#292d3e", "backgroundAlt": "#1e2132", diff --git a/packages/tui/src/theme/assets/redrob.json b/packages/tui/src/theme/assets/redrob.json index 2cd4dde820..8ab2558fbc 100644 --- a/packages/tui/src/theme/assets/redrob.json +++ b/packages/tui/src/theme/assets/redrob.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "blue4": "#8aafff", "blue5": "#507fff", diff --git a/packages/tui/src/theme/assets/rosepine.json b/packages/tui/src/theme/assets/rosepine.json index 509fd368f9..a702844daf 100644 --- a/packages/tui/src/theme/assets/rosepine.json +++ b/packages/tui/src/theme/assets/rosepine.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "base": "#191724", "surface": "#1f1d2e", diff --git a/packages/tui/src/theme/assets/solarized.json b/packages/tui/src/theme/assets/solarized.json index 1b2fc37649..f2777ab460 100644 --- a/packages/tui/src/theme/assets/solarized.json +++ b/packages/tui/src/theme/assets/solarized.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "base03": "#002b36", "base02": "#073642", diff --git a/packages/tui/src/theme/assets/synthwave84.json b/packages/tui/src/theme/assets/synthwave84.json index 6ebed98018..3e310ab8fa 100644 --- a/packages/tui/src/theme/assets/synthwave84.json +++ b/packages/tui/src/theme/assets/synthwave84.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "background": "#262335", "backgroundAlt": "#1e1a29", diff --git a/packages/tui/src/theme/assets/tokyonight.json b/packages/tui/src/theme/assets/tokyonight.json index aee9fc8ab7..cd54c0b39f 100644 --- a/packages/tui/src/theme/assets/tokyonight.json +++ b/packages/tui/src/theme/assets/tokyonight.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "darkStep1": "#1a1b26", "darkStep2": "#1e2030", diff --git a/packages/tui/src/theme/assets/vercel.json b/packages/tui/src/theme/assets/vercel.json index 2c49c36b60..dc3a3699b2 100644 --- a/packages/tui/src/theme/assets/vercel.json +++ b/packages/tui/src/theme/assets/vercel.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "background100": "#0A0A0A", "background200": "#000000", diff --git a/packages/tui/src/theme/assets/vesper.json b/packages/tui/src/theme/assets/vesper.json index e6be9250a7..b634e09ca6 100644 --- a/packages/tui/src/theme/assets/vesper.json +++ b/packages/tui/src/theme/assets/vesper.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "vesperBg": "#101010", "vesperFg": "#FFF", diff --git a/packages/tui/src/theme/assets/zenburn.json b/packages/tui/src/theme/assets/zenburn.json index f904c42a46..3accf077d1 100644 --- a/packages/tui/src/theme/assets/zenburn.json +++ b/packages/tui/src/theme/assets/zenburn.json @@ -1,5 +1,5 @@ { - "$schema": "https://code.redrob.ai/theme.json", + "$schema": "https://console.redrob.ai/code/theme.json", "defs": { "bg": "#3f3f3f", "bgAlt": "#4f4f4f", diff --git a/packages/tui/src/theme/schema.ts b/packages/tui/src/theme/schema.ts new file mode 100644 index 0000000000..7b45b70776 --- /dev/null +++ b/packages/tui/src/theme/schema.ts @@ -0,0 +1,158 @@ +import type { Theme } from "." + +/** + * JSON Schema for a TUI theme file (`{ "$schema": THEME_SCHEMA_URL, "defs": {...}, "theme": {...} }`). + * + * The theme format has no runtime schema -- `ThemeJson` in `./index.ts` is a plain TypeScript type and + * `resolveTheme` is the only validator -- so this document is written by hand. The colour-key list is + * checked against the `Theme` type: adding a colour to `Theme` without listing it here fails + * `typecheck`, and listing one that does not exist fails too. `redrob generate --schema` writes the + * result to `packages/redrob/schema/theme.json`, which `check:generated` keeps in step. + */ + +type ThemeColor = Exclude +type OptionalColor = "selectedListItemText" | "backgroundMenu" +type RequiredColor = Exclude + +// `satisfies` plus the two Exact* checks below make this list exactly the set of colour keys. +export const THEME_REQUIRED_COLORS = [ + "primary", + "secondary", + "accent", + "error", + "warning", + "success", + "info", + "text", + "textMuted", + "background", + "backgroundPanel", + "backgroundElement", + "border", + "borderActive", + "borderSubtle", + "diffAdded", + "diffRemoved", + "diffContext", + "diffHunkHeader", + "diffHighlightAdded", + "diffHighlightRemoved", + "diffAddedBg", + "diffRemovedBg", + "diffContextBg", + "diffLineNumber", + "diffAddedLineNumberBg", + "diffRemovedLineNumberBg", + "markdownText", + "markdownHeading", + "markdownLink", + "markdownLinkText", + "markdownCode", + "markdownBlockQuote", + "markdownEmph", + "markdownStrong", + "markdownHorizontalRule", + "markdownListItem", + "markdownListEnumeration", + "markdownImage", + "markdownImageText", + "markdownCodeBlock", + "syntaxComment", + "syntaxKeyword", + "syntaxFunction", + "syntaxVariable", + "syntaxString", + "syntaxNumber", + "syntaxType", + "syntaxOperator", + "syntaxPunctuation", +] as const satisfies readonly RequiredColor[] + +export const THEME_OPTIONAL_COLORS = ["selectedListItemText", "backgroundMenu"] as const satisfies readonly OptionalColor[] + +// Compile-time exhaustiveness: every RequiredColor / OptionalColor must appear in its list. +type MissingRequired = Exclude +type MissingOptional = Exclude +const _exactRequired: MissingRequired extends never ? true : never = true +const _exactOptional: MissingOptional extends never ? true : never = true +void _exactRequired +void _exactOptional + +export const HEX_COLOR_PATTERN = "^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" + +/** + * Builds the schema document. `$id` is where the Console serves it; editors fetch it from a theme + * file's `$schema` field. + */ +export function themeJsonSchema(id: string): Record { + const colorValue = { $ref: "#/$defs/ColorValue" } + const properties: Record = {} + for (const key of THEME_REQUIRED_COLORS) properties[key] = colorValue + for (const key of THEME_OPTIONAL_COLORS) properties[key] = colorValue + properties.thinkingOpacity = { + type: "number", + minimum: 0, + maximum: 1, + description: "Opacity applied to thinking/reasoning text. Defaults to 0.6.", + } + + return { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: id, + title: "Redrob Code TUI theme", + description: + "A colour theme for the Redrob Code terminal interface. `defs` holds named colours; `theme` maps every interface role to a colour, a `defs` name, another role, or a dark/light pair.", + type: "object", + properties: { + $schema: { type: "string" }, + defs: { + type: "object", + description: "Named colours referenced from `theme` (or from other defs).", + additionalProperties: { $ref: "#/$defs/HexOrRef" }, + }, + theme: { + type: "object", + properties, + required: [...THEME_REQUIRED_COLORS], + additionalProperties: false, + }, + }, + required: ["theme"], + additionalProperties: false, + $defs: { + HexColor: { + type: "string", + pattern: HEX_COLOR_PATTERN, + description: "Hex colour: #RGB, #RGBA, #RRGGBB or #RRGGBBAA, e.g. #8aafff", + }, + RefName: { + type: "string", + description: + "The name of an entry in `defs`, the name of another role in `theme`, or `transparent` / `none` for no colour.", + not: { pattern: "^#" }, + }, + HexOrRef: { anyOf: [{ $ref: "#/$defs/HexColor" }, { $ref: "#/$defs/RefName" }] }, + AnsiIndex: { + type: "integer", + minimum: 0, + maximum: 255, + description: "An ANSI 256-colour palette index.", + }, + Variant: { + type: "object", + description: "A colour that differs between dark and light terminals.", + properties: { + dark: { $ref: "#/$defs/HexOrRef" }, + light: { $ref: "#/$defs/HexOrRef" }, + }, + required: ["dark", "light"], + additionalProperties: false, + }, + ColorValue: { + anyOf: [{ $ref: "#/$defs/HexOrRef" }, { $ref: "#/$defs/AnsiIndex" }, { $ref: "#/$defs/Variant" }], + }, + }, + allowComments: true, + allowTrailingCommas: true, + } +} diff --git a/packages/tui/test/theme-schema.test.ts b/packages/tui/test/theme-schema.test.ts new file mode 100644 index 0000000000..d097282a10 --- /dev/null +++ b/packages/tui/test/theme-schema.test.ts @@ -0,0 +1,67 @@ +import { expect, test } from "bun:test" +import { THEME_SCHEMA_URL } from "@redrob-code/core/links" +import { DEFAULT_THEMES } from "../src/theme" +import { HEX_COLOR_PATTERN, THEME_OPTIONAL_COLORS, THEME_REQUIRED_COLORS, themeJsonSchema } from "../src/theme/schema" + +// The theme format has no runtime validator, so the hand-written schema is checked against the one +// corpus that is known-good: every bundled theme. A theme the schema would reject is a schema bug. + +const schema = themeJsonSchema(THEME_SCHEMA_URL) +const hex = new RegExp(HEX_COLOR_PATTERN) +const themeProps = (schema.properties as any).theme as { properties: Record; required: string[] } + +test("schema is stamped with the Console URL every theme file points at", () => { + expect(schema.$id).toBe(THEME_SCHEMA_URL) + for (const [name, theme] of Object.entries(DEFAULT_THEMES)) { + expect(theme.$schema, name).toBe(THEME_SCHEMA_URL) + } +}) + +test("every bundled theme fits the schema's key set", () => { + const allowed = new Set(Object.keys(themeProps.properties)) + for (const [name, theme] of Object.entries(DEFAULT_THEMES)) { + const keys = Object.keys(theme.theme) + const unknown = keys.filter((key) => !allowed.has(key)) + expect(unknown, `${name} has keys the schema does not know`).toEqual([]) + const missing = themeProps.required.filter((key) => !keys.includes(key)) + expect(missing, `${name} lacks keys the schema requires`).toEqual([]) + } + expect(themeProps.required).toEqual([...THEME_REQUIRED_COLORS]) + for (const key of THEME_OPTIONAL_COLORS) expect(themeProps.required).not.toContain(key) +}) + +test("every colour value in the bundled themes is a hex, a resolvable ref, an ANSI index or a variant", () => { + for (const [name, theme] of Object.entries(DEFAULT_THEMES)) { + const defs = theme.defs ?? {} + const roles = new Set(Object.keys(theme.theme)) + const checkScalar = (value: unknown, where: string) => { + if (typeof value === "number") { + expect(Number.isInteger(value) && value >= 0 && value <= 255, where).toBe(true) + return + } + expect(typeof value, where).toBe("string") + const text = value as string + if (text.startsWith("#")) { + expect(text, where).toMatch(hex) + return + } + const resolvable = text === "transparent" || text === "none" || text in defs || roles.has(text) + expect(resolvable, `${where}: "${text}" is not a def, a role, transparent or none`).toBe(true) + } + for (const [key, value] of Object.entries(defs)) checkScalar(value, `${name}.defs.${key}`) + for (const [key, value] of Object.entries(theme.theme)) { + if (key === "thinkingOpacity") { + expect(typeof value, `${name}.theme.thinkingOpacity`).toBe("number") + continue + } + if (typeof value === "object" && value !== null) { + const variant = value as Record + expect(Object.keys(variant).sort(), `${name}.theme.${key}`).toEqual(["dark", "light"]) + checkScalar(variant.dark, `${name}.theme.${key}.dark`) + checkScalar(variant.light, `${name}.theme.${key}.light`) + continue + } + checkScalar(value, `${name}.theme.${key}`) + } + } +}) diff --git a/patches/install-korean-ime-fix.sh b/patches/install-korean-ime-fix.sh index 0522d8cefb..c75375b3d6 100755 --- a/patches/install-korean-ime-fix.sh +++ b/patches/install-korean-ime-fix.sh @@ -114,7 +114,7 @@ echo "" ok "Done! Korean IME fix is now active." echo "" info "To uninstall and revert to the official release:" -echo " curl -fsSL https://code.redrob.ai/install | bash" +echo " curl -fsSL https://console.redrob.ai/code/install.sh | sh" echo "" info "To update (re-pull and rebuild):" echo " $0" diff --git a/sdks/vscode/README.md b/sdks/vscode/README.md index a0b1d61b2d..25ba018a68 100644 --- a/sdks/vscode/README.md +++ b/sdks/vscode/README.md @@ -1,10 +1,10 @@ # Redrob Code VS Code Extension -A Visual Studio Code extension that integrates [Redrob Code](https://code.redrob.ai) directly into your development workflow. +A Visual Studio Code extension that integrates [Redrob Code](https://console.redrob.ai/code) directly into your development workflow. ## Prerequisites -This extension requires the [Redrob Code CLI](https://code.redrob.ai) to be installed on your system. Visit [Redrob Code.ai](https://code.redrob.ai) for installation instructions. +This extension requires the [Redrob Code CLI](https://console.redrob.ai/code) to be installed on your system. Visit [console.redrob.ai/code](https://console.redrob.ai/code) for installation instructions. ## Features