Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0c73b5439 | |||
| bbbe4651c2 | |||
| 7dfe0eb6a9 | |||
| 47a4268b9b | |||
| 0c90b88e08 |
@@ -391,7 +391,7 @@ Everything else is immutable CAS content.
|
||||
providers:
|
||||
openrouter:
|
||||
baseUrl: "https://openrouter.ai/api/v1"
|
||||
apiKeyEnv: "OPENROUTER_API_KEY"
|
||||
apiKey: "sk-..."
|
||||
|
||||
models:
|
||||
sonnet:
|
||||
|
||||
@@ -402,7 +402,7 @@ workflow 怎么配置和使用 model?
|
||||
```136:160:packages/workflow-protocol/src/types.ts
|
||||
export type ProviderConfig = {
|
||||
baseUrl: string;
|
||||
apiKeyEnv: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type ModelConfig = {
|
||||
@@ -429,7 +429,7 @@ export type WorkflowConfig = {
|
||||
export function resolveModel(config: WorkflowConfig, alias: ModelAlias): ResolvedLlmProvider {
|
||||
const modelEntry = config.models[alias];
|
||||
const providerEntry = config.providers[modelEntry.provider];
|
||||
const apiKey = process.env[providerEntry.apiKeyEnv];
|
||||
const apiKey = providerEntry.apiKey;
|
||||
return { baseUrl: providerEntry.baseUrl, apiKey, model: modelEntry.name };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -280,13 +280,13 @@ threads.yaml: { "01J7K9M2XNPQR5VWBCDF8G3H4T": "8FWKR3TN5V1QA" }
|
||||
providers:
|
||||
openai:
|
||||
baseUrl: "https://api.openai.com/v1"
|
||||
apiKeyEnv: "OPENAI_API_KEY"
|
||||
apiKey: "sk-..."
|
||||
anthropic:
|
||||
baseUrl: "https://api.anthropic.com/v1"
|
||||
apiKeyEnv: "ANTHROPIC_API_KEY"
|
||||
apiKey: "sk-ant-..."
|
||||
openrouter:
|
||||
baseUrl: "https://openrouter.ai/api/v1"
|
||||
apiKeyEnv: "OPENROUTER_API_KEY"
|
||||
apiKey: "sk-or-..."
|
||||
|
||||
models:
|
||||
sonnet:
|
||||
@@ -465,7 +465,7 @@ type Scenario = string; // e.g. "extract"
|
||||
|
||||
type ProviderConfig = {
|
||||
baseUrl: string;
|
||||
apiKeyEnv: string; // env var name to read API key from
|
||||
apiKey: string; // API key stored directly
|
||||
};
|
||||
|
||||
type ModelConfig = {
|
||||
|
||||
@@ -119,7 +119,7 @@ uwf setup --provider openai --base-url https://api.openai.com/v1 \
|
||||
--api-key sk-... --model gpt-4o --agent hermes
|
||||
```
|
||||
|
||||
Config: `~/.uncaged/workflow/config.yaml`. API keys: `~/.uncaged/workflow/.env`.
|
||||
Config: `~/.uncaged/workflow/config.yaml` (includes API keys).
|
||||
|
||||
### Skill
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ describe("config command", () => {
|
||||
const sampleConfig = `providers:
|
||||
dashscope:
|
||||
baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
apiKeyEnv: DASHSCOPE_API_KEY
|
||||
apiKey: sk-test-dashscope-key
|
||||
openai:
|
||||
baseUrl: https://api.openai.com/v1
|
||||
apiKeyEnv: OPENAI_API_KEY
|
||||
apiKey: sk-test-openai-key
|
||||
models:
|
||||
default:
|
||||
provider: dashscope
|
||||
@@ -102,16 +102,16 @@ defaultModel: default
|
||||
});
|
||||
|
||||
describe("maskApiKeys", () => {
|
||||
test("deep clones and masks all apiKeyEnv values in providers", () => {
|
||||
test("deep clones and masks all apiKey values in providers", () => {
|
||||
const config = {
|
||||
providers: {
|
||||
dashscope: {
|
||||
baseUrl: "https://example.com",
|
||||
apiKeyEnv: "DASHSCOPE_API_KEY",
|
||||
apiKey: "sk-test-key-12345",
|
||||
},
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com",
|
||||
apiKeyEnv: "OPENAI_API_KEY",
|
||||
apiKey: "sk-another-secret",
|
||||
},
|
||||
},
|
||||
models: {
|
||||
@@ -123,11 +123,11 @@ defaultModel: default
|
||||
providers: {
|
||||
dashscope: {
|
||||
baseUrl: "https://example.com",
|
||||
apiKeyEnv: "***MASKED***",
|
||||
apiKey: "***MASKED***",
|
||||
},
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com",
|
||||
apiKeyEnv: "***MASKED***",
|
||||
apiKey: "***MASKED***",
|
||||
},
|
||||
},
|
||||
models: {
|
||||
@@ -164,7 +164,7 @@ defaultModel: default
|
||||
}
|
||||
});
|
||||
|
||||
test("masks all apiKeyEnv values in providers section", async () => {
|
||||
test("masks all apiKey values in providers section", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
@@ -172,8 +172,8 @@ defaultModel: default
|
||||
const providers = result.providers as Record<string, unknown>;
|
||||
const dashscope = providers.dashscope as Record<string, unknown>;
|
||||
const openai = providers.openai as Record<string, unknown>;
|
||||
expect(dashscope.apiKeyEnv).toBe("***MASKED***");
|
||||
expect(openai.apiKeyEnv).toBe("***MASKED***");
|
||||
expect(dashscope.apiKey).toBe("***MASKED***");
|
||||
expect(openai.apiKey).toBe("***MASKED***");
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -240,7 +240,7 @@ defaultModel: default
|
||||
const result = await cmdConfigGet(tempDir, "providers.dashscope");
|
||||
expect(result).toEqual({
|
||||
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
apiKeyEnv: "DASHSCOPE_API_KEY",
|
||||
apiKey: "sk-test-dashscope-key",
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
@@ -464,4 +464,159 @@ defaultModel: default
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdConfigSet validation", () => {
|
||||
test("rejects unknown top-level key", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "unknownKey", "value")).rejects.toThrow(
|
||||
/Unknown config key.*unknownKey/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects unknown nested key in providers", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(
|
||||
cmdConfigSet(tempDir, "providers.myProvider.unknownField", "value"),
|
||||
).rejects.toThrow(/Unknown field.*unknownField.*providers/);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects unknown nested key in models", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "models.default.invalidField", "value")).rejects.toThrow(
|
||||
/Unknown field.*invalidField.*models/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects unknown nested key in agents", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "agents.hermes.badField", "value")).rejects.toThrow(
|
||||
/Unknown field.*badField.*agents/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects nested path on scalar key (defaultAgent)", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "defaultAgent.foo", "value")).rejects.toThrow(
|
||||
/defaultAgent.*scalar|Cannot set property/i,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects nested path on scalar key (defaultModel)", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "defaultModel.bar", "value")).rejects.toThrow(
|
||||
/defaultModel.*scalar|Cannot set property/i,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects incomplete nested path (providers without field)", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "providers.myProvider", "value")).rejects.toThrow(
|
||||
/incomplete path|must specify a field/i,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects incomplete nested path (models without field)", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "models.myModel", "value")).rejects.toThrow(
|
||||
/incomplete path|must specify a field/i,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects incomplete nested path (agents without field)", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "agents.myAgent", "value")).rejects.toThrow(
|
||||
/incomplete path|must specify a field/i,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("allows valid nested keys in providers", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await cmdConfigSet(tempDir, "providers.newprovider.baseUrl", "https://example.com");
|
||||
await cmdConfigSet(tempDir, "providers.newprovider.apiKey", "sk-test");
|
||||
const baseUrl = await cmdConfigGet(tempDir, "providers.newprovider.baseUrl");
|
||||
const apiKey = await cmdConfigGet(tempDir, "providers.newprovider.apiKey");
|
||||
expect(baseUrl).toBe("https://example.com");
|
||||
expect(apiKey).toBe("sk-test");
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("allows valid nested keys in models", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await cmdConfigSet(tempDir, "models.gpt4.provider", "openai");
|
||||
await cmdConfigSet(tempDir, "models.gpt4.name", "gpt-4o");
|
||||
const provider = await cmdConfigGet(tempDir, "models.gpt4.provider");
|
||||
const name = await cmdConfigGet(tempDir, "models.gpt4.name");
|
||||
expect(provider).toBe("openai");
|
||||
expect(name).toBe("gpt-4o");
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("allows valid nested keys in agents", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await cmdConfigSet(tempDir, "agents.hermes.command", "uwf-hermes");
|
||||
await cmdConfigSet(tempDir, "agents.hermes.args", '["--flag"]');
|
||||
const command = await cmdConfigGet(tempDir, "agents.hermes.command");
|
||||
const args = await cmdConfigGet(tempDir, "agents.hermes.args");
|
||||
expect(command).toBe("uwf-hermes");
|
||||
expect(args).toEqual(["--flag"]);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,4 +134,34 @@ describe("cmdSetup agent configuration", () => {
|
||||
const config2 = parse(readFileSync(join(storageRoot, "config.yaml"), "utf8"));
|
||||
expect(config2.defaultAgent).toBe("builtin");
|
||||
});
|
||||
|
||||
test("normalizes agent name with uwf- prefix to bare name", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({}), { status: 200 }),
|
||||
);
|
||||
|
||||
const result = await cmdSetup({ ...baseArgs(), agent: "uwf-hermes" });
|
||||
|
||||
expect(result.defaultAgent).toBe("hermes");
|
||||
const config = parse(readFileSync(join(storageRoot, "config.yaml"), "utf8"));
|
||||
expect(config.agents.hermes).toEqual({ command: "uwf-hermes", args: [] });
|
||||
expect(config.defaultAgent).toBe("hermes");
|
||||
// Verify no duplicate uwf- prefix
|
||||
expect(config.agents["uwf-hermes"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("normalizes uwf-claude-code to claude-code", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({}), { status: 200 }),
|
||||
);
|
||||
|
||||
const result = await cmdSetup({ ...baseArgs(), agent: "uwf-claude-code" });
|
||||
|
||||
expect(result.defaultAgent).toBe("claude-code");
|
||||
const config = parse(readFileSync(join(storageRoot, "config.yaml"), "utf8"));
|
||||
expect(config.agents["claude-code"]).toEqual({ command: "uwf-claude-code", args: [] });
|
||||
expect(config.defaultAgent).toBe("claude-code");
|
||||
// Verify no duplicate uwf- prefix
|
||||
expect(config.agents["uwf-claude-code"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,9 +129,8 @@ describe("cmdSetup with validation", () => {
|
||||
const result = await cmdSetup(setupArgs());
|
||||
|
||||
expect(result.validation).toEqual({ ok: true, value: undefined });
|
||||
// Config files should still be written
|
||||
// Config file should still be written
|
||||
expect(result.configPath).toBeTruthy();
|
||||
expect(result.envPath).toBeTruthy();
|
||||
});
|
||||
|
||||
test("includes validation failure — config still saved", async () => {
|
||||
@@ -143,8 +142,7 @@ describe("cmdSetup with validation", () => {
|
||||
|
||||
expect(result.validation).toBeDefined();
|
||||
expect((result.validation as { ok: boolean }).ok).toBe(false);
|
||||
// Config files should still be written despite validation failure
|
||||
// Config file should still be written despite validation failure
|
||||
expect(result.configPath).toBeTruthy();
|
||||
expect(result.envPath).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,66 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { parse, stringify } from "yaml";
|
||||
|
||||
/**
|
||||
* Valid configuration key schema
|
||||
*/
|
||||
const VALID_CONFIG_KEYS: Record<string, { nested: boolean; knownFields?: string[] }> = {
|
||||
providers: {
|
||||
nested: true,
|
||||
knownFields: ["baseUrl", "apiKey"],
|
||||
},
|
||||
models: {
|
||||
nested: true,
|
||||
knownFields: ["provider", "name"],
|
||||
},
|
||||
agents: {
|
||||
nested: true,
|
||||
knownFields: ["command", "args"],
|
||||
},
|
||||
defaultAgent: { nested: false },
|
||||
defaultModel: { nested: false },
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a config key path against the known schema
|
||||
*/
|
||||
function validateConfigKey(path: string[]): void {
|
||||
if (path.length === 0) {
|
||||
throw new Error("Path cannot be empty");
|
||||
}
|
||||
|
||||
const topLevel = path[0];
|
||||
const schema = VALID_CONFIG_KEYS[topLevel];
|
||||
|
||||
if (!schema) {
|
||||
const validKeys = Object.keys(VALID_CONFIG_KEYS).join(", ");
|
||||
throw new Error(`Unknown config key: ${topLevel}. Valid top-level keys are: ${validKeys}`);
|
||||
}
|
||||
|
||||
// Scalar keys cannot have nested paths
|
||||
if (!schema.nested && path.length > 1) {
|
||||
throw new Error(`${topLevel} is a scalar key and cannot have nested properties`);
|
||||
}
|
||||
|
||||
// Nested keys must have at least 3 segments (e.g., providers.myProvider.baseUrl)
|
||||
if (schema.nested && path.length < 3) {
|
||||
const fields = schema.knownFields?.join(", ") ?? "";
|
||||
throw new Error(
|
||||
`Incomplete path for ${topLevel}. Must specify a field (e.g., ${topLevel}.<name>.<field>). Valid fields: ${fields}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate the field name for nested keys
|
||||
if (schema.nested && path.length >= 3 && schema.knownFields) {
|
||||
const field = path[path.length - 1];
|
||||
if (!schema.knownFields.includes(field)) {
|
||||
throw new Error(
|
||||
`Unknown field '${field}' in ${topLevel}. Valid fields are: ${schema.knownFields.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the config.yaml file
|
||||
*/
|
||||
@@ -100,21 +160,21 @@ export function setNestedValue(obj: Record<string, unknown>, path: string[], val
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clone and mask all apiKeyEnv values in providers section
|
||||
* Deep clone and mask all apiKey values in providers section
|
||||
*/
|
||||
export function maskApiKeys(config: Record<string, unknown>): Record<string, unknown> {
|
||||
// Deep clone
|
||||
const cloned = JSON.parse(JSON.stringify(config)) as Record<string, unknown>;
|
||||
|
||||
// Mask apiKeyEnv values in providers
|
||||
// Mask apiKey values in providers
|
||||
if (cloned.providers && typeof cloned.providers === "object") {
|
||||
const providers = cloned.providers as Record<string, unknown>;
|
||||
for (const providerName of Object.keys(providers)) {
|
||||
const provider = providers[providerName];
|
||||
if (provider && typeof provider === "object") {
|
||||
const providerObj = provider as Record<string, unknown>;
|
||||
if ("apiKeyEnv" in providerObj) {
|
||||
providerObj.apiKeyEnv = "***MASKED***";
|
||||
if ("apiKey" in providerObj) {
|
||||
providerObj.apiKey = "***MASKED***";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,6 +267,10 @@ export async function cmdConfigSet(
|
||||
}
|
||||
|
||||
const path = parseDotPath(key);
|
||||
|
||||
// Validate the key path
|
||||
validateConfigKey(path);
|
||||
|
||||
const lastSegment = path[path.length - 1];
|
||||
|
||||
// Parse value if it's for an array key (args)
|
||||
|
||||
@@ -85,10 +85,6 @@ function getConfigPath(root: string): string {
|
||||
return join(root, "config.yaml");
|
||||
}
|
||||
|
||||
function getEnvPath(root: string): string {
|
||||
return join(root, ".env");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load existing config.yaml or return empty structure.
|
||||
*/
|
||||
@@ -106,37 +102,6 @@ function loadExistingConfig(configPath: string): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load existing .env as key=value map.
|
||||
*/
|
||||
function loadEnvFile(envPath: string): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
try {
|
||||
if (existsSync(envPath)) {
|
||||
for (const line of readFileSync(envPath, "utf8").split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
||||
const eq = trimmed.indexOf("=");
|
||||
if (eq > 0) {
|
||||
env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function saveEnvFile(envPath: string, env: Record<string, string>): void {
|
||||
const lines = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
||||
writeFileSync(envPath, `${lines.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
function apiKeyEnvName(providerName: string): string {
|
||||
return `${providerName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Extracted helpers — _discoverAgents
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -397,8 +362,7 @@ function mergeConfig(existing: Record<string, unknown>, args: SetupArgs): Record
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
|
||||
const envName = apiKeyEnvName(args.provider);
|
||||
providers[args.provider] = { baseUrl: args.baseUrl, apiKeyEnv: envName };
|
||||
providers[args.provider] = { baseUrl: args.baseUrl, apiKey: args.apiKey };
|
||||
|
||||
const models = (
|
||||
typeof existing.models === "object" && existing.models !== null
|
||||
@@ -413,7 +377,7 @@ function mergeConfig(existing: Record<string, unknown>, args: SetupArgs): Record
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
|
||||
const agentName = args.agent ?? "hermes";
|
||||
const agentName = _agentNameFromBinary(args.agent ?? "hermes");
|
||||
// Ensure the selected agent has an entry
|
||||
if (!agents[agentName]) {
|
||||
agents[agentName] = { command: `uwf-${agentName}`, args: [] };
|
||||
@@ -437,25 +401,17 @@ export async function cmdSetup(args: SetupArgs): Promise<Record<string, unknown>
|
||||
mkdirSync(storageRoot, { recursive: true });
|
||||
|
||||
const configPath = getConfigPath(storageRoot);
|
||||
const envPath = getEnvPath(storageRoot);
|
||||
|
||||
const existing = loadExistingConfig(configPath);
|
||||
const merged = mergeConfig(existing, args);
|
||||
|
||||
writeFileSync(configPath, stringify(merged, { indent: 2 }), "utf8");
|
||||
|
||||
// Write API key to .env
|
||||
const envName = apiKeyEnvName(args.provider);
|
||||
const envData = loadEnvFile(envPath);
|
||||
envData[envName] = args.apiKey;
|
||||
saveEnvFile(envPath, envData);
|
||||
|
||||
// Validate model connectivity
|
||||
const validation = await validateModel(args.baseUrl, args.apiKey, args.model);
|
||||
|
||||
return {
|
||||
configPath,
|
||||
envPath,
|
||||
provider: args.provider,
|
||||
model: args.model,
|
||||
defaultAgent: merged.defaultAgent,
|
||||
|
||||
@@ -100,7 +100,7 @@ type ProviderAlias = string;
|
||||
type ModelAlias = string;
|
||||
type AgentAlias = string;
|
||||
|
||||
type ProviderConfig = { baseUrl: string; apiKeyEnv: string };
|
||||
type ProviderConfig = { baseUrl: string; apiKey: string };
|
||||
type ModelConfig = {
|
||||
provider: ProviderAlias;
|
||||
name: string;
|
||||
|
||||
@@ -151,7 +151,7 @@ export type Scenario = string;
|
||||
|
||||
export type ProviderConfig = {
|
||||
baseUrl: string;
|
||||
apiKeyEnv: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type ModelConfig = {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { getSchema, validate } from "@uncaged/json-cas";
|
||||
|
||||
import type { CasRef, ModelAlias, WorkflowConfig } from "@uncaged/workflow-protocol";
|
||||
import { config as loadDotenv } from "dotenv";
|
||||
import { createAgentStore, getEnvPath, resolveStorageRoot } from "./storage.js";
|
||||
import { createAgentStore, resolveStorageRoot } from "./storage.js";
|
||||
|
||||
export type ResolvedLlmProvider = {
|
||||
baseUrl: string;
|
||||
@@ -38,9 +37,9 @@ export function resolveModel(config: WorkflowConfig, alias: ModelAlias): Resolve
|
||||
if (providerEntry === undefined) {
|
||||
throw new Error(`unknown provider "${modelEntry.provider}" for model "${alias}"`);
|
||||
}
|
||||
const apiKey = process.env[providerEntry.apiKeyEnv];
|
||||
const apiKey = providerEntry.apiKey;
|
||||
if (apiKey === undefined || apiKey === "") {
|
||||
throw new Error(`missing API key env var: ${providerEntry.apiKeyEnv}`);
|
||||
throw new Error(`missing API key for provider: ${modelEntry.provider}`);
|
||||
}
|
||||
return {
|
||||
baseUrl: providerEntry.baseUrl,
|
||||
@@ -130,7 +129,7 @@ export type ExtractResult = {
|
||||
|
||||
/**
|
||||
* Call an OpenAI-compatible LLM to extract structured output matching outputSchema.
|
||||
* Loads config.yaml and .env from the workflow storage root.
|
||||
* Loads config.yaml from the workflow storage root.
|
||||
*/
|
||||
export async function extract(
|
||||
rawOutput: string,
|
||||
@@ -138,7 +137,6 @@ export async function extract(
|
||||
config: WorkflowConfig,
|
||||
): Promise<ExtractResult> {
|
||||
const storageRoot = resolveStorageRoot();
|
||||
loadDotenv({ path: getEnvPath(storageRoot) });
|
||||
|
||||
const { store } = await createAgentStore(storageRoot);
|
||||
const schema = getSchema(store, outputSchema);
|
||||
|
||||
@@ -84,11 +84,11 @@ function normalizeProviders(raw: unknown): Record<ProviderAlias, ProviderConfig>
|
||||
throw new Error(`config.providers.${name} must be a mapping`);
|
||||
}
|
||||
const baseUrl = entry.baseUrl;
|
||||
const apiKeyEnv = entry.apiKeyEnv;
|
||||
if (typeof baseUrl !== "string" || typeof apiKeyEnv !== "string") {
|
||||
throw new Error(`config.providers.${name} requires baseUrl and apiKeyEnv`);
|
||||
const apiKey = entry.apiKey;
|
||||
if (typeof baseUrl !== "string" || typeof apiKey !== "string") {
|
||||
throw new Error(`config.providers.${name} requires baseUrl and apiKey`);
|
||||
}
|
||||
providers[name] = { baseUrl, apiKeyEnv };
|
||||
providers[name] = { baseUrl, apiKey };
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user