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;