This commit is contained in:
ZacharyZhang-NY committed 2026-03-18 01:15:58 -04:00
1 parent 7a3db27ffa
commit 1ba3147ed2
96 files changed
+9400 -6269

No files matched your search

@@ -236,7 +236,7 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
env.AGENT_COMPANY_API_KEY = authToken;
}
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
const runtimeEnv = ensurePathInEnv(env);
await ensureCommandResolvable(command, cwd, runtimeEnv);
const timeoutSec = asNumber(config.timeoutSec, 0);
@@ -78,7 +78,7 @@ export async function testEnvironment(
for (const [key, value] of Object.entries(envConfig)) {
if (typeof value === "string") env[key] = value;
}
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
const runtimeEnv = ensurePathInEnv(env);
try {
await ensureCommandResolvable(command, cwd, runtimeEnv);
checks.push({
@@ -96,15 +96,13 @@ export async function testEnvironment(
}
const configApiKey = env.ANTHROPIC_API_KEY;
const hostApiKey = process.env.ANTHROPIC_API_KEY;
if (isNonEmpty(configApiKey) || isNonEmpty(hostApiKey)) {
const source = isNonEmpty(configApiKey) ? "adapter config env" : "server environment";
if (isNonEmpty(configApiKey)) {
checks.push({
code: "claude_anthropic_api_key_overrides_subscription",
level: "warn",
message:
"ANTHROPIC_API_KEY is set. Claude will use API-key auth instead of subscription credentials.",
detail: `Detected in ${source}.`,
detail: "Detected in adapter runtime env.",
hint: "Unset ANTHROPIC_API_KEY if you want subscription-based Claude login behavior.",
});
} else {
@@ -243,7 +243,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
env.AGENT_COMPANY_API_KEY = authToken;
}
const billingType = resolveCodexBillingType(env);
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
const runtimeEnv = ensurePathInEnv(env);
await ensureCommandResolvable(command, cwd, runtimeEnv);
const timeoutSec = asNumber(config.timeoutSec, 0);
@@ -80,7 +80,7 @@ export async function testEnvironment(
for (const [key, value] of Object.entries(envConfig)) {
if (typeof value === "string") env[key] = value;
}
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
const runtimeEnv = ensurePathInEnv(env);
try {
await ensureCommandResolvable(command, cwd, runtimeEnv);
checks.push({
@@ -98,21 +98,19 @@ export async function testEnvironment(
}
const configOpenAiKey = env.OPENAI_API_KEY;
const hostOpenAiKey = process.env.OPENAI_API_KEY;
if (isNonEmpty(configOpenAiKey) || isNonEmpty(hostOpenAiKey)) {
const source = isNonEmpty(configOpenAiKey) ? "adapter config env" : "server environment";
if (isNonEmpty(configOpenAiKey)) {
checks.push({
code: "codex_openai_api_key_present",
level: "info",
message: "OPENAI_API_KEY is set for Codex authentication.",
detail: `Detected in ${source}.`,
detail: "Detected in adapter runtime env.",
});
} else {
checks.push({
code: "codex_openai_api_key_missing",
level: "warn",
message: "OPENAI_API_KEY is not set. Codex runs may fail until authentication is configured.",
hint: "Set OPENAI_API_KEY in adapter env, shell environment, or Codex auth configuration.",
hint: "Set OPENAI_API_KEY in connector secret bindings or Codex auth configuration.",
});
}
@@ -145,6 +143,7 @@ export async function testEnvironment(
})();
const args = ["exec", "--json"];
args.push("--skip-git-repo-check");
if (search) args.unshift("--search");
if (bypass) args.push("--dangerously-bypass-approvals-and-sandbox");
if (model) args.push("--model", model);
@@ -214,7 +214,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
env.AGENT_COMPANY_API_KEY = authToken;
}
const billingType = resolveGeminiBillingType(env);
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
const runtimeEnv = ensurePathInEnv(env);
await ensureCommandResolvable(command, cwd, runtimeEnv);
const timeoutSec = asNumber(config.timeoutSec, 0);
@@ -70,7 +70,7 @@ export async function testEnvironment(
for (const [key, value] of Object.entries(envConfig)) {
if (typeof value === "string") env[key] = value;
}
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
const runtimeEnv = ensurePathInEnv(env);
try {
await ensureCommandResolvable(command, cwd, runtimeEnv);
checks.push({
@@ -88,22 +88,16 @@ export async function testEnvironment(
}
const configGeminiApiKey = env.GEMINI_API_KEY;
const hostGeminiApiKey = process.env.GEMINI_API_KEY;
const configGoogleApiKey = env.GOOGLE_API_KEY;
const hostGoogleApiKey = process.env.GOOGLE_API_KEY;
const hasGca = env.GOOGLE_GENAI_USE_GCA === "true" || process.env.GOOGLE_GENAI_USE_GCA === "true";
const hasGca = env.GOOGLE_GENAI_USE_GCA === "true";
if (
isNonEmpty(configGeminiApiKey) ||
isNonEmpty(hostGeminiApiKey) ||
isNonEmpty(configGoogleApiKey) ||
isNonEmpty(hostGoogleApiKey) ||
hasGca
) {
const source = hasGca
? "Google account login (GCA)"
: isNonEmpty(configGeminiApiKey) || isNonEmpty(configGoogleApiKey)
? "adapter config env"
: "server environment";
: "adapter runtime env";
checks.push({
code: "gemini_api_key_present",
level: "info",
@@ -115,7 +109,7 @@ export async function testEnvironment(
code: "gemini_api_key_missing",
level: "info",
message: "No explicit API key detected. Gemini CLI may still authenticate via `gemini auth login` (OAuth).",
hint: "If the hello probe fails with an auth error, set GEMINI_API_KEY or GOOGLE_API_KEY in adapter env, or run `gemini auth login`.",
hint: "If the hello probe fails with an auth error, set GEMINI_API_KEY or GOOGLE_API_KEY in connector secret bindings, or run `gemini auth login`.",
});
}
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@agentcompany/adapter-kimi-local",
"version": "0.1.0",
"type": "module",
"exports": {
".": "./src/index.ts",
"./server": "./src/server/index.ts",
"./ui": "./src/ui/index.ts",
"./cli": "./src/cli/index.ts"
},
"publishConfig": {
"access": "public",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"import": "./dist/server/index.js"
},
"./ui": {
"types": "./dist/ui/index.d.ts",
"import": "./dist/ui/index.js"
},
"./cli": {
"types": "./dist/cli/index.d.ts",
"import": "./dist/cli/index.js"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentcompany/adapter-utils": "workspace:*",
"picocolors": "^1.1.1"
},
"devDependencies": {
"@types/node": "^24.6.0",
"typescript": "^5.7.3"
}
}
@@ -0,0 +1,9 @@
import pc from "picocolors";
export function printKimiStreamEvent(line: string, debug = false) {
if (debug) {
console.log(pc.dim(`[kimi] ${line}`));
return;
}
console.log(line);
}
@@ -0,0 +1 @@
export { printKimiStreamEvent } from "./format-event.js";
+33
View File
@@ -0,0 +1,33 @@
export const type = "kimi_local";
export const label = "Kimi Code (local)";
export const DEFAULT_KIMI_LOCAL_MODEL = "kimi-for-coding";
export const models = [
{ id: DEFAULT_KIMI_LOCAL_MODEL, label: DEFAULT_KIMI_LOCAL_MODEL },
];
export const agentConfigurationDoc = `# kimi_local agent configuration
Adapter: kimi_local
Use when:
- You want AgentCompany to invoke the official Kimi Code CLI locally
- You accept generic CLI execution without session resume or structured transcript guarantees
Core fields:
- cwd (string, optional): absolute working directory fallback for the agent process
- promptTemplate (string, optional): prompt template rendered by AgentCompany before execution
- model (string, optional): informational only, defaults to kimi-for-coding
- command (string, optional): defaults to "kimi"
- extraArgs (string[], optional): additional CLI args
- env (object, optional): KEY=VALUE environment variables
Operational fields:
- timeoutSec (number, optional): run timeout in seconds
- graceSec (number, optional): graceful termination period in seconds
Notes:
- Kimi integration currently runs in generic CLI mode.
- Session resume, model discovery, graceful cancel handshakes, structured transcript, and cost attribution are not guaranteed.
- AgentCompany must surface these capability downgrades explicitly in the desktop UI.
`;
@@ -0,0 +1,73 @@
import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils";
import {
asNumber,
asString,
asStringArray,
ensureAbsoluteDirectory,
ensureCommandResolvable,
ensurePathInEnv,
runChildProcess,
} from "@agentcompany/adapter-utils/server-utils";
import { DEFAULT_KIMI_LOCAL_MODEL } from "../index.js";
function normalizeEnv(input: unknown): Record<string, string> {
if (typeof input !== "object" || input === null || Array.isArray(input)) return {};
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
if (typeof value === "string") env[key] = value;
}
return env;
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, config, context, onLog } = ctx;
const command = asString(config.command, "kimi");
const cwd = asString(config.cwd, process.cwd());
const model = asString(config.model, DEFAULT_KIMI_LOCAL_MODEL);
const timeoutSec = asNumber(config.timeoutSec, 0);
const graceSec = asNumber(config.graceSec, 20);
const extraArgs = asStringArray(config.extraArgs);
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
const prompt = typeof context.taskTitle === "string" && context.taskTitle.length > 0
? [
context.taskTitle,
typeof context.taskDescription === "string" && context.taskDescription.length > 0
? context.taskDescription
: "",
].filter(Boolean).join("\n\n")
: [
`You are agent ${ctx.agent.name}.`,
"Respond with a concise execution summary when complete.",
].join("\n\n");
const env = normalizeEnv(ensurePathInEnv(normalizeEnv(config.env)));
await ensureCommandResolvable(command, cwd, env);
const result = await runChildProcess(runId, command, extraArgs, {
cwd,
env,
timeoutSec,
graceSec,
stdin: prompt,
onLog,
});
const summary =
result.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.slice(-8)
.join("\n") || null;
return {
exitCode: result.exitCode,
signal: result.signal,
timedOut: result.timedOut,
summary,
model,
billingType: "unknown",
};
}
@@ -0,0 +1,2 @@
export { execute } from "./execute.js";
export { testEnvironment } from "./test.js";
@@ -0,0 +1,62 @@
import type {
AdapterEnvironmentTestContext,
AdapterEnvironmentTestResult,
} from "@agentcompany/adapter-utils";
import {
asString,
ensureCommandResolvable,
ensurePathInEnv,
} from "@agentcompany/adapter-utils/server-utils";
function normalizeEnv(input: unknown): Record<string, string> {
if (typeof input !== "object" || input === null || Array.isArray(input)) return {};
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
if (typeof value === "string") env[key] = value;
}
return env;
}
export async function testEnvironment(
ctx: AdapterEnvironmentTestContext,
): Promise<AdapterEnvironmentTestResult> {
const command = asString(ctx.config.command, "kimi");
const cwd = asString(ctx.config.cwd, process.cwd());
const env = normalizeEnv(ensurePathInEnv(normalizeEnv(ctx.config.env)));
try {
await ensureCommandResolvable(command, cwd, env);
return {
adapterType: "kimi_local",
status: "warn",
checks: [
{
code: "kimi_command_detected",
level: "info",
message: `Detected Kimi Code command "${command}".`,
},
{
code: "kimi_runtime_generic_mode",
level: "warn",
message:
"Kimi integration is running in generic CLI mode. Session resume, model discovery, and structured transcript are not guaranteed.",
},
],
testedAt: new Date().toISOString(),
};
} catch (error) {
return {
adapterType: "kimi_local",
status: "fail",
checks: [
{
code: "kimi_command_missing",
level: "error",
message: error instanceof Error ? error.message : "Kimi command is not available.",
hint: "Install Kimi Code with `curl -L code.kimi.com/install.sh | bash` and ensure the CLI is available on PATH.",
},
],
testedAt: new Date().toISOString(),
};
}
}
@@ -0,0 +1,72 @@
import type { CreateConfigValues } from "@agentcompany/adapter-utils";
import { DEFAULT_KIMI_LOCAL_MODEL } from "../index.js";
function parseCommaArgs(value: string): string[] {
return value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
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<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
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<string, unknown>;
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 buildKimiLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const config: Record<string, unknown> = {};
if (v.cwd) config.cwd = v.cwd;
if (v.promptTemplate) config.promptTemplate = v.promptTemplate;
config.model = v.model || DEFAULT_KIMI_LOCAL_MODEL;
config.timeoutSec = 0;
config.graceSec = 20;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
if (Object.keys(env).length > 0) config.env = env;
if (v.command) config.command = v.command;
if (v.extraArgs) config.extraArgs = parseCommaArgs(v.extraArgs);
return config;
}
@@ -0,0 +1,2 @@
export { buildKimiLocalConfig } from "./build-config.js";
export { parseKimiStdoutLine } from "./parse-stdout.js";
@@ -0,0 +1,7 @@
import type { TranscriptEntry } from "@agentcompany/adapter-utils";
export function parseKimiStdoutLine(line: string, ts: string): TranscriptEntry[] {
const text = line.trim();
if (!text) return [];
return [{ kind: "stdout", ts, text: line }];
}
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { parseKimiStdoutLine } from "../src/ui/parse-stdout.js";
describe("parseKimiStdoutLine", () => {
it("returns a stdout entry for non-empty lines", () => {
const entries = parseKimiStdoutLine("hello", "2026-03-17T00:00:00.000Z");
expect(entries).toEqual([
{ kind: "stdout", ts: "2026-03-17T00:00:00.000Z", text: "hello" },
]);
});
it("drops blank lines", () => {
expect(parseKimiStdoutLine(" ", "2026-03-17T00:00:00.000Z")).toEqual([]);
});
});
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
@@ -159,7 +159,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
env.AGENT_COMPANY_API_KEY = authToken;
}
const runtimeEnv = Object.fromEntries(
Object.entries(ensurePathInEnv({ ...process.env, ...env })).filter(
Object.entries(ensurePathInEnv(env)).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
@@ -106,7 +106,7 @@ export async function discoverOpenCodeModels(input: {
const command = resolveOpenCodeCommand(input.command);
const cwd = asString(input.cwd, process.cwd());
const env = normalizeEnv(input.env);
const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...env }));
const runtimeEnv = normalizeEnv(ensurePathInEnv(env));
const result = await runChildProcess(
`opencode-models-${Date.now()}-${Math.random().toString(16).slice(2)}`,
@@ -90,7 +90,7 @@ export async function testEnvironment(
});
}
const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...env }));
const runtimeEnv = normalizeEnv(ensurePathInEnv(env));
const cwdInvalid = checks.some((check) => check.code === "opencode_cwd_invalid");
if (cwdInvalid) {
@@ -191,9 +191,13 @@ export async function testEnvironment(
}
}
const modelUnavailable = checks.some((check) => check.code === "opencode_hello_probe_model_unavailable");
if (!configuredModel && !modelUnavailable) {
// No model configured – skip model requirement if no model-related checks exist
if (!configuredModel) {
checks.push({
code: "opencode_model_missing",
level: "warn",
message: "OpenCode requires a configured provider/model ID before runs can start.",
hint: "Run `opencode models`, choose a provider/model, and save it in the connector settings.",
});
} else if (configuredModel && canRunProbe) {
try {
await ensureOpenCodeModelConfiguredAndAvailable({
+1 -1
View File
@@ -272,7 +272,7 @@ export async function runChildProcess(
const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg));
return new Promise<RunProcessResult>((resolve, reject) => {
const rawMerged: NodeJS.ProcessEnv = { ...process.env, ...opts.env };
const rawMerged: NodeJS.ProcessEnv = { ...opts.env };
// Strip Claude Code nesting-guard env vars so spawned `claude` processes
// don't refuse to start with "cannot be launched inside another session".