diff --git a/apps/cli/src/commands/credentials.ts b/apps/cli/src/commands/credentials.ts index 68ca2785..d14f6cdf 100644 --- a/apps/cli/src/commands/credentials.ts +++ b/apps/cli/src/commands/credentials.ts @@ -9,7 +9,7 @@ import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs"; import { dirname } from "node:path"; -import { getCredentialsPath, getSkillsEnvPath } from "@multica/core"; +import { getCredentialsPath } from "@multica/core"; import { cyan, yellow, green, dim, red } from "../colors.js"; type Command = "init" | "show" | "edit" | "help"; @@ -17,10 +17,7 @@ type Command = "init" | "show" | "edit" | "help"; interface CredentialsOptions { command: Command; force: boolean; - coreOnly: boolean; - skillsOnly: boolean; pathOverride?: string | undefined; - skillsPathOverride?: string | undefined; } function printHelp() { @@ -28,21 +25,20 @@ function printHelp() { ${cyan("Usage:")} multica credentials [options] ${cyan("Commands:")} - ${yellow("init")} Create credentials.json5 and skills.env.json5 + ${yellow("init")} Create credentials.json5 ${yellow("show")} Show credential file paths ${yellow("edit")} Open credentials directory in file manager ${yellow("help")} Show this help ${cyan("Options for 'init':")} ${yellow("--force")} Overwrite existing files - ${yellow("--core-only")} Only create credentials.json5 - ${yellow("--skills-only")} Only create skills.env.json5 ${yellow("--path")} PATH Override credentials path - ${yellow("--skills-path")} PATH Override skills env path ${cyan("Files Created:")} ~/.super-multica/credentials.json5 LLM providers + tools config - ~/.super-multica/skills.env.json5 Skills/plugins/integrations env vars + +${dim("Skill-specific API keys are stored in .env files within each skill's directory.")} +${dim("Example: ~/.super-multica/skills//.env")} ${cyan("Examples:")} ${dim("# Initialize credentials")} @@ -50,9 +46,6 @@ ${cyan("Examples:")} ${dim("# Force overwrite")} multica credentials init --force - - ${dim("# Only create core credentials")} - multica credentials init --core-only `); } @@ -61,8 +54,6 @@ function parseArgs(argv: string[]): CredentialsOptions { const opts: CredentialsOptions = { command: "help", force: false, - coreOnly: false, - skillsOnly: false, }; const positional: string[] = []; @@ -79,22 +70,10 @@ function parseArgs(argv: string[]): CredentialsOptions { opts.force = true; continue; } - if (arg === "--core-only") { - opts.coreOnly = true; - continue; - } - if (arg === "--skills-only") { - opts.skillsOnly = true; - continue; - } if (arg === "--path") { opts.pathOverride = args.shift(); continue; } - if (arg === "--skills-path") { - opts.skillsPathOverride = args.shift(); - continue; - } positional.push(arg); } @@ -119,58 +98,25 @@ function buildCoreTemplate(): string { `; } -function buildSkillsTemplate(): string { - return `{ - env: { - // Dynamic keys (skills, plugins, integrations) - // LINEAR_API_KEY: "lin-..." - } -} -`; -} - function cmdInit(opts: CredentialsOptions): void { - const createCore = !opts.skillsOnly; - const createSkills = !opts.coreOnly; - - if (!createCore && !createSkills) { - console.error(`${red("Error:")} Both --core-only and --skills-only were provided.`); + const path = opts.pathOverride ?? getCredentialsPath(); + if (existsSync(path) && !opts.force) { + console.error(`${red("Error:")} Credentials file already exists at ${path}`); + console.error("Use --force to overwrite."); process.exit(1); } - - if (createCore) { - const path = opts.pathOverride ?? getCredentialsPath(); - if (existsSync(path) && !opts.force) { - console.error(`${red("Error:")} Credentials file already exists at ${path}`); - console.error("Use --force to overwrite."); - process.exit(1); - } - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, buildCoreTemplate(), "utf8"); - chmodSync(path, 0o600); - console.log(`${green("Created:")} ${path}`); - } - - if (createSkills) { - const skillsPath = opts.skillsPathOverride ?? getSkillsEnvPath(); - if (existsSync(skillsPath) && !opts.force) { - console.error(`${red("Error:")} Skills env file already exists at ${skillsPath}`); - console.error("Use --force to overwrite."); - process.exit(1); - } - mkdirSync(dirname(skillsPath), { recursive: true }); - writeFileSync(skillsPath, buildSkillsTemplate(), "utf8"); - chmodSync(skillsPath, 0o600); - console.log(`${green("Created:")} ${skillsPath}`); - } + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, buildCoreTemplate(), "utf8"); + chmodSync(path, 0o600); + console.log(`${green("Created:")} ${path}`); console.log(""); - console.log("Edit these files to add your credentials."); + console.log("Edit this file to add your LLM provider credentials."); + console.log(`${dim("Skill-specific API keys go in .env files within each skill's directory.")}`); } function cmdShow(): void { const credentialsPath = getCredentialsPath(); - const skillsEnvPath = getSkillsEnvPath(); console.log(`\n${cyan("Credential Files:")}\n`); @@ -179,12 +125,10 @@ function cmdShow(): void { console.log(` Exists: ${existsSync(credentialsPath) ? green("Yes") : red("No")}`); console.log(""); - console.log(`${yellow("skills.env.json5")}`); - console.log(` Path: ${skillsEnvPath}`); - console.log(` Exists: ${existsSync(skillsEnvPath) ? green("Yes") : red("No")}`); + console.log(`${dim("Skill-specific API keys are stored in .env files within each skill's directory.")}`); console.log(""); - if (!existsSync(credentialsPath) || !existsSync(skillsEnvPath)) { + if (!existsSync(credentialsPath)) { console.log(`${dim("Run 'multica credentials init' to create missing files.")}`); } } diff --git a/apps/cli/src/commands/skills.ts b/apps/cli/src/commands/skills.ts index d9bf5e20..a6971fa0 100644 --- a/apps/cli/src/commands/skills.ts +++ b/apps/cli/src/commands/skills.ts @@ -19,7 +19,6 @@ import { checkEligibilityDetailed, type DiagnosticItem, } from "@multica/core"; -import { credentialManager } from "@multica/core"; import { cyan, yellow, green, dim, red } from "../colors.js"; type Command = "list" | "status" | "install" | "add" | "remove" | "help"; @@ -268,7 +267,7 @@ function cmdStatusDetail(manager: SkillManager, skillId: string, verbose?: boole if (hasEnvs > 0) { const envs = requirements?.env ?? metadata?.requiresEnv ?? []; - printRequirementStatus("Environment vars", envs, checkEnvVars); + printRequirementStatus("Environment vars", envs, (e) => checkEnvVars(e, skill.env)); } } @@ -360,10 +359,11 @@ function checkBinaries(bins: string[]): Map { return result; } -function checkEnvVars(envs: string[]): Map { +function checkEnvVars(envs: string[], skillEnv: Record): Map { const result = new Map(); for (const env of envs) { - result.set(env, credentialManager.hasEnv(env)); + const found = Object.prototype.hasOwnProperty.call(skillEnv, env) || env in process.env; + result.set(env, found); } return result; } diff --git a/apps/cli/src/skills.ts b/apps/cli/src/skills.ts index 64169ca4..4eca4601 100644 --- a/apps/cli/src/skills.ts +++ b/apps/cli/src/skills.ts @@ -22,7 +22,6 @@ import { checkEligibilityDetailed, type DiagnosticItem, } from "@multica/core"; -import { credentialManager } from "@multica/core"; // ============================================================================ // Types @@ -268,7 +267,7 @@ function cmdStatusDetail(manager: SkillManager, skillId: string, verbose?: boole if (hasEnvs > 0) { const envs = requirements?.env ?? metadata?.requiresEnv ?? []; - printRequirementStatus("Environment vars", envs, checkEnvVars); + printRequirementStatus("Environment vars", envs, (e) => checkEnvVars(e, skill.env)); } } @@ -363,10 +362,11 @@ function checkBinaries(bins: string[]): Map { return result; } -function checkEnvVars(envs: string[]): Map { +function checkEnvVars(envs: string[], skillEnv: Record): Map { const result = new Map(); for (const env of envs) { - result.set(env, credentialManager.hasEnv(env)); + const found = Object.prototype.hasOwnProperty.call(skillEnv, env) || env in process.env; + result.set(env, found); } return result; } diff --git a/packages/core/src/agent/credentials-cli.ts b/packages/core/src/agent/credentials-cli.ts index d5fa9a13..316c3e69 100644 --- a/packages/core/src/agent/credentials-cli.ts +++ b/packages/core/src/agent/credentials-cli.ts @@ -3,12 +3,12 @@ * Credentials CLI * * Commands: - * init Create credentials.json5 and skills.env.json5 + * init Create credentials.json5 */ import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs"; import { dirname } from "node:path"; -import { getCredentialsPath, getSkillsEnvPath } from "./credentials.js"; +import { getCredentialsPath } from "./credentials.js"; type Command = "init" | "help"; @@ -16,21 +16,19 @@ function printUsage(): void { console.log("Usage: pnpm credentials:cli [options]"); console.log(""); console.log("Commands:"); - console.log(" init Create credentials.json5 and skills.env.json5 (empty templates)"); + console.log(" init Create credentials.json5 (empty template)"); console.log(" help Show this help"); console.log(""); console.log("Options:"); console.log(" --force Overwrite existing files"); - console.log(" --core-only Only create credentials.json5"); - console.log(" --skills-only Only create skills.env.json5"); console.log(" --path Override credentials path (SMC_CREDENTIALS_PATH)"); - console.log(" --skills-path Override skills env path (SMC_SKILLS_ENV_PATH)"); + console.log(""); + console.log("Skill-specific API keys are stored in .env files within each skill's directory."); + console.log("Example: ~/.super-multica/skills//.env"); console.log(""); console.log("Examples:"); console.log(" pnpm credentials:cli init"); console.log(" pnpm credentials:cli init --force"); - console.log(" pnpm credentials:cli init --core-only"); - console.log(" pnpm credentials:cli init --skills-only"); } function buildCoreTemplate(): string { @@ -51,23 +49,10 @@ function buildCoreTemplate(): string { `; } -function buildSkillsTemplate(): string { - return `{ - env: { - // Dynamic keys (skills, plugins, integrations) - // LINEAR_API_KEY: "lin-..." - } -} -`; -} - function parseArgs(argv: string[]) { const args = [...argv]; let force = false; let pathOverride: string | undefined; - let skillsPathOverride: string | undefined; - let coreOnly = false; - let skillsOnly = false; const positional: string[] = []; while (args.length > 0) { @@ -77,76 +62,42 @@ function parseArgs(argv: string[]) { force = true; continue; } - if (arg === "--core-only") { - coreOnly = true; - continue; - } - if (arg === "--skills-only") { - skillsOnly = true; - continue; - } if (arg === "--path") { pathOverride = args.shift(); continue; } - if (arg === "--skills-path") { - skillsPathOverride = args.shift(); - continue; - } if (arg === "--help" || arg === "-h") { - return { command: "help" as Command, force, pathOverride, skillsPathOverride, coreOnly, skillsOnly }; + return { command: "help" as Command, force, pathOverride }; } positional.push(arg); } const command = (positional[0] || "help") as Command; - return { command, force, pathOverride, skillsPathOverride, coreOnly, skillsOnly }; + return { command, force, pathOverride }; } -function cmdInit(force: boolean, pathOverride?: string, skillsPathOverride?: string, coreOnly?: boolean, skillsOnly?: boolean): void { - const createCore = skillsOnly ? false : true; - const createSkills = coreOnly ? false : true; - - if (!createCore && !createSkills) { - console.error("Error: both --core-only and --skills-only were provided."); +function cmdInit(force: boolean, pathOverride?: string): void { + const path = pathOverride ?? getCredentialsPath(); + if (existsSync(path) && !force) { + console.error(`Error: credentials file already exists at ${path}`); + console.error("Use --force to overwrite."); process.exit(1); } + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, buildCoreTemplate(), "utf8"); + chmodSync(path, 0o600); + console.log(`Created: ${path}`); - if (createCore) { - const path = pathOverride ?? getCredentialsPath(); - if (existsSync(path) && !force) { - console.error(`Error: credentials file already exists at ${path}`); - console.error("Use --force to overwrite."); - process.exit(1); - } - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, buildCoreTemplate(), "utf8"); - chmodSync(path, 0o600); - console.log(`Created: ${path}`); - } - - if (createSkills) { - const skillsPath = skillsPathOverride ?? getSkillsEnvPath(); - if (existsSync(skillsPath) && !force) { - console.error(`Error: skills env file already exists at ${skillsPath}`); - console.error("Use --force to overwrite."); - process.exit(1); - } - mkdirSync(dirname(skillsPath), { recursive: true }); - writeFileSync(skillsPath, buildSkillsTemplate(), "utf8"); - chmodSync(skillsPath, 0o600); - console.log(`Created: ${skillsPath}`); - } - - console.log("Edit these files to add your credentials."); + console.log("Edit this file to add your credentials."); + console.log("Skill-specific API keys go in .env files within each skill's directory."); } async function main() { - const { command, force, pathOverride, skillsPathOverride, coreOnly, skillsOnly } = parseArgs(process.argv.slice(2)); + const { command, force, pathOverride } = parseArgs(process.argv.slice(2)); switch (command) { case "init": - cmdInit(force, pathOverride, skillsPathOverride, coreOnly, skillsOnly); + cmdInit(force, pathOverride); break; case "help": default: diff --git a/packages/core/src/agent/credentials.ts b/packages/core/src/agent/credentials.ts index edc501ea..6e6f8363 100644 --- a/packages/core/src/agent/credentials.ts +++ b/packages/core/src/agent/credentials.ts @@ -35,12 +35,7 @@ export type CredentialsConfig = { channels?: Record> | undefined> | undefined; }; -type SkillsEnvConfig = { - env?: Record | undefined; -}; - const DEFAULT_CREDENTIALS_PATH = join(DATA_DIR, "credentials.json5"); -const DEFAULT_SKILLS_ENV_PATH = join(DATA_DIR, "skills.env.json5"); function expandHome(value: string): string { if (value === "~") return homedir(); @@ -58,42 +53,16 @@ function isTestEnv(): boolean { ); } -function isString(value: unknown): value is string { - return typeof value === "string"; -} - -function setEnvValue(target: Record, key: string, value: unknown): void { - if (isString(value)) { - target[key] = value; - } -} - -function applyEnvMap(target: Record, env?: Record): void { - if (!env) return; - for (const [key, value] of Object.entries(env)) { - setEnvValue(target, key, value); - } -} - export function getCredentialsPath(): string { const raw = process.env.SMC_CREDENTIALS_PATH ?? DEFAULT_CREDENTIALS_PATH; return expandHome(raw); } -export function getSkillsEnvPath(): string { - const raw = process.env.SMC_SKILLS_ENV_PATH ?? DEFAULT_SKILLS_ENV_PATH; - return expandHome(raw); -} - export class CredentialManager { private corePath: string | null = null; - private skillsPath: string | null = null; private disabledState: boolean | null = null; private coreConfig: CredentialsConfig | null = null; - private skillsConfig: SkillsEnvConfig | null = null; - private resolvedSkillsEnv: Record | null = null; private coreMtimeMs: number | null = null; - private skillsMtimeMs: number | null = null; private isDisabled(): boolean { if (process.env.SMC_CREDENTIALS_DISABLE === "1") return true; @@ -139,63 +108,6 @@ export class CredentialManager { } } - private loadSkillsEnv(): void { - const path = getSkillsEnvPath(); - const disabled = this.isDisabled(); - let mtimeMs: number | null = null; - - if (!disabled && existsSync(path)) { - try { - mtimeMs = statSync(path).mtimeMs; - } catch { - mtimeMs = null; - } - } - - if ( - this.skillsPath === path - && this.disabledState === disabled - && this.resolvedSkillsEnv - && this.skillsMtimeMs === mtimeMs - ) { - return; - } - - this.skillsPath = path; - this.disabledState = disabled; - this.skillsConfig = null; - this.resolvedSkillsEnv = null; - this.skillsMtimeMs = mtimeMs; - - if (disabled) return; - if (mtimeMs === null) return; - - const raw = readFileSync(path, "utf8"); - try { - this.skillsConfig = JSON5.parse(raw) as SkillsEnvConfig; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - throw new Error(`Failed to parse skills env file (${path}): ${message}`); - } - } - - private buildSkillsEnv(): Record { - const env: Record = {}; - if (!this.skillsConfig) return env; - - applyEnvMap(env, this.skillsConfig.env); - - return env; - } - - private getResolvedSkillsEnv(): Record { - this.loadSkillsEnv(); - if (!this.resolvedSkillsEnv) { - this.resolvedSkillsEnv = this.buildSkillsEnv(); - } - return this.resolvedSkillsEnv; - } - getLlmProvider(): string | undefined { this.loadCore(); return this.coreConfig?.llm?.provider; @@ -211,22 +123,6 @@ export class CredentialManager { return this.coreConfig?.tools?.[toolName]; } - getEnv(name: string): string | undefined { - const resolved = this.getResolvedSkillsEnv(); - if (Object.prototype.hasOwnProperty.call(resolved, name)) { - return resolved[name]; - } - return process.env[name]; - } - - hasEnv(name: string): boolean { - const resolved = this.getResolvedSkillsEnv(); - if (Object.prototype.hasOwnProperty.call(resolved, name)) { - return true; - } - return name in process.env; - } - /** * Get explicit profile order for a provider from credentials.json5 `llm.order`. * Returns undefined if no explicit order is configured. @@ -257,19 +153,11 @@ export class CredentialManager { return this.coreConfig?.channels ?? {}; } - getResolvedEnvSnapshot(): Record { - return { ...this.getResolvedSkillsEnv() }; - } - reset(): void { this.corePath = null; - this.skillsPath = null; this.disabledState = null; this.coreConfig = null; - this.skillsConfig = null; - this.resolvedSkillsEnv = null; this.coreMtimeMs = null; - this.skillsMtimeMs = null; } /** diff --git a/packages/core/src/agent/index.ts b/packages/core/src/agent/index.ts index 13325fe0..6dccd6f7 100644 --- a/packages/core/src/agent/index.ts +++ b/packages/core/src/agent/index.ts @@ -7,7 +7,7 @@ export * from "./skills/index.js"; export * from "./channel.js"; export * from "./sync-agent.js"; export * from "./async-agent.js"; -export { credentialManager, getCredentialsPath, getSkillsEnvPath, type CredentialsConfig } from "./credentials.js"; +export { credentialManager, getCredentialsPath, type CredentialsConfig } from "./credentials.js"; export * from "./providers/index.js"; export * from "./tools.js"; export * from "./tools/policy.js";