feat(credentials): add JSON5 credential system
This commit is contained in:
parent
b1d80f29ae
commit
3ee8946e29
10 changed files with 454 additions and 110 deletions
19
package.json
19
package.json
|
|
@ -8,17 +8,19 @@
|
|||
"multica": "./bin/multica-interactive.mjs",
|
||||
"multica-interactive": "./bin/multica-interactive.mjs",
|
||||
"multica-cli": "./bin/multica-cli.mjs",
|
||||
"multica-profile": "./bin/multica-profile.mjs"
|
||||
"multica-profile": "./bin/multica-profile.mjs",
|
||||
"multica-credentials": "./bin/multica-credentials.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently -n gateway,console,web -c blue,yellow,green \"pnpm dev:gateway\" \"pnpm dev:console\" \"pnpm dev:web\"",
|
||||
"agent:cli": "tsx --env-file=.env src/agent/cli/non-interactive.ts",
|
||||
"agent:interactive": "tsx --env-file=.env src/agent/cli/interactive.ts",
|
||||
"agent:profile": "tsx --env-file=.env src/agent/cli/profile.ts",
|
||||
"skills:cli": "tsx --env-file=.env src/agent/cli/skills.ts",
|
||||
"tools:cli": "tsx --env-file=.env src/agent/cli/tools.ts",
|
||||
"dev:gateway": "tsx --env-file=.env --watch src/gateway/main.ts",
|
||||
"dev:console": "tsx --env-file=.env --watch src/console/main.ts",
|
||||
"agent:cli": "tsx src/agent/cli/non-interactive.ts",
|
||||
"agent:interactive": "tsx src/agent/cli/interactive.ts",
|
||||
"agent:profile": "tsx src/agent/cli/profile.ts",
|
||||
"credentials:cli": "tsx src/agent/credentials-cli.ts",
|
||||
"skills:cli": "tsx src/agent/cli/skills.ts",
|
||||
"tools:cli": "tsx src/agent/cli/tools.ts",
|
||||
"dev:gateway": "tsx --watch src/gateway/main.ts",
|
||||
"dev:console": "tsx --watch src/console/main.ts",
|
||||
"dev:web": "pnpm --filter @multica/web dev",
|
||||
"dev:desktop": "pnpm --filter @multica/desktop dev",
|
||||
"build": "turbo build",
|
||||
|
|
@ -59,6 +61,7 @@
|
|||
"@nestjs/websockets": "^11.1.12",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"fast-glob": "^3.3.3",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.18.12",
|
||||
"nestjs-pino": "^4.5.0",
|
||||
"pino": "^10.3.0",
|
||||
|
|
|
|||
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
|
|
@ -34,13 +34,13 @@ importers:
|
|||
dependencies:
|
||||
'@mariozechner/pi-agent-core':
|
||||
specifier: ^0.50.3
|
||||
version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)
|
||||
version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@mariozechner/pi-ai':
|
||||
specifier: ^0.50.3
|
||||
version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)
|
||||
version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@mariozechner/pi-coding-agent':
|
||||
specifier: ^0.50.3
|
||||
version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)
|
||||
version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@mozilla/readability':
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
|
|
@ -68,6 +68,9 @@ importers:
|
|||
fast-glob:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
linkedom:
|
||||
specifier: ^0.18.12
|
||||
version: 0.18.12
|
||||
|
|
@ -6397,11 +6400,11 @@ snapshots:
|
|||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.0.2
|
||||
|
||||
'@anthropic-ai/sdk@0.71.2(zod@3.25.76)':
|
||||
'@anthropic-ai/sdk@0.71.2(zod@4.3.6)':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
optionalDependencies:
|
||||
zod: 3.25.76
|
||||
zod: 4.3.6
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
dependencies:
|
||||
|
|
@ -7424,12 +7427,12 @@ snapshots:
|
|||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@google/genai@1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))':
|
||||
'@google/genai@1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))':
|
||||
dependencies:
|
||||
google-auth-library: 10.5.0
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
'@modelcontextprotocol/sdk': 1.25.3(hono@4.11.7)(zod@3.25.76)
|
||||
'@modelcontextprotocol/sdk': 1.25.3(hono@4.11.7)(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
|
|
@ -7684,9 +7687,9 @@ snapshots:
|
|||
std-env: 3.10.0
|
||||
yoctocolors: 2.1.2
|
||||
|
||||
'@mariozechner/pi-agent-core@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)':
|
||||
'@mariozechner/pi-agent-core@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)
|
||||
'@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@mariozechner/pi-tui': 0.50.3
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
|
|
@ -7697,21 +7700,21 @@ snapshots:
|
|||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-ai@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)':
|
||||
'@mariozechner/pi-ai@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.71.2(zod@3.25.76)
|
||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock-runtime': 3.978.0
|
||||
'@google/genai': 1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))
|
||||
'@google/genai': 1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))
|
||||
'@mistralai/mistralai': 1.10.0
|
||||
'@sinclair/typebox': 0.34.48
|
||||
ajv: 8.17.1
|
||||
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||
chalk: 5.6.2
|
||||
openai: 6.10.0(ws@8.18.3)(zod@3.25.76)
|
||||
openai: 6.10.0(ws@8.18.3)(zod@4.3.6)
|
||||
partial-json: 0.1.7
|
||||
proxy-agent: 6.5.0
|
||||
undici: 7.19.2
|
||||
zod-to-json-schema: 3.25.1(zod@3.25.76)
|
||||
zod-to-json-schema: 3.25.1(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- aws-crt
|
||||
|
|
@ -7721,12 +7724,12 @@ snapshots:
|
|||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-coding-agent@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)':
|
||||
'@mariozechner/pi-coding-agent@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@mariozechner/clipboard': 0.3.0
|
||||
'@mariozechner/jiti': 2.6.5
|
||||
'@mariozechner/pi-agent-core': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)
|
||||
'@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)
|
||||
'@mariozechner/pi-agent-core': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@mariozechner/pi-tui': 0.50.3
|
||||
'@silvia-odwyer/photon-node': 0.3.4
|
||||
chalk: 5.6.2
|
||||
|
|
@ -7784,6 +7787,29 @@ snapshots:
|
|||
- hono
|
||||
- supports-color
|
||||
|
||||
'@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.9(hono@4.11.7)
|
||||
ajv: 8.17.1
|
||||
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.6
|
||||
cross-spawn: 7.0.6
|
||||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.0.6
|
||||
express: 5.2.1
|
||||
express-rate-limit: 7.5.1(express@5.2.1)
|
||||
jose: 6.1.3
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.1
|
||||
raw-body: 3.0.2
|
||||
zod: 4.3.6
|
||||
zod-to-json-schema: 3.25.1(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- hono
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@mozilla/readability@0.6.0': {}
|
||||
|
||||
'@mswjs/interceptors@0.40.0':
|
||||
|
|
@ -12023,10 +12049,10 @@ snapshots:
|
|||
powershell-utils: 0.1.0
|
||||
wsl-utils: 0.3.1
|
||||
|
||||
openai@6.10.0(ws@8.18.3)(zod@3.25.76):
|
||||
openai@6.10.0(ws@8.18.3)(zod@4.3.6):
|
||||
optionalDependencies:
|
||||
ws: 8.18.3
|
||||
zod: 3.25.76
|
||||
zod: 4.3.6
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
|
|
@ -13642,6 +13668,10 @@ snapshots:
|
|||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
zod-to-json-schema@3.25.1(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
zod-validation-error@4.0.2(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ async function build() {
|
|||
{ entry: "src/agent/cli/interactive.ts", outfile: "bin/multica-interactive.mjs" },
|
||||
{ entry: "src/agent/cli/non-interactive.ts", outfile: "bin/multica-cli.mjs" },
|
||||
{ entry: "src/agent/cli/profile.ts", outfile: "bin/multica-profile.mjs" },
|
||||
{ entry: "src/agent/credentials-cli.ts", outfile: "bin/multica-credentials.mjs" },
|
||||
];
|
||||
|
||||
for (const { entry, outfile } of entryPoints) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
checkEligibilityDetailed,
|
||||
type DiagnosticItem,
|
||||
} from "../skills/index.js";
|
||||
import { credentialManager } from "../credentials.js";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
|
@ -365,7 +366,7 @@ function checkBinaries(bins: string[]): Map<string, boolean> {
|
|||
function checkEnvVars(envs: string[]): Map<string, boolean> {
|
||||
const result = new Map<string, boolean>();
|
||||
for (const env of envs) {
|
||||
result.set(env, env in process.env);
|
||||
result.set(env, credentialManager.hasEnv(env));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
160
src/agent/credentials-cli.ts
Normal file
160
src/agent/credentials-cli.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Credentials CLI
|
||||
*
|
||||
* Commands:
|
||||
* init Create credentials.json5 and skills.env.json5
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { getCredentialsPath, getSkillsEnvPath } from "./credentials.js";
|
||||
|
||||
type Command = "init" | "help";
|
||||
|
||||
function printUsage(): void {
|
||||
console.log("Usage: pnpm credentials:cli <command> [options]");
|
||||
console.log("");
|
||||
console.log("Commands:");
|
||||
console.log(" init Create credentials.json5 and skills.env.json5 (empty templates)");
|
||||
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("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 {
|
||||
return `{
|
||||
version: 1,
|
||||
llm: {
|
||||
// provider: "openai",
|
||||
providers: {
|
||||
// openai: { apiKey: "sk-...", baseUrl: "https://api.openai.com/v1", model: "gpt-4.1" }
|
||||
}
|
||||
},
|
||||
tools: {
|
||||
// brave: { apiKey: "brv-..." },
|
||||
// perplexity: { apiKey: "pplx-...", baseUrl: "https://api.perplexity.ai", model: "perplexity/sonar-pro" }
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
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) {
|
||||
const arg = args.shift();
|
||||
if (!arg) break;
|
||||
if (arg === "--force" || arg === "-f") {
|
||||
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 };
|
||||
}
|
||||
positional.push(arg);
|
||||
}
|
||||
|
||||
const command = (positional[0] || "help") as Command;
|
||||
return { command, force, pathOverride, skillsPathOverride, coreOnly, skillsOnly };
|
||||
}
|
||||
|
||||
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.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { command, force, pathOverride, skillsPathOverride, coreOnly, skillsOnly } = parseArgs(process.argv.slice(2));
|
||||
|
||||
switch (command) {
|
||||
case "init":
|
||||
cmdInit(force, pathOverride, skillsPathOverride, coreOnly, skillsOnly);
|
||||
break;
|
||||
case "help":
|
||||
default:
|
||||
printUsage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
202
src/agent/credentials.ts
Normal file
202
src/agent/credentials.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import JSON5 from "json5";
|
||||
import { DATA_DIR } from "../shared/paths.js";
|
||||
|
||||
type ProviderConfig = {
|
||||
apiKey?: string | undefined;
|
||||
baseUrl?: string | undefined;
|
||||
model?: string | undefined;
|
||||
};
|
||||
|
||||
type ToolConfig = {
|
||||
apiKey?: string | undefined;
|
||||
baseUrl?: string | undefined;
|
||||
model?: string | undefined;
|
||||
};
|
||||
|
||||
export type CredentialsConfig = {
|
||||
version?: number | undefined;
|
||||
llm?: {
|
||||
provider?: string | undefined;
|
||||
providers?: Record<string, ProviderConfig> | undefined;
|
||||
} | undefined;
|
||||
tools?: Record<string, ToolConfig> | undefined;
|
||||
};
|
||||
|
||||
type SkillsEnvConfig = {
|
||||
env?: Record<string, string> | 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();
|
||||
if (value.startsWith("~/")) {
|
||||
return join(homedir(), value.slice(2));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isTestEnv(): boolean {
|
||||
return (
|
||||
process.env.NODE_ENV === "test" ||
|
||||
process.env.VITEST !== undefined ||
|
||||
process.env.VITEST_WORKER_ID !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
function setEnvValue(target: Record<string, string>, key: string, value: unknown): void {
|
||||
if (isString(value)) {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function applyEnvMap(target: Record<string, string>, env?: Record<string, string>): 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<string, string> | null = null;
|
||||
|
||||
private isDisabled(): boolean {
|
||||
if (process.env.SMC_CREDENTIALS_DISABLE === "1") return true;
|
||||
return isTestEnv();
|
||||
}
|
||||
|
||||
private loadCore(): void {
|
||||
const path = getCredentialsPath();
|
||||
const disabled = this.isDisabled();
|
||||
|
||||
if (this.corePath === path && this.disabledState === disabled && this.coreConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.corePath = path;
|
||||
this.disabledState = disabled;
|
||||
this.coreConfig = null;
|
||||
|
||||
if (disabled) return;
|
||||
if (!existsSync(path)) return;
|
||||
|
||||
const raw = readFileSync(path, "utf8");
|
||||
try {
|
||||
this.coreConfig = JSON5.parse(raw) as CredentialsConfig;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Failed to parse credentials file (${path}): ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private loadSkillsEnv(): void {
|
||||
const path = getSkillsEnvPath();
|
||||
const disabled = this.isDisabled();
|
||||
|
||||
if (this.skillsPath === path && this.disabledState === disabled && this.resolvedSkillsEnv) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.skillsPath = path;
|
||||
this.disabledState = disabled;
|
||||
this.skillsConfig = null;
|
||||
this.resolvedSkillsEnv = null;
|
||||
|
||||
if (disabled) return;
|
||||
if (!existsSync(path)) 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<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
if (!this.skillsConfig) return env;
|
||||
|
||||
applyEnvMap(env, this.skillsConfig.env);
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
private getResolvedSkillsEnv(): Record<string, string> {
|
||||
this.loadSkillsEnv();
|
||||
if (!this.resolvedSkillsEnv) {
|
||||
this.resolvedSkillsEnv = this.buildSkillsEnv();
|
||||
}
|
||||
return this.resolvedSkillsEnv;
|
||||
}
|
||||
|
||||
getLlmProvider(): string | undefined {
|
||||
this.loadCore();
|
||||
return this.coreConfig?.llm?.provider;
|
||||
}
|
||||
|
||||
getLlmProviderConfig(provider: string): ProviderConfig | undefined {
|
||||
this.loadCore();
|
||||
return this.coreConfig?.llm?.providers?.[provider];
|
||||
}
|
||||
|
||||
getToolConfig(toolName: string): ToolConfig | undefined {
|
||||
this.loadCore();
|
||||
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;
|
||||
}
|
||||
|
||||
getResolvedEnvSnapshot(): Record<string, string> {
|
||||
return { ...this.getResolvedSkillsEnv() };
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.corePath = null;
|
||||
this.skillsPath = null;
|
||||
this.disabledState = null;
|
||||
this.coreConfig = null;
|
||||
this.skillsConfig = null;
|
||||
this.resolvedSkillsEnv = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const credentialManager = new CredentialManager();
|
||||
|
|
@ -6,6 +6,7 @@ import { resolveModel, resolveTools } from "./tools.js";
|
|||
import { SessionManager } from "./session/session-manager.js";
|
||||
import { ProfileManager } from "./profile/index.js";
|
||||
import { SkillManager } from "./skills/index.js";
|
||||
import { credentialManager, getCredentialsPath } from "./credentials.js";
|
||||
import {
|
||||
checkContextWindow,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
|
|
@ -19,28 +20,7 @@ import { mergeToolsConfig, type ToolsConfig } from "./tools/policy.js";
|
|||
*/
|
||||
function resolveApiKey(provider: string, explicitKey?: string): string | undefined {
|
||||
if (explicitKey) return explicitKey;
|
||||
|
||||
const providerEnvMap: Record<string, string> = {
|
||||
openai: "OPENAI_API_KEY",
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
google: "GOOGLE_API_KEY",
|
||||
"google-genai": "GOOGLE_API_KEY",
|
||||
kimi: "MOONSHOT_API_KEY",
|
||||
"kimi-coding": "MOONSHOT_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
groq: "GROQ_API_KEY",
|
||||
mistral: "MISTRAL_API_KEY",
|
||||
together: "TOGETHER_API_KEY",
|
||||
};
|
||||
|
||||
const envVar = providerEnvMap[provider];
|
||||
if (envVar) {
|
||||
return process.env[envVar];
|
||||
}
|
||||
|
||||
// Try generic format: PROVIDER_API_KEY
|
||||
const normalizedProvider = provider.toUpperCase().replace(/-/g, "_");
|
||||
return process.env[`${normalizedProvider}_API_KEY`];
|
||||
return credentialManager.getLlmProviderConfig(provider)?.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,28 +29,7 @@ function resolveApiKey(provider: string, explicitKey?: string): string | undefin
|
|||
*/
|
||||
function resolveBaseUrl(provider: string, explicitUrl?: string): string | undefined {
|
||||
if (explicitUrl) return explicitUrl;
|
||||
|
||||
const providerEnvMap: Record<string, string> = {
|
||||
openai: "OPENAI_BASE_URL",
|
||||
anthropic: "ANTHROPIC_BASE_URL",
|
||||
google: "GOOGLE_BASE_URL",
|
||||
"google-genai": "GOOGLE_BASE_URL",
|
||||
kimi: "MOONSHOT_BASE_URL",
|
||||
"kimi-coding": "MOONSHOT_BASE_URL",
|
||||
deepseek: "DEEPSEEK_BASE_URL",
|
||||
groq: "GROQ_BASE_URL",
|
||||
mistral: "MISTRAL_BASE_URL",
|
||||
together: "TOGETHER_BASE_URL",
|
||||
};
|
||||
|
||||
const envVar = providerEnvMap[provider];
|
||||
if (envVar) {
|
||||
return process.env[envVar];
|
||||
}
|
||||
|
||||
// Try generic format: PROVIDER_BASE_URL
|
||||
const normalizedProvider = provider.toUpperCase().replace(/-/g, "_");
|
||||
return process.env[`${normalizedProvider}_BASE_URL`];
|
||||
return credentialManager.getLlmProviderConfig(provider)?.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -79,28 +38,7 @@ function resolveBaseUrl(provider: string, explicitUrl?: string): string | undefi
|
|||
*/
|
||||
function resolveModelId(provider: string, explicitModel?: string): string | undefined {
|
||||
if (explicitModel) return explicitModel;
|
||||
|
||||
const providerEnvMap: Record<string, string> = {
|
||||
openai: "OPENAI_MODEL",
|
||||
anthropic: "ANTHROPIC_MODEL",
|
||||
google: "GOOGLE_MODEL",
|
||||
"google-genai": "GOOGLE_MODEL",
|
||||
kimi: "MOONSHOT_MODEL",
|
||||
"kimi-coding": "MOONSHOT_MODEL",
|
||||
deepseek: "DEEPSEEK_MODEL",
|
||||
groq: "GROQ_MODEL",
|
||||
mistral: "MISTRAL_MODEL",
|
||||
together: "TOGETHER_MODEL",
|
||||
};
|
||||
|
||||
const envVar = providerEnvMap[provider];
|
||||
if (envVar) {
|
||||
return process.env[envVar];
|
||||
}
|
||||
|
||||
// Try generic format: PROVIDER_MODEL
|
||||
const normalizedProvider = provider.toUpperCase().replace(/-/g, "_");
|
||||
return process.env[`${normalizedProvider}_MODEL`];
|
||||
return credentialManager.getLlmProviderConfig(provider)?.model;
|
||||
}
|
||||
|
||||
export class Agent {
|
||||
|
|
@ -122,7 +60,7 @@ export class Agent {
|
|||
this.debug = options.debug ?? false;
|
||||
|
||||
// Resolve provider and model from options > env vars > defaults
|
||||
const resolvedProvider = options.provider ?? process.env.LLM_PROVIDER ?? "kimi-coding";
|
||||
const resolvedProvider = options.provider ?? credentialManager.getLlmProvider() ?? "kimi-coding";
|
||||
const resolvedModel = resolveModelId(resolvedProvider, options.model);
|
||||
const apiKey = resolveApiKey(resolvedProvider, options.apiKey);
|
||||
|
||||
|
|
@ -181,8 +119,7 @@ export class Agent {
|
|||
if (!model) {
|
||||
throw new Error(
|
||||
`Unknown model: provider="${effectiveProvider}", model="${effectiveModel}". ` +
|
||||
`Check your LLM_PROVIDER and model env vars (e.g. OPENAI_MODEL). ` +
|
||||
`For OpenRouter, use LLM_PROVIDER=openrouter.`,
|
||||
`Check ${getCredentialsPath()} for llm.provider and llm.providers.${effectiveProvider}.model.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { getModel, type Model } from "@mariozechner/pi-ai";
|
|||
import type { SessionEntry, SessionMeta } from "./types.js";
|
||||
import { appendEntry, readEntries, writeEntries } from "./storage.js";
|
||||
import { compactMessages, compactMessagesAsync } from "./compaction.js";
|
||||
import { credentialManager } from "../credentials.js";
|
||||
|
||||
/** Get Kimi model for summarization (use a cheaper model than k2-thinking) */
|
||||
function getSummaryModel(): Model<any> {
|
||||
|
|
@ -11,7 +12,12 @@ function getSummaryModel(): Model<any> {
|
|||
|
||||
/** Get Kimi API key */
|
||||
function getSummaryApiKey(): string | undefined {
|
||||
return process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY;
|
||||
const providers = ["kimi", "moonshot", "kimi-coding"];
|
||||
for (const provider of providers) {
|
||||
const apiKey = credentialManager.getLlmProviderConfig(provider)?.apiKey;
|
||||
if (apiKey) return apiKey;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type SessionManagerOptions = {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
normalizeRequirements,
|
||||
normalizePlatforms,
|
||||
} from "./types.js";
|
||||
import { credentialManager, getSkillsEnvPath } from "../credentials.js";
|
||||
|
||||
// ============================================================================
|
||||
// Diagnostic Types
|
||||
|
|
@ -77,7 +78,7 @@ export function binaryExists(binary: string): boolean {
|
|||
* @returns True if set (even if empty string)
|
||||
*/
|
||||
function envExists(envVar: string): boolean {
|
||||
return envVar in process.env;
|
||||
return credentialManager.hasEnv(envVar);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -450,7 +451,7 @@ function generateEnvHint(envVars: string[], skill: Skill): string {
|
|||
// Check for well-known API key patterns
|
||||
if (envVar.endsWith("_API_KEY") || envVar.endsWith("_KEY")) {
|
||||
const service = envVar.replace(/_API_KEY$|_KEY$/, "").toLowerCase();
|
||||
hints.push(`Set ${envVar} in your environment or add to .env file`);
|
||||
hints.push(`Set ${envVar} in your environment or add to ${getSkillsEnvPath()}`);
|
||||
|
||||
// Add provider-specific hints
|
||||
const providerHint = getApiKeyHint(envVar);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from "./cache.js";
|
||||
import type { CacheEntry } from "./cache.js";
|
||||
import { jsonResult, readNumberParam, readStringParam } from "./param-helpers.js";
|
||||
import { credentialManager } from "../../credentials.js";
|
||||
|
||||
const SEARCH_PROVIDERS = ["brave", "perplexity"] as const;
|
||||
type SearchProvider = (typeof SEARCH_PROVIDERS)[number];
|
||||
|
|
@ -178,21 +179,21 @@ function inferPerplexityBaseUrl(apiKey: string): string {
|
|||
}
|
||||
|
||||
function resolvePerplexityApiKey(): { apiKey: string; source: string } | { apiKey: null; source: "none" } {
|
||||
const perplexityKey = (process.env.PERPLEXITY_API_KEY ?? "").trim();
|
||||
const perplexityKey = (credentialManager.getToolConfig("perplexity")?.apiKey ?? "").trim();
|
||||
if (perplexityKey) {
|
||||
return { apiKey: perplexityKey, source: "PERPLEXITY_API_KEY" };
|
||||
return { apiKey: perplexityKey, source: "perplexity" };
|
||||
}
|
||||
|
||||
const openrouterKey = (process.env.OPENROUTER_API_KEY ?? "").trim();
|
||||
const openrouterKey = (credentialManager.getToolConfig("openrouter")?.apiKey ?? "").trim();
|
||||
if (openrouterKey) {
|
||||
return { apiKey: openrouterKey, source: "OPENROUTER_API_KEY" };
|
||||
return { apiKey: openrouterKey, source: "openrouter" };
|
||||
}
|
||||
|
||||
return { apiKey: null, source: "none" };
|
||||
}
|
||||
|
||||
function resolveBraveApiKey(): string | undefined {
|
||||
return (process.env.BRAVE_API_KEY ?? "").trim() || undefined;
|
||||
return (credentialManager.getToolConfig("brave")?.apiKey ?? "").trim() || undefined;
|
||||
}
|
||||
|
||||
function resolveProvider(requested?: string): SearchProvider {
|
||||
|
|
@ -340,24 +341,26 @@ async function runWebSearch(params: {
|
|||
return {
|
||||
error: "missing_api_key",
|
||||
message:
|
||||
"Perplexity search requires PERPLEXITY_API_KEY or OPENROUTER_API_KEY environment variable.",
|
||||
"Perplexity search requires tools.perplexity.apiKey (or tools.openrouter.apiKey) in credentials.json5.",
|
||||
};
|
||||
}
|
||||
|
||||
const apiKey = perplexityResult.apiKey;
|
||||
const baseUrl = inferPerplexityBaseUrl(apiKey);
|
||||
const perplexityConfig = credentialManager.getToolConfig("perplexity");
|
||||
const baseUrl = (perplexityConfig?.baseUrl ?? "").trim() || inferPerplexityBaseUrl(apiKey);
|
||||
const model = (perplexityConfig?.model ?? "").trim() || DEFAULT_PERPLEXITY_MODEL;
|
||||
const { content, citations } = await runPerplexitySearch({
|
||||
query: params.query,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
model: DEFAULT_PERPLEXITY_MODEL,
|
||||
model,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
query: params.query,
|
||||
provider: params.provider,
|
||||
model: DEFAULT_PERPLEXITY_MODEL,
|
||||
model,
|
||||
tookMs: Date.now() - start,
|
||||
content,
|
||||
citations,
|
||||
|
|
@ -371,7 +374,7 @@ async function runWebSearch(params: {
|
|||
if (!apiKey) {
|
||||
return {
|
||||
error: "missing_api_key",
|
||||
message: "Brave search requires BRAVE_API_KEY environment variable.",
|
||||
message: "Brave search requires tools.brave.apiKey in credentials.json5.",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue