From c09d2509e3266cc24129ba752ea240b3c0378609 Mon Sep 17 00:00:00 2001 From: Priya Raman Date: Wed, 5 Aug 2026 23:03:58 +0000 Subject: [PATCH] fix(adapters): keep user-scoped env bindings on the agent Test action The agent "Test" action dropped a user-scoped environment variable that a real run keeps. In create mode the form builds the adapter config with the adapter build-config parser. That parser recognized only `plain` and `secret_ref` bindings and dropped every `user_secret_ref` binding. So the resolved config sent to the test route had no binding to resolve, and the probe ran without the variable. Add one shared, binding-type-agnostic parser in `@paperclipai/adapter-utils` (`buildAdapterEnvConfig`, `parseEnvBindings`, `parseEnvVars`). It keeps `plain`, `secret_ref`, and `user_secret_ref` bindings so the server resolver receives each one, the same way a real run does. The parser never resolves a secret and never reads a secret value; it only preserves the binding shape. Replace the eight byte-identical copies of the parser across the adapter build-config files with the shared helper. This removes the duplication and fixes the same drop in every adapter at once. Edit mode already passed the env map through unchanged, so it was not affected. Add unit tests: the shared parser keeps each binding type and the create-mode build-config keeps a `user_secret_ref` binding. Co-authored-by: Paperclip --- .../adapter-utils/src/env-bindings.test.ts | 82 +++++++++++++++ packages/adapter-utils/src/env-bindings.ts | 99 +++++++++++++++++++ packages/adapter-utils/src/index.ts | 5 + .../claude-local/src/ui/build-config.test.ts | 30 ++++++ .../claude-local/src/ui/build-config.ts | 53 +--------- .../codex-local/src/ui/build-config.ts | 53 +--------- .../cursor-cloud/src/ui/build-config.ts | 53 +--------- .../cursor-local/src/ui/build-config.ts | 53 +--------- .../gemini-local/src/ui/build-config.ts | 53 +--------- .../grok-local/src/ui/build-config.ts | 53 +--------- .../opencode-local/src/ui/build-config.ts | 53 +--------- .../adapters/pi-local/src/ui/build-config.ts | 53 +--------- 12 files changed, 232 insertions(+), 408 deletions(-) create mode 100644 packages/adapter-utils/src/env-bindings.test.ts create mode 100644 packages/adapter-utils/src/env-bindings.ts diff --git a/packages/adapter-utils/src/env-bindings.test.ts b/packages/adapter-utils/src/env-bindings.test.ts new file mode 100644 index 0000000000..a3d1dd78d3 --- /dev/null +++ b/packages/adapter-utils/src/env-bindings.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import { buildAdapterEnvConfig, parseEnvBindings, parseEnvVars } from "./env-bindings.js"; + +describe("parseEnvBindings", () => { + it("keeps plain, company secret, and user-scoped bindings", () => { + const env = parseEnvBindings({ + PLAIN: { type: "plain", value: "on" }, + LEGACY_STRING: "raw", + COMPANY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" }, + USER: { type: "user_secret_ref", key: "github_token", version: "latest", required: true }, + }); + + expect(env).toEqual({ + PLAIN: { type: "plain", value: "on" }, + LEGACY_STRING: { type: "plain", value: "raw" }, + COMPANY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" }, + USER: { type: "user_secret_ref", key: "github_token", version: "latest", required: true }, + }); + }); + + it("keeps a user-scoped binding without an optional version or required flag", () => { + const env = parseEnvBindings({ + USER: { type: "user_secret_ref", key: "github_token" }, + }); + + expect(env).toEqual({ USER: { type: "user_secret_ref", key: "github_token" } }); + }); + + it("preserves allowMissingOverride when present", () => { + const env = parseEnvBindings({ + USER: { type: "user_secret_ref", key: "k", allowMissingOverride: true }, + }); + + expect(env.USER).toEqual({ type: "user_secret_ref", key: "k", allowMissingOverride: true }); + }); + + it("drops invalid keys, unknown shapes, and incomplete refs", () => { + const env = parseEnvBindings({ + "1BAD": { type: "plain", value: "x" }, + MISSING_KEY: { type: "user_secret_ref" }, + MISSING_ID: { type: "secret_ref" }, + UNKNOWN: { type: "mystery", value: "y" }, + }); + + expect(env).toEqual({}); + }); + + it("returns an empty map for non-object input", () => { + expect(parseEnvBindings(null)).toEqual({}); + expect(parseEnvBindings([])).toEqual({}); + expect(parseEnvBindings("nope")).toEqual({}); + }); +}); + +describe("parseEnvVars", () => { + it("reads KEY=value lines and skips blanks, comments, and invalid keys", () => { + // The key is trimmed; the value keeps every character after the first "=". + const env = parseEnvVars("A=1\n# comment\n\nB = two = three\n1BAD=x\nNOEQ"); + + expect(env).toEqual({ A: "1", B: " two = three" }); + }); +}); + +describe("buildAdapterEnvConfig", () => { + it("lets a structured binding win over a legacy plain-text entry with the same key", () => { + const env = buildAdapterEnvConfig( + { SHARED: { type: "user_secret_ref", key: "token" } }, + "SHARED=legacy\nONLY_LEGACY=x", + ); + + expect(env).toEqual({ + SHARED: { type: "user_secret_ref", key: "token" }, + ONLY_LEGACY: { type: "plain", value: "x" }, + }); + }); + + it("tolerates missing legacy text", () => { + expect(buildAdapterEnvConfig({ A: { type: "plain", value: "1" } }, undefined)).toEqual({ + A: { type: "plain", value: "1" }, + }); + }); +}); diff --git a/packages/adapter-utils/src/env-bindings.ts b/packages/adapter-utils/src/env-bindings.ts new file mode 100644 index 0000000000..efdd57b858 --- /dev/null +++ b/packages/adapter-utils/src/env-bindings.ts @@ -0,0 +1,99 @@ +// Shared parser for the agent configuration form environment fields. +// +// The form supplies two sources for adapter `env`: +// - `envBindings`: structured bindings keyed by variable name. A binding is a +// plain value, a company `secret_ref`, or a user-scoped `user_secret_ref`. +// - `envVars`: a legacy plain-text block of `KEY=value` lines. +// +// The server resolves every binding to a string, both at run time and at test +// time. This parser must keep each binding shape intact so the resolver gets +// it. A dropped binding makes the agent "Test" action miss a variable that a +// real run receives. So this parser keeps all three binding types. It never +// resolves a secret and never reads a secret value; it only preserves the +// binding shape for the server resolver. + +const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; + +function isVersionSelector(value: unknown): value is number | "latest" { + return typeof value === "number" || value === "latest"; +} + +/** + * Convert the structured `envBindings` map into an adapter `env` map. Keep + * `plain`, `secret_ref`, and `user_secret_ref` bindings. Drop entries with an + * invalid variable name or an unknown binding shape. + */ +export function parseEnvBindings(bindings: unknown): Record { + if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; + const env: Record = {}; + for (const [key, raw] of Object.entries(bindings)) { + if (!ENV_KEY_RE.test(key)) continue; + if (typeof raw === "string") { + env[key] = { type: "plain", value: raw }; + continue; + } + if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; + const rec = raw as Record; + if (rec.type === "plain" && typeof rec.value === "string") { + env[key] = { type: "plain", value: rec.value }; + continue; + } + if (rec.type === "secret_ref" && typeof rec.secretId === "string") { + env[key] = { + type: "secret_ref", + secretId: rec.secretId, + ...(isVersionSelector(rec.version) ? { version: rec.version } : {}), + }; + continue; + } + if (rec.type === "user_secret_ref" && typeof rec.key === "string") { + env[key] = { + type: "user_secret_ref", + key: rec.key, + ...(isVersionSelector(rec.version) ? { version: rec.version } : {}), + ...(typeof rec.required === "boolean" ? { required: rec.required } : {}), + ...(typeof rec.allowMissingOverride === "boolean" + ? { allowMissingOverride: rec.allowMissingOverride } + : {}), + }; + } + } + return env; +} + +/** + * Parse the legacy plain-text `KEY=value` block. Skip blank lines, comment + * lines, and lines with an invalid variable name. + */ +export function parseEnvVars(text: string): Record { + const env: Record = {}; + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq <= 0) continue; + const key = trimmed.slice(0, eq).trim(); + const value = trimmed.slice(eq + 1); + if (!ENV_KEY_RE.test(key)) continue; + env[key] = value; + } + return env; +} + +/** + * Build the adapter `env` map from both form sources. A structured binding wins + * over a legacy plain-text entry with the same key. + */ +export function buildAdapterEnvConfig( + envBindings: unknown, + envVars: string | undefined | null, +): Record { + const env = parseEnvBindings(envBindings); + const legacy = parseEnvVars(envVars ?? ""); + for (const [key, value] of Object.entries(legacy)) { + if (!Object.prototype.hasOwnProperty.call(env, key)) { + env[key] = { type: "plain", value }; + } + } + return env; +} diff --git a/packages/adapter-utils/src/index.ts b/packages/adapter-utils/src/index.ts index dc5db8865f..5df19260aa 100644 --- a/packages/adapter-utils/src/index.ts +++ b/packages/adapter-utils/src/index.ts @@ -65,6 +65,11 @@ export { redactCommandText, } from "./command-redaction.js"; export { buildSandboxNpmInstallCommand } from "./sandbox-install-command.js"; +export { + buildAdapterEnvConfig, + parseEnvBindings, + parseEnvVars, +} from "./env-bindings.js"; export { createRuntimeProgressReporter } from "./runtime-progress.js"; export type { RuntimeProgressSink, diff --git a/packages/adapters/claude-local/src/ui/build-config.test.ts b/packages/adapters/claude-local/src/ui/build-config.test.ts index 3f8d3626bf..30eb04c9d7 100644 --- a/packages/adapters/claude-local/src/ui/build-config.test.ts +++ b/packages/adapters/claude-local/src/ui/build-config.test.ts @@ -47,4 +47,34 @@ describe("buildClaudeLocalConfig", () => { expect(buildClaudeLocalConfig(makeValues({ claudeEngine: "cli" }))).toMatchObject({ engine: "cli" }); expect(buildClaudeLocalConfig(makeValues({ claudeEngine: "acp" }))).toMatchObject({ engine: "acp" }); }); + + it("keeps user-scoped env bindings so the server resolves them at test time", () => { + const config = buildClaudeLocalConfig( + makeValues({ + envBindings: { + GH_TOKEN: { type: "user_secret_ref", key: "github_token", version: "latest", required: true }, + }, + }), + ); + + expect(config.env).toEqual({ + GH_TOKEN: { type: "user_secret_ref", key: "github_token", version: "latest", required: true }, + }); + }); + + it("keeps company secret and plain env bindings", () => { + const config = buildClaudeLocalConfig( + makeValues({ + envBindings: { + API_KEY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" }, + FLAG: { type: "plain", value: "on" }, + }, + }), + ); + + expect(config.env).toEqual({ + API_KEY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" }, + FLAG: { type: "plain", value: "on" }, + }); + }); }); diff --git a/packages/adapters/claude-local/src/ui/build-config.ts b/packages/adapters/claude-local/src/ui/build-config.ts index c17fcd7178..9262984806 100644 --- a/packages/adapters/claude-local/src/ui/build-config.ts +++ b/packages/adapters/claude-local/src/ui/build-config.ts @@ -1,4 +1,4 @@ -import type { CreateConfigValues } from "@paperclipai/adapter-utils"; +import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils"; function parseCommaArgs(value: string): string[] { return value @@ -7,49 +7,6 @@ function parseCommaArgs(value: string): string[] { .filter(Boolean); } -function parseEnvVars(text: string): Record { - const env: Record = {}; - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq <= 0) continue; - const key = trimmed.slice(0, eq).trim(); - const value = trimmed.slice(eq + 1); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - env[key] = value; - } - return env; -} - -function parseEnvBindings(bindings: unknown): Record { - if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; - const env: Record = {}; - for (const [key, raw] of Object.entries(bindings)) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - if (typeof raw === "string") { - env[key] = { type: "plain", value: raw }; - continue; - } - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; - const rec = raw as Record; - if (rec.type === "plain" && typeof rec.value === "string") { - env[key] = { type: "plain", value: rec.value }; - continue; - } - if (rec.type === "secret_ref" && typeof rec.secretId === "string") { - env[key] = { - type: "secret_ref", - secretId: rec.secretId, - ...(typeof rec.version === "number" || rec.version === "latest" - ? { version: rec.version } - : {}), - }; - } - } - return env; -} - function parseJsonObject(text: string): Record | null { const trimmed = text.trim(); if (!trimmed) return null; @@ -84,13 +41,7 @@ export function buildClaudeLocalConfig(v: CreateConfigValues): Record 0) ac.env = env; ac.maxTurnsPerRun = v.maxTurnsPerRun; ac.dangerouslySkipPermissions = v.dangerouslySkipPermissions; diff --git a/packages/adapters/codex-local/src/ui/build-config.ts b/packages/adapters/codex-local/src/ui/build-config.ts index e82293fefe..1a1da822b8 100644 --- a/packages/adapters/codex-local/src/ui/build-config.ts +++ b/packages/adapters/codex-local/src/ui/build-config.ts @@ -1,4 +1,4 @@ -import type { CreateConfigValues } from "@paperclipai/adapter-utils"; +import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils"; import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "../index.js"; function parseCommaArgs(value: string): string[] { @@ -8,49 +8,6 @@ function parseCommaArgs(value: string): string[] { .filter(Boolean); } -function parseEnvVars(text: string): Record { - const env: Record = {}; - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq <= 0) continue; - const key = trimmed.slice(0, eq).trim(); - const value = trimmed.slice(eq + 1); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - env[key] = value; - } - return env; -} - -function parseEnvBindings(bindings: unknown): Record { - if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; - const env: Record = {}; - for (const [key, raw] of Object.entries(bindings)) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - if (typeof raw === "string") { - env[key] = { type: "plain", value: raw }; - continue; - } - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; - const rec = raw as Record; - if (rec.type === "plain" && typeof rec.value === "string") { - env[key] = { type: "plain", value: rec.value }; - continue; - } - if (rec.type === "secret_ref" && typeof rec.secretId === "string") { - env[key] = { - type: "secret_ref", - secretId: rec.secretId, - ...(typeof rec.version === "number" || rec.version === "latest" - ? { version: rec.version } - : {}), - }; - } - } - return env; -} - function parseJsonObject(text: string): Record | null { const trimmed = text.trim(); if (!trimmed) return null; @@ -79,13 +36,7 @@ export function buildCodexLocalConfig(v: CreateConfigValues): Record 0) ac.env = env; ac.search = v.search; ac.fastMode = v.fastMode; diff --git a/packages/adapters/cursor-cloud/src/ui/build-config.ts b/packages/adapters/cursor-cloud/src/ui/build-config.ts index 3e37804028..7ffecbaa24 100644 --- a/packages/adapters/cursor-cloud/src/ui/build-config.ts +++ b/packages/adapters/cursor-cloud/src/ui/build-config.ts @@ -1,47 +1,4 @@ -import type { CreateConfigValues } from "@paperclipai/adapter-utils"; - -function parseEnvVars(text: string): Record { - const env: Record = {}; - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq <= 0) continue; - const key = trimmed.slice(0, eq).trim(); - const value = trimmed.slice(eq + 1); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - env[key] = value; - } - return env; -} - -function parseEnvBindings(bindings: unknown): Record { - if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; - const env: Record = {}; - for (const [key, raw] of Object.entries(bindings)) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - if (typeof raw === "string") { - env[key] = { type: "plain", value: raw }; - continue; - } - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; - const rec = raw as Record; - if (rec.type === "plain" && typeof rec.value === "string") { - env[key] = { type: "plain", value: rec.value }; - continue; - } - if (rec.type === "secret_ref" && typeof rec.secretId === "string") { - env[key] = { - type: "secret_ref", - secretId: rec.secretId, - ...(typeof rec.version === "number" || rec.version === "latest" - ? { version: rec.version } - : {}), - }; - } - } - return env; -} +import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils"; export function buildCursorCloudConfig(values: CreateConfigValues): Record { const config: Record = { @@ -52,13 +9,7 @@ export function buildCursorCloudConfig(values: CreateConfigValues): Record 0) { config.env = env; } diff --git a/packages/adapters/cursor-local/src/ui/build-config.ts b/packages/adapters/cursor-local/src/ui/build-config.ts index 0bc09e8312..a6da4ab930 100644 --- a/packages/adapters/cursor-local/src/ui/build-config.ts +++ b/packages/adapters/cursor-local/src/ui/build-config.ts @@ -1,4 +1,4 @@ -import type { CreateConfigValues } from "@paperclipai/adapter-utils"; +import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils"; import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js"; function parseCommaArgs(value: string): string[] { @@ -8,49 +8,6 @@ function parseCommaArgs(value: string): string[] { .filter(Boolean); } -function parseEnvVars(text: string): Record { - const env: Record = {}; - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq <= 0) continue; - const key = trimmed.slice(0, eq).trim(); - const value = trimmed.slice(eq + 1); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - env[key] = value; - } - return env; -} - -function parseEnvBindings(bindings: unknown): Record { - if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; - const env: Record = {}; - for (const [key, raw] of Object.entries(bindings)) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - if (typeof raw === "string") { - env[key] = { type: "plain", value: raw }; - continue; - } - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; - const rec = raw as Record; - if (rec.type === "plain" && typeof rec.value === "string") { - env[key] = { type: "plain", value: rec.value }; - continue; - } - if (rec.type === "secret_ref" && typeof rec.secretId === "string") { - env[key] = { - type: "secret_ref", - secretId: rec.secretId, - ...(typeof rec.version === "number" || rec.version === "latest" - ? { version: rec.version } - : {}), - }; - } - } - return env; -} - function normalizeMode(value: string): "plan" | "ask" | null { const mode = value.trim().toLowerCase(); if (mode === "plan" || mode === "ask") return mode; @@ -66,13 +23,7 @@ export function buildCursorLocalConfig(v: CreateConfigValues): Record 0) ac.env = env; if (v.command) ac.command = v.command; if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs); diff --git a/packages/adapters/gemini-local/src/ui/build-config.ts b/packages/adapters/gemini-local/src/ui/build-config.ts index baa5418308..d500b8ef65 100644 --- a/packages/adapters/gemini-local/src/ui/build-config.ts +++ b/packages/adapters/gemini-local/src/ui/build-config.ts @@ -1,4 +1,4 @@ -import type { CreateConfigValues } from "@paperclipai/adapter-utils"; +import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils"; import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js"; function parseCommaArgs(value: string): string[] { @@ -8,49 +8,6 @@ function parseCommaArgs(value: string): string[] { .filter(Boolean); } -function parseEnvVars(text: string): Record { - const env: Record = {}; - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq <= 0) continue; - const key = trimmed.slice(0, eq).trim(); - const value = trimmed.slice(eq + 1); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - env[key] = value; - } - return env; -} - -function parseEnvBindings(bindings: unknown): Record { - if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; - const env: Record = {}; - for (const [key, raw] of Object.entries(bindings)) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - if (typeof raw === "string") { - env[key] = { type: "plain", value: raw }; - continue; - } - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; - const rec = raw as Record; - if (rec.type === "plain" && typeof rec.value === "string") { - env[key] = { type: "plain", value: rec.value }; - continue; - } - if (rec.type === "secret_ref" && typeof rec.secretId === "string") { - env[key] = { - type: "secret_ref", - secretId: rec.secretId, - ...(typeof rec.version === "number" || rec.version === "latest" - ? { version: rec.version } - : {}), - }; - } - } - return env; -} - export function buildGeminiLocalConfig(v: CreateConfigValues): Record { const ac: Record = {}; if (v.cwd) ac.cwd = v.cwd; @@ -66,13 +23,7 @@ export function buildGeminiLocalConfig(v: CreateConfigValues): Record 0) ac.env = env; ac.sandbox = !v.dangerouslyBypassSandbox; diff --git a/packages/adapters/grok-local/src/ui/build-config.ts b/packages/adapters/grok-local/src/ui/build-config.ts index 6c9e9a66d5..37c9a48863 100644 --- a/packages/adapters/grok-local/src/ui/build-config.ts +++ b/packages/adapters/grok-local/src/ui/build-config.ts @@ -1,4 +1,4 @@ -import type { CreateConfigValues } from "@paperclipai/adapter-utils"; +import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils"; import { DEFAULT_GROK_LOCAL_MODEL } from "../index.js"; function parseCommaArgs(value: string): string[] { @@ -8,49 +8,6 @@ function parseCommaArgs(value: string): string[] { .filter(Boolean); } -function parseEnvVars(text: string): Record { - const env: Record = {}; - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq <= 0) continue; - const key = trimmed.slice(0, eq).trim(); - const value = trimmed.slice(eq + 1); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - env[key] = value; - } - return env; -} - -function parseEnvBindings(bindings: unknown): Record { - if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; - const env: Record = {}; - for (const [key, raw] of Object.entries(bindings)) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - if (typeof raw === "string") { - env[key] = { type: "plain", value: raw }; - continue; - } - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; - const rec = raw as Record; - if (rec.type === "plain" && typeof rec.value === "string") { - env[key] = { type: "plain", value: rec.value }; - continue; - } - if (rec.type === "secret_ref" && typeof rec.secretId === "string") { - env[key] = { - type: "secret_ref", - secretId: rec.secretId, - ...(typeof rec.version === "number" || rec.version === "latest" - ? { version: rec.version } - : {}), - }; - } - } - return env; -} - export function buildGrokLocalConfig(v: CreateConfigValues): Record { const ac: Record = {}; if (v.cwd) ac.cwd = v.cwd; @@ -59,13 +16,7 @@ export function buildGrokLocalConfig(v: CreateConfigValues): Record 0) ac.env = env; if (v.command) ac.command = v.command; diff --git a/packages/adapters/opencode-local/src/ui/build-config.ts b/packages/adapters/opencode-local/src/ui/build-config.ts index a6ab272888..17d9da9509 100644 --- a/packages/adapters/opencode-local/src/ui/build-config.ts +++ b/packages/adapters/opencode-local/src/ui/build-config.ts @@ -1,4 +1,4 @@ -import type { CreateConfigValues } from "@paperclipai/adapter-utils"; +import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils"; function parseCommaArgs(value: string): string[] { return value @@ -7,49 +7,6 @@ function parseCommaArgs(value: string): string[] { .filter(Boolean); } -function parseEnvVars(text: string): Record { - const env: Record = {}; - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq <= 0) continue; - const key = trimmed.slice(0, eq).trim(); - const value = trimmed.slice(eq + 1); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - env[key] = value; - } - return env; -} - -function parseEnvBindings(bindings: unknown): Record { - if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; - const env: Record = {}; - for (const [key, raw] of Object.entries(bindings)) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - if (typeof raw === "string") { - env[key] = { type: "plain", value: raw }; - continue; - } - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; - const rec = raw as Record; - if (rec.type === "plain" && typeof rec.value === "string") { - env[key] = { type: "plain", value: rec.value }; - continue; - } - if (rec.type === "secret_ref" && typeof rec.secretId === "string") { - env[key] = { - type: "secret_ref", - secretId: rec.secretId, - ...(typeof rec.version === "number" || rec.version === "latest" - ? { version: rec.version } - : {}), - }; - } - } - return env; -} - export function buildOpenCodeLocalConfig(v: CreateConfigValues): Record { const ac: Record = {}; if (v.cwd) ac.cwd = v.cwd; @@ -61,13 +18,7 @@ export function buildOpenCodeLocalConfig(v: CreateConfigValues): Record 0) ac.env = env; if (v.command) ac.command = v.command; if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs); diff --git a/packages/adapters/pi-local/src/ui/build-config.ts b/packages/adapters/pi-local/src/ui/build-config.ts index 470ecedaf0..06927f6f8d 100644 --- a/packages/adapters/pi-local/src/ui/build-config.ts +++ b/packages/adapters/pi-local/src/ui/build-config.ts @@ -1,47 +1,4 @@ -import type { CreateConfigValues } from "@paperclipai/adapter-utils"; - -function parseEnvVars(text: string): Record { - const env: Record = {}; - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq <= 0) continue; - const key = trimmed.slice(0, eq).trim(); - const value = trimmed.slice(eq + 1); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - env[key] = value; - } - return env; -} - -function parseEnvBindings(bindings: unknown): Record { - if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; - const env: Record = {}; - for (const [key, raw] of Object.entries(bindings)) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; - if (typeof raw === "string") { - env[key] = { type: "plain", value: raw }; - continue; - } - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; - const rec = raw as Record; - if (rec.type === "plain" && typeof rec.value === "string") { - env[key] = { type: "plain", value: rec.value }; - continue; - } - if (rec.type === "secret_ref" && typeof rec.secretId === "string") { - env[key] = { - type: "secret_ref", - secretId: rec.secretId, - ...(typeof rec.version === "number" || rec.version === "latest" - ? { version: rec.version } - : {}), - }; - } - } - return env; -} +import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils"; export function buildPiLocalConfig(v: CreateConfigValues): Record { const ac: Record = {}; @@ -54,13 +11,7 @@ export function buildPiLocalConfig(v: CreateConfigValues): Record 0) ac.env = env; if (v.command) ac.command = v.command; if (v.extraArgs) ac.extraArgs = v.extraArgs;