chore(adapter): delete adapter packages and drop build wiring

Removes the four workspace adapter packages (claude-local, codex-local,
gemini-local, adapter-utils) now that all runtime provider dispatch lives
in @mariozechner/pi-ai + pi-agent-core + pi-coding-agent. Refreshes the
lockfile, drops bundledMainDependencies from the Vite config, and adds
pi-coding-agent as a direct dependency for the worker's coding tools.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 02:20:46 -04:00
1 parent 4a120f4fc6
commit 60178ceafa
60 files changed
+2067 -6014

No files matched your search

+1 -8
View File
@@ -3,16 +3,9 @@ import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { defineConfig, externalizeDepsPlugin } from "electron-vite"; import { defineConfig, externalizeDepsPlugin } from "electron-vite";
const bundledMainDependencies = [
"@agentcompany/adapter-claude-local",
"@agentcompany/adapter-codex-local",
"@agentcompany/adapter-gemini-local",
"@agentcompany/adapter-utils",
];
export default defineConfig({ export default defineConfig({
main: { main: {
plugins: [externalizeDepsPlugin({ exclude: bundledMainDependencies })], plugins: [externalizeDepsPlugin()],
resolve: { resolve: {
alias: { alias: {
"@shared": resolve("src/shared"), "@shared": resolve("src/shared"),
+3 -4
View File
@@ -34,10 +34,9 @@
"postinstall": "node scripts/postinstall.mjs" "postinstall": "node scripts/postinstall.mjs"
}, },
"dependencies": { "dependencies": {
"@agentcompany/adapter-claude-local": "workspace:*", "@mariozechner/pi-agent-core": "^0.67.6",
"@agentcompany/adapter-codex-local": "workspace:*", "@mariozechner/pi-ai": "^0.67.6",
"@agentcompany/adapter-gemini-local": "workspace:*", "@mariozechner/pi-coding-agent": "^0.67.6",
"@agentcompany/adapter-utils": "workspace:*",
"@phosphor-icons/react": "2.1.10", "@phosphor-icons/react": "2.1.10",
"@tanstack/react-virtual": "^3.13.23", "@tanstack/react-virtual": "^3.13.23",
"electron-log": "5.4.3", "electron-log": "5.4.3",
@@ -1,51 +0,0 @@
{
"name": "@agentcompany/adapter-claude-local",
"version": "0.3.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",
"skills"
],
"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"
}
}
@@ -1,83 +0,0 @@
import pc from "picocolors";
import { errorText as asErrorText } from "@agentcompany/adapter-utils/parse-utils";
export function printClaudeStreamEvent(raw: string, debug: boolean): void {
const line = raw.trim();
if (!line) return;
let parsed: Record<string, unknown> | null = null;
try {
parsed = JSON.parse(line) as Record<string, unknown>;
} catch {
console.log(line);
return;
}
const type = typeof parsed.type === "string" ? parsed.type : "";
if (type === "system" && parsed.subtype === "init") {
const model = typeof parsed.model === "string" ? parsed.model : "unknown";
const sessionId = typeof parsed.session_id === "string" ? parsed.session_id : "";
console.log(pc.blue(`Claude initialized (model: ${model}${sessionId ? `, session: ${sessionId}` : ""})`));
return;
}
if (type === "assistant") {
const message =
typeof parsed.message === "object" && parsed.message !== null && !Array.isArray(parsed.message)
? (parsed.message as Record<string, unknown>)
: {};
const content = Array.isArray(message.content) ? message.content : [];
for (const blockRaw of content) {
if (typeof blockRaw !== "object" || blockRaw === null || Array.isArray(blockRaw)) continue;
const block = blockRaw as Record<string, unknown>;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text) console.log(pc.green(`assistant: ${text}`));
} else if (blockType === "tool_use") {
const name = typeof block.name === "string" ? block.name : "unknown";
console.log(pc.yellow(`tool_call: ${name}`));
if (block.input !== undefined) {
console.log(pc.gray(JSON.stringify(block.input, null, 2)));
}
}
}
return;
}
if (type === "result") {
const usage =
typeof parsed.usage === "object" && parsed.usage !== null && !Array.isArray(parsed.usage)
? (parsed.usage as Record<string, unknown>)
: {};
const input = Number(usage.input_tokens ?? 0);
const output = Number(usage.output_tokens ?? 0);
const cached = Number(usage.cache_read_input_tokens ?? 0);
const cost = Number(parsed.total_cost_usd ?? 0);
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
const isError = parsed.is_error === true;
const resultText = typeof parsed.result === "string" ? parsed.result : "";
if (resultText) {
console.log(pc.green("result:"));
console.log(resultText);
}
const errors = Array.isArray(parsed.errors) ? parsed.errors.map(asErrorText).filter(Boolean) : [];
if (subtype.startsWith("error") || isError || errors.length > 0) {
console.log(pc.red(`claude_result: subtype=${subtype || "unknown"} is_error=${isError ? "true" : "false"}`));
if (errors.length > 0) {
console.log(pc.red(`claude_errors: ${errors.join(" | ")}`));
}
}
console.log(
pc.blue(
`tokens: in=${Number.isFinite(input) ? input : 0} out=${Number.isFinite(output) ? output : 0} cached=${Number.isFinite(cached) ? cached : 0} cost=$${Number.isFinite(cost) ? cost.toFixed(6) : "0.000000"}`,
),
);
return;
}
if (debug) {
console.log(pc.gray(line));
}
}
@@ -1 +0,0 @@
export { printClaudeStreamEvent } from "./format-event.js";
@@ -1,37 +0,0 @@
export const type = "claude_local";
export const label = "Claude Code (local)";
export const models = [
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
{ id: "claude-haiku-4-6", label: "Claude Haiku 4.6" },
{ id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
];
export const agentConfigurationDoc = `# claude_local agent configuration
Adapter: claude_local
Core fields:
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
- instructionsFilePath (string, optional): absolute path to a markdown instructions file injected at runtime
- model (string, optional): Claude model id
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
- chrome (boolean, optional): pass --chrome when running Claude
- promptTemplate (string, optional): run prompt template
- maxTurnsPerRun (number, optional): max turns for one run
- dangerouslySkipPermissions (boolean, optional): pass --dangerously-skip-permissions to claude
- command (string, optional): defaults to "claude"
- extraArgs (string[], optional): additional CLI args
- env (object, optional): KEY=VALUE environment variables
- workspaceStrategy (object, optional): execution workspace strategy; currently supports { type: "git_worktree", baseRef?, branchTemplate?, worktreeParentDir? }
- workspaceRuntime (object, optional): workspace runtime service intents; local host-managed services are realized before Claude starts and exposed back via context/env
Operational fields:
- timeoutSec (number, optional): run timeout in seconds
- graceSec (number, optional): SIGTERM grace period in seconds
Notes:
- When AgentCompany realizes a workspace/runtime for a run, it injects AGENT_COMPANY_WORKSPACE_* and AGENT_COMPANY_RUNTIME_* env vars for agent-side tooling.
`;
@@ -1,134 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { AdapterExecutionContext, ManagedSkillRef } from "@agentcompany/adapter-utils";
import type { RunProcessResult } from "@agentcompany/adapter-utils/server-utils";
import {
asString,
asNumber,
ensureAbsoluteDirectory,
ensureCommandResolvable,
ensurePathInEnv,
createManagedSkillSymlinks,
} from "@agentcompany/adapter-utils/server-utils";
import {
hasNonEmptyEnvValue,
buildSkillsCandidates,
resolveSkillsDir,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
} from "@agentcompany/adapter-utils";
export {
hasNonEmptyEnvValue,
buildSkillsCandidates,
resolveSkillsDir as resolveAgentCompanySkillsDir,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
};
export const SKILLS_CANDIDATES = buildSkillsCandidates(import.meta.url);
/**
* Create a tmpdir with `.claude/skills/` containing symlinks to skills from
* managed skill entries (preferred) or the repo's static `skills/` directory
* (backward compatibility fallback), so `--add-dir` makes Claude Code discover
* them as proper registered skills.
*/
export async function buildSkillsDir(
managedSkills: ManagedSkillRef[],
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>,
): Promise<string> {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "agent-company-skills-"));
const target = path.join(tmp, ".claude", "skills");
await fs.mkdir(target, { recursive: true });
if (managedSkills.length > 0) {
await createManagedSkillSymlinks(managedSkills, target, onLog);
return tmp;
}
const skillsDir = await resolveSkillsDir(SKILLS_CANDIDATES);
if (!skillsDir) return tmp;
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
await fs.symlink(
path.join(skillsDir, entry.name),
path.join(target, entry.name),
);
}
}
return tmp;
}
export interface ClaudeExecutionInput {
runId: string;
agent: AdapterExecutionContext["agent"];
config: Record<string, unknown>;
context: Record<string, unknown>;
authToken?: string;
}
export interface ClaudeRuntimeConfig {
command: string;
cwd: string;
workspaceId: string | null;
workspaceRepoUrl: string | null;
workspaceRepoRef: string | null;
env: Record<string, string>;
timeoutSec: number;
graceSec: number;
extraArgs: string[];
}
export function buildLoginResult(input: {
proc: RunProcessResult;
loginUrl: string | null;
}) {
return {
exitCode: input.proc.exitCode,
signal: input.proc.signal,
timedOut: input.proc.timedOut,
stdout: input.proc.stdout,
stderr: input.proc.stderr,
loginUrl: input.loginUrl,
};
}
export function resolveClaudeBillingType(env: Record<string, string>): "api" | "subscription" {
return hasNonEmptyEnvValue(env, "ANTHROPIC_API_KEY") ? "api" : "subscription";
}
export async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<ClaudeRuntimeConfig> {
const { runId, agent, config, context, authToken } = input;
const command = asString(config.command, "claude");
const { env, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef } = await buildCommonAgentCompanyEnv(
{ runId, agent, config, context, authToken },
{ ensureAbsoluteDirectory },
);
const runtimeEnv = ensurePathInEnv(env);
await ensureCommandResolvable(command, cwd, runtimeEnv);
const timeoutSec = asNumber(config.timeoutSec, 0);
const graceSec = asNumber(config.graceSec, 20);
const extraArgs = parseExtraArgs(config);
return {
command,
cwd,
workspaceId,
workspaceRepoUrl,
workspaceRepoRef,
env,
timeoutSec,
graceSec,
extraArgs,
};
}
@@ -1,336 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils";
import type { RunProcessResult } from "@agentcompany/adapter-utils/server-utils";
import {
asString,
asNumber,
asBoolean,
parseObject,
parseJson,
redactEnvForLogs,
filterConflictingArgs,
renderTemplate,
runChildProcess,
} from "@agentcompany/adapter-utils/server-utils";
import {
parseClaudeStreamJson,
describeClaudeFailure,
detectClaudeLoginRequired,
isClaudeMaxTurnsResult,
isClaudeUnknownSessionError,
} from "./parse.js";
import {
buildSkillsDir,
buildLoginResult,
resolveClaudeBillingType,
buildClaudeRuntimeConfig,
parseManagedSkills,
resolveSessionResumeState,
} from "./execute-helpers.js";
export async function runClaudeLogin(input: {
runId: string;
agent: AdapterExecutionContext["agent"];
config: Record<string, unknown>;
context?: Record<string, unknown>;
authToken?: string;
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
}) {
const onLog = input.onLog ?? (async () => {});
const runtime = await buildClaudeRuntimeConfig({
runId: input.runId,
agent: input.agent,
config: input.config,
context: input.context ?? {},
authToken: input.authToken,
});
const proc = await runChildProcess(input.runId, runtime.command, ["login"], {
cwd: runtime.cwd,
env: runtime.env,
timeoutSec: runtime.timeoutSec,
graceSec: runtime.graceSec,
onLog,
});
const loginMeta = detectClaudeLoginRequired({
parsed: null,
stdout: proc.stdout,
stderr: proc.stderr,
});
return buildLoginResult({
proc,
loginUrl: loginMeta.loginUrl,
});
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
"{{context.taskTitle}}\n\n{{context.taskDescription}}",
);
const model = asString(config.model, "");
const effort = asString(config.effort, "");
const chrome = asBoolean(config.chrome, false);
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
const commandNotes = instructionsFilePath
? [
`Injected agent instructions via --append-system-prompt-file ${instructionsFilePath} (with path directive appended)`,
]
: [];
const runtimeConfig = await buildClaudeRuntimeConfig({
runId,
agent,
config,
context,
authToken,
});
const {
command,
cwd,
workspaceId,
workspaceRepoUrl,
workspaceRepoRef,
env,
timeoutSec,
graceSec,
extraArgs,
} = runtimeConfig;
const billingType = resolveClaudeBillingType(env);
const managedSkills = parseManagedSkills(config);
const skillsDir = await buildSkillsDir(managedSkills, onLog);
// When instructionsFilePath is configured, create a combined temp file that
// includes both the file content and the path directive, so we only need
// --append-system-prompt-file (Claude CLI forbids using both flags together).
let effectiveInstructionsFilePath = instructionsFilePath;
if (instructionsFilePath) {
const instructionsContent = await fs.readFile(instructionsFilePath, "utf-8");
const pathDirective = `\nThe above agent instructions were loaded from ${instructionsFilePath}. Resolve any relative file references from ${instructionsFileDir}.`;
const combinedPath = path.join(skillsDir, "agent-instructions.md");
await fs.writeFile(combinedPath, instructionsContent + pathDirective, "utf-8");
effectiveInstructionsFilePath = combinedPath;
}
const sessionResume = resolveSessionResumeState(runtime, cwd);
const { sessionId, runtimeSessionId, runtimeSessionCwd, canResumeSession } = sessionResume;
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
`[agent-company] Claude session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
const prompt = renderTemplate(promptTemplate, {
agentId: agent.id,
companyId: agent.companyId,
runId,
company: { id: agent.companyId },
agent,
run: { id: runId, source: "on_demand" },
context,
});
const buildClaudeArgs = (resumeSessionId: string | null) => {
const args = ["--print", "-", "--output-format", "stream-json", "--verbose"];
if (resumeSessionId) args.push("--resume", resumeSessionId);
if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
if (chrome) args.push("--chrome");
if (model) args.push("--model", model);
if (effort) args.push("--effort", effort);
if (maxTurns > 0) args.push("--max-turns", String(maxTurns));
if (effectiveInstructionsFilePath) {
args.push("--append-system-prompt-file", effectiveInstructionsFilePath);
}
args.push("--add-dir", skillsDir);
if (extraArgs.length > 0) {
// Filter out flags the adapter already manages to prevent duplicates
// (e.g. user shell aliases that inject --dangerously-skip-permissions)
const managedFlags = [
"--dangerously-skip-permissions",
"--output-format",
"--model",
"--effort",
"--max-turns",
"--add-dir",
"--append-system-prompt-file",
"--teammate-mode",
];
const cleaned = filterConflictingArgs(extraArgs, managedFlags);
if (cleaned.length > 0) args.push(...cleaned);
}
return args;
};
const parseFallbackErrorMessage = (proc: RunProcessResult) => {
const stderrLine =
proc.stderr
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean) ?? "";
if ((proc.exitCode ?? 0) === 0) {
return "Failed to parse claude JSON output";
}
return stderrLine
? `Claude exited with code ${proc.exitCode ?? -1}: ${stderrLine}`
: `Claude exited with code ${proc.exitCode ?? -1}`;
};
const runAttempt = async (resumeSessionId: string | null) => {
const args = buildClaudeArgs(resumeSessionId);
if (onMeta) {
await onMeta({
adapterType: "claude_local",
command,
cwd,
commandArgs: args,
commandNotes,
env: redactEnvForLogs(env),
prompt,
context,
});
}
const proc = await runChildProcess(runId, command, args, {
cwd,
env,
stdin: prompt,
timeoutSec,
graceSec,
onLog,
});
const parsedStream = parseClaudeStreamJson(proc.stdout);
const parsed = parsedStream.resultJson ?? parseJson(proc.stdout);
return { proc, parsedStream, parsed };
};
const toAdapterResult = (
attempt: {
proc: RunProcessResult;
parsedStream: ReturnType<typeof parseClaudeStreamJson>;
parsed: Record<string, unknown> | null;
},
opts: { fallbackSessionId: string | null; clearSessionOnMissingSession?: boolean },
): AdapterExecutionResult => {
const { proc, parsedStream, parsed } = attempt;
const loginMeta = detectClaudeLoginRequired({
parsed,
stdout: proc.stdout,
stderr: proc.stderr,
});
const errorMeta =
loginMeta.loginUrl != null
? {
loginUrl: loginMeta.loginUrl,
}
: undefined;
if (proc.timedOut) {
return {
exitCode: proc.exitCode,
signal: proc.signal,
timedOut: true,
errorMessage: `Timed out after ${timeoutSec}s`,
errorCode: "timeout",
errorMeta,
clearSession: Boolean(opts.clearSessionOnMissingSession),
};
}
if (!parsed) {
return {
exitCode: proc.exitCode,
signal: proc.signal,
timedOut: false,
errorMessage: parseFallbackErrorMessage(proc),
errorCode: loginMeta.requiresLogin ? "claude_auth_required" : null,
errorMeta,
resultJson: {
stdout: proc.stdout,
stderr: proc.stderr,
},
clearSession: Boolean(opts.clearSessionOnMissingSession),
};
}
const usage =
parsedStream.usage ??
(() => {
const usageObj = parseObject(parsed.usage);
return {
inputTokens: asNumber(usageObj.input_tokens, 0),
cachedInputTokens: asNumber(usageObj.cache_read_input_tokens, 0),
outputTokens: asNumber(usageObj.output_tokens, 0),
};
})();
const resolvedSessionId =
parsedStream.sessionId ??
(asString(parsed.session_id, opts.fallbackSessionId ?? "") || opts.fallbackSessionId);
const resolvedSessionParams = resolvedSessionId
? ({
sessionId: resolvedSessionId,
cwd,
...(workspaceId ? { workspaceId } : {}),
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
} as Record<string, unknown>)
: null;
const clearSessionForMaxTurns = isClaudeMaxTurnsResult(parsed);
return {
exitCode: proc.exitCode,
signal: proc.signal,
timedOut: false,
errorMessage:
(proc.exitCode ?? 0) === 0
? null
: describeClaudeFailure(parsed) ?? `Claude exited with code ${proc.exitCode ?? -1}`,
errorCode: loginMeta.requiresLogin ? "claude_auth_required" : null,
errorMeta,
usage,
sessionId: resolvedSessionId,
sessionParams: resolvedSessionParams,
sessionDisplayId: resolvedSessionId,
provider: "anthropic",
model: parsedStream.model || asString(parsed.model, model),
billingType,
costUsd: parsedStream.costUsd ?? asNumber(parsed.total_cost_usd, 0),
resultJson: parsed,
summary: parsedStream.summary || asString(parsed.result, ""),
clearSession: clearSessionForMaxTurns || Boolean(opts.clearSessionOnMissingSession && !resolvedSessionId),
};
};
try {
const initial = await runAttempt(sessionId ?? null);
if (
sessionId &&
!initial.proc.timedOut &&
(initial.proc.exitCode ?? 0) !== 0 &&
initial.parsed &&
isClaudeUnknownSessionError(initial.parsed)
) {
await onLog(
"stderr",
`[agent-company] Claude resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
);
const retry = await runAttempt(null);
return toAdapterResult(retry, { fallbackSessionId: null, clearSessionOnMissingSession: true });
}
return toAdapterResult(initial, { fallbackSessionId: runtimeSessionId || runtime.sessionId });
} finally {
fs.rm(skillsDir, { recursive: true, force: true }).catch(() => {});
}
}
@@ -1,12 +0,0 @@
export { execute, runClaudeLogin } from "./execute.js";
export { testEnvironment } from "./test.js";
export {
parseClaudeStreamJson,
describeClaudeFailure,
isClaudeMaxTurnsResult,
isClaudeUnknownSessionError,
} from "./parse.js";
export { listClaudeSkills, syncClaudeSkills } from "./skills.js";
import { createStandardSessionCodec } from "@agentcompany/adapter-utils/session-codec";
export const sessionCodec = createStandardSessionCodec();
@@ -1,179 +0,0 @@
import type { UsageSummary } from "@agentcompany/adapter-utils";
import { asString, asNumber, parseObject, parseJson } from "@agentcompany/adapter-utils/server-utils";
const CLAUDE_AUTH_REQUIRED_RE = /(?:not\s+logged\s+in|please\s+log\s+in|please\s+run\s+`?claude\s+login`?|login\s+required|requires\s+login|unauthorized|authentication\s+required)/i;
const URL_RE = /(https?:\/\/[^\s'"`<>()[\]{};,!?]+[^\s'"`<>()[\]{};,!.?:]+)/gi;
export function parseClaudeStreamJson(stdout: string) {
let sessionId: string | null = null;
let model = "";
let finalResult: Record<string, unknown> | null = null;
const assistantTexts: string[] = [];
for (const rawLine of stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
const event = parseJson(line);
if (!event) continue;
const type = asString(event.type, "");
if (type === "system" && asString(event.subtype, "") === "init") {
sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
model = asString(event.model, model);
continue;
}
if (type === "assistant") {
sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
const message = parseObject(event.message);
const content = Array.isArray(message.content) ? message.content : [];
for (const entry of content) {
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
const block = entry as Record<string, unknown>;
if (asString(block.type, "") === "text") {
const text = asString(block.text, "");
if (text) assistantTexts.push(text);
}
}
continue;
}
if (type === "result") {
finalResult = event;
sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
}
}
if (!finalResult) {
return {
sessionId,
model,
costUsd: null as number | null,
usage: null as UsageSummary | null,
summary: assistantTexts.join("\n\n").trim(),
resultJson: null as Record<string, unknown> | null,
};
}
const usageObj = parseObject(finalResult.usage);
const usage: UsageSummary = {
inputTokens: asNumber(usageObj.input_tokens, 0),
cachedInputTokens: asNumber(usageObj.cache_read_input_tokens, 0),
outputTokens: asNumber(usageObj.output_tokens, 0),
};
const costRaw = finalResult.total_cost_usd;
const costUsd = typeof costRaw === "number" && Number.isFinite(costRaw) ? costRaw : null;
const summary = asString(finalResult.result, assistantTexts.join("\n\n")).trim();
return {
sessionId,
model,
costUsd,
usage,
summary,
resultJson: finalResult,
};
}
function extractClaudeErrorMessages(parsed: Record<string, unknown>): string[] {
const raw = Array.isArray(parsed.errors) ? parsed.errors : [];
const messages: string[] = [];
for (const entry of raw) {
if (typeof entry === "string") {
const msg = entry.trim();
if (msg) messages.push(msg);
continue;
}
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
continue;
}
const obj = entry as Record<string, unknown>;
const msg = asString(obj.message, "") || asString(obj.error, "") || asString(obj.code, "");
if (msg) {
messages.push(msg);
continue;
}
try {
messages.push(JSON.stringify(obj));
} catch {
// skip non-serializable entry
}
}
return messages;
}
export function extractClaudeLoginUrl(text: string): string | null {
const match = text.match(URL_RE);
if (!match || match.length === 0) return null;
for (const rawUrl of match) {
const cleaned = rawUrl.replace(/[\])}.!,?;:'"]+$/g, "");
if (cleaned.includes("claude") || cleaned.includes("anthropic") || cleaned.includes("auth")) {
return cleaned;
}
}
return match[0]?.replace(/[\])}.!,?;:'"]+$/g, "") ?? null;
}
export function detectClaudeLoginRequired(input: {
parsed: Record<string, unknown> | null;
stdout: string;
stderr: string;
}): { requiresLogin: boolean; loginUrl: string | null } {
const resultText = asString(input.parsed?.result, "").trim();
const messages = [resultText, ...extractClaudeErrorMessages(input.parsed ?? {}), input.stdout, input.stderr]
.join("\n")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const requiresLogin = messages.some((line) => CLAUDE_AUTH_REQUIRED_RE.test(line));
return {
requiresLogin,
loginUrl: extractClaudeLoginUrl([input.stdout, input.stderr].join("\n")),
};
}
export function describeClaudeFailure(parsed: Record<string, unknown>): string | null {
const subtype = asString(parsed.subtype, "");
const resultText = asString(parsed.result, "").trim();
const errors = extractClaudeErrorMessages(parsed);
let detail = resultText;
if (!detail && errors.length > 0) {
detail = errors[0] ?? "";
}
const parts = ["Claude run failed"];
if (subtype) parts.push(`subtype=${subtype}`);
if (detail) parts.push(detail);
return parts.length > 1 ? parts.join(": ") : null;
}
export function isClaudeMaxTurnsResult(parsed: Record<string, unknown> | null | undefined): boolean {
if (!parsed) return false;
const subtype = asString(parsed.subtype, "").trim().toLowerCase();
if (subtype === "error_max_turns") return true;
const stopReason = asString(parsed.stop_reason, "").trim().toLowerCase();
if (stopReason === "max_turns") return true;
const resultText = asString(parsed.result, "").trim();
return /max(?:imum)?\s+turns?/i.test(resultText);
}
export function isClaudeUnknownSessionError(parsed: Record<string, unknown>): boolean {
const resultText = asString(parsed.result, "").trim();
const allMessages = [resultText, ...extractClaudeErrorMessages(parsed)]
.map((msg) => msg.trim())
.filter(Boolean);
return allMessages.some((msg) =>
/no conversation found with session id|unknown session|session .* not found/i.test(msg),
);
}
@@ -1,131 +0,0 @@
// ---------------------------------------------------------------------------
// Claude adapter skill listing / sync -- ephemeral mode
// Skills are mounted into a temp dir via --add-dir at run time, not persisted.
// ---------------------------------------------------------------------------
import path from "node:path";
import type {
AdapterSkillContext,
AdapterSkillEntry,
AdapterSkillSnapshot,
} from "@agentcompany/adapter-utils";
import {
parseObject,
asString,
readAgentCompanyRuntimeSkillEntries,
readInstalledSkillTargets,
resolveAgentCompanyDesiredSkillNames,
} from "@agentcompany/adapter-utils/server-utils";
function resolveClaudeSkillsHome(config: Record<string, unknown>): string {
const env = parseObject(config.env);
const home = asString(env.HOME, asString(env.USERPROFILE, ""));
const base = home || (process.env.HOME ?? process.env.USERPROFILE ?? "");
return path.join(base, ".claude", "skills");
}
async function buildClaudeSkillSnapshot(
config: Record<string, unknown>,
): Promise<AdapterSkillSnapshot> {
const moduleDir = path.resolve(import.meta.dirname ?? __dirname);
const availableEntries = await readAgentCompanyRuntimeSkillEntries(config, moduleDir);
const desiredSkills = resolveAgentCompanyDesiredSkillNames(config, availableEntries);
const desiredSet = new Set(desiredSkills);
const entries: AdapterSkillEntry[] = [];
const warnings: string[] = [];
for (const available of availableEntries) {
const desired = desiredSet.has(available.key);
entries.push({
key: available.key,
runtimeName: available.runtimeName,
desired,
managed: true,
state: desired ? "configured" : "available",
sourcePath: available.source,
targetPath: null,
detail: desired
? "Will be mounted into the ephemeral Claude skill directory on the next run."
: null,
required: Boolean(available.required),
requiredReason: available.requiredReason ?? null,
origin: available.required ? "paperclip_required" : "company_managed",
originLabel: available.required ? "Required by AgentCompany" : "Managed by AgentCompany",
readOnly: false,
});
}
// Scan installed Claude skills for external entries
const skillsHome = resolveClaudeSkillsHome(config);
const installed = await readInstalledSkillTargets(skillsHome);
for (const [name, installedEntry] of installed.entries()) {
if (availableEntries.some((entry) => entry.runtimeName === name)) continue;
entries.push({
key: name,
runtimeName: name,
desired: false,
managed: false,
state: "external",
origin: "user_installed",
originLabel: "User-installed",
locationLabel: "~/.claude/skills",
readOnly: true,
sourcePath: null,
targetPath: installedEntry.targetPath ?? path.join(skillsHome, name),
detail: "Installed outside AgentCompany management.",
});
}
// Check for desired skills not in available
for (const desiredSkill of desiredSkills) {
if (availableEntries.some((entry) => entry.key === desiredSkill)) continue;
if (entries.some((entry) => entry.key === desiredSkill)) continue;
warnings.push(`Desired skill "${desiredSkill}" is not available from the AgentCompany skills directory.`);
entries.push({
key: desiredSkill,
runtimeName: null,
desired: true,
managed: true,
state: "missing",
sourcePath: null,
targetPath: null,
detail: "AgentCompany cannot find this skill in the local runtime skills directory.",
origin: "external_unknown",
originLabel: "External or unavailable",
readOnly: false,
});
}
entries.sort((left, right) => left.key.localeCompare(right.key));
return {
adapterType: "claude_local",
supported: true,
mode: "ephemeral",
desiredSkills,
entries,
warnings,
};
}
/**
* List the current skill state for the Claude adapter.
* Ephemeral mode: skills are injected at run time via --add-dir, not persisted.
*/
export async function listClaudeSkills(
ctx: AdapterSkillContext,
): Promise<AdapterSkillSnapshot> {
return buildClaudeSkillSnapshot(ctx.config);
}
/**
* Sync skills for the Claude adapter.
* Ephemeral mode: no on-disk sync needed; skills are injected at run time.
* Returns the current snapshot for informational purposes.
*/
export async function syncClaudeSkills(
ctx: AdapterSkillContext,
_desiredSkills: string[],
): Promise<AdapterSkillSnapshot> {
return buildClaudeSkillSnapshot(ctx.config);
}
@@ -1,124 +0,0 @@
import type {
AdapterEnvironmentCheck,
AdapterEnvironmentTestContext,
AdapterEnvironmentTestResult,
} from "@agentcompany/adapter-utils";
import {
asString,
asBoolean,
asNumber,
asStringArray,
parseObject,
ensureAbsoluteDirectory,
ensureCommandResolvable,
ensurePathInEnv,
runChildProcess,
} from "@agentcompany/adapter-utils/server-utils";
import { summarizeStatus, isNonEmpty, commandLooksLike, summarizeProbeDetail } from "@agentcompany/adapter-utils/test-utils";
import { detectClaudeLoginRequired, parseClaudeStreamJson } from "./parse.js";
export async function testEnvironment(
ctx: AdapterEnvironmentTestContext,
): Promise<AdapterEnvironmentTestResult> {
const checks: AdapterEnvironmentCheck[] = [];
const config = parseObject(ctx.config);
const command = asString(config.command, "claude");
const cwd = asString(config.cwd, process.cwd());
try {
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
checks.push({ code: "claude_cwd_valid", level: "info", message: `Working directory is valid: ${cwd}` });
} catch (err) {
checks.push({ code: "claude_cwd_invalid", level: "error", message: err instanceof Error ? err.message : "Invalid working directory", detail: cwd });
}
const envConfig = parseObject(config.env);
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(envConfig)) {
if (typeof value === "string") env[key] = value;
}
const runtimeEnv = ensurePathInEnv(env);
try {
await ensureCommandResolvable(command, cwd, runtimeEnv);
checks.push({ code: "claude_command_resolvable", level: "info", message: `Command is executable: ${command}` });
} catch (err) {
checks.push({ code: "claude_command_unresolvable", level: "error", message: err instanceof Error ? err.message : "Command is not executable", detail: command });
}
const configApiKey = env.ANTHROPIC_API_KEY;
if (isNonEmpty(configApiKey)) {
checks.push({ code: "claude_anthropic_api_key_overrides_subscription", level: "info", message: "ANTHROPIC_API_KEY is set. Claude will use API-key auth." });
} else {
checks.push({ code: "claude_subscription_mode_possible", level: "info", message: "ANTHROPIC_API_KEY is not set; subscription-based auth can be used if Claude is logged in." });
}
// Hello probe: wrapped in try-catch so probe failures never crash the test.
// If the command is resolvable, that is sufficient for "pass" status.
const canRunProbe = checks.every((c) => c.code !== "claude_cwd_invalid" && c.code !== "claude_command_unresolvable");
if (canRunProbe && commandLooksLike(command, "claude")) {
try {
const model = asString(config.model, "").trim();
const effort = asString(config.effort, "").trim();
const chrome = asBoolean(config.chrome, false);
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
const extraArgs = (() => {
const fromExtraArgs = asStringArray(config.extraArgs);
if (fromExtraArgs.length > 0) return fromExtraArgs;
return asStringArray(config.args);
})();
const args = ["--print", "-", "--output-format", "stream-json", "--verbose"];
if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
if (chrome) args.push("--chrome");
if (model) args.push("--model", model);
if (effort) args.push("--effort", effort);
if (maxTurns > 0) args.push("--max-turns", String(maxTurns));
if (extraArgs.length > 0) args.push(...extraArgs);
const probe = await runChildProcess(
`claude-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
command,
args,
{ cwd, env, timeoutSec: 45, graceSec: 5, stdin: "Respond with hello.", onLog: async () => {} },
);
const parsedStream = parseClaudeStreamJson(probe.stdout);
const parsed = parsedStream.resultJson;
const loginMeta = detectClaudeLoginRequired({ parsed, stdout: probe.stdout, stderr: probe.stderr });
const detail = summarizeProbeDetail(probe.stdout, probe.stderr);
if (probe.timedOut) {
checks.push({ code: "claude_hello_probe_timed_out", level: "info", message: "Claude hello probe timed out." });
} else if (loginMeta.requiresLogin) {
checks.push({
code: "claude_hello_probe_auth_required", level: "warn",
message: "Claude CLI is installed, but login is required.",
...(detail ? { detail } : {}),
hint: loginMeta.loginUrl ? `Run \`claude login\` at ${loginMeta.loginUrl}` : "Run `claude login` first.",
});
} else if ((probe.exitCode ?? 1) === 0) {
const summary = parsedStream.summary.trim();
const hasHello = /\bhello\b/i.test(summary);
checks.push({
code: hasHello ? "claude_hello_probe_passed" : "claude_hello_probe_unexpected_output",
level: "info",
message: hasHello ? "Claude hello probe succeeded." : "Claude probe completed.",
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
});
} else {
checks.push({ code: "claude_hello_probe_failed", level: "info", message: "Claude hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
}
} catch {
// Probe failed to run — command is still resolvable so this is non-fatal.
checks.push({ code: "claude_hello_probe_error", level: "info", message: "Claude hello probe could not run." });
}
}
return {
adapterType: ctx.adapterType,
status: summarizeStatus(checks),
checks,
testedAt: new Date().toISOString(),
};
}
@@ -1,39 +0,0 @@
import type { CreateConfigValues } from "@agentcompany/adapter-utils";
import { parseCommaArgs, parseEnvVars, parseEnvBindings, parseJsonObject } from "@agentcompany/adapter-utils/config-utils";
export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
if (v.cwd) ac.cwd = v.cwd;
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
if (v.model) ac.model = v.model;
if (v.thinkingEffort) ac.effort = v.thinkingEffort;
if (v.chrome) ac.chrome = true;
ac.timeoutSec = 0;
ac.graceSec = 15;
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) ac.env = env;
ac.maxTurnsPerRun = v.maxTurnsPerRun;
ac.dangerouslySkipPermissions = v.dangerouslySkipPermissions;
if (v.workspaceStrategyType === "git_worktree") {
ac.workspaceStrategy = {
type: "git_worktree",
...(v.workspaceBaseRef ? { baseRef: v.workspaceBaseRef } : {}),
...(v.workspaceBranchTemplate ? { branchTemplate: v.workspaceBranchTemplate } : {}),
...(v.worktreeParentDir ? { worktreeParentDir: v.worktreeParentDir } : {}),
};
}
const runtimeServices = parseJsonObject(v.runtimeServicesJson ?? "");
if (runtimeServices && Array.isArray(runtimeServices.services)) {
ac.workspaceRuntime = runtimeServices;
}
if (v.command) ac.command = v.command;
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
return ac;
}
@@ -1,2 +0,0 @@
export { parseClaudeStdoutLine } from "./parse-stdout.js";
export { buildClaudeLocalConfig } from "./build-config.js";
@@ -1,115 +0,0 @@
import type { TranscriptEntry } from "@agentcompany/adapter-utils";
import { asRecord, safeJsonParse, errorText } from "@agentcompany/adapter-utils/parse-utils";
function asNumber(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
export function parseClaudeStdoutLine(line: string, ts: string): TranscriptEntry[] {
const parsed = asRecord(safeJsonParse(line));
if (!parsed) {
return [{ kind: "stdout", ts, text: line }];
}
const type = typeof parsed.type === "string" ? parsed.type : "";
if (type === "system" && parsed.subtype === "init") {
return [
{
kind: "init",
ts,
model: typeof parsed.model === "string" ? parsed.model : "unknown",
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : "",
},
];
}
if (type === "assistant") {
const message = asRecord(parsed.message) ?? {};
const content = Array.isArray(message.content) ? message.content : [];
const entries: TranscriptEntry[] = [];
for (const blockRaw of content) {
const block = asRecord(blockRaw);
if (!block) continue;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text) entries.push({ kind: "assistant", ts, text });
} else if (blockType === "thinking") {
const text = typeof block.thinking === "string" ? block.thinking : "";
if (text) entries.push({ kind: "thinking", ts, text });
} else if (blockType === "tool_use") {
entries.push({
kind: "tool_call",
ts,
name: typeof block.name === "string" ? block.name : "unknown",
toolUseId:
typeof block.id === "string"
? block.id
: typeof block.tool_use_id === "string"
? block.tool_use_id
: undefined,
input: block.input ?? {},
});
}
}
return entries.length > 0 ? entries : [{ kind: "stdout", ts, text: line }];
}
if (type === "user") {
const message = asRecord(parsed.message) ?? {};
const content = Array.isArray(message.content) ? message.content : [];
const entries: TranscriptEntry[] = [];
for (const blockRaw of content) {
const block = asRecord(blockRaw);
if (!block) continue;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text) entries.push({ kind: "user", ts, text });
} else if (blockType === "tool_result") {
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
const isError = block.is_error === true;
let text = "";
if (typeof block.content === "string") {
text = block.content;
} else if (Array.isArray(block.content)) {
const parts: string[] = [];
for (const part of block.content) {
const p = asRecord(part);
if (p && typeof p.text === "string") parts.push(p.text);
}
text = parts.join("\n");
}
entries.push({ kind: "tool_result", ts, toolUseId, content: text, isError });
}
}
if (entries.length > 0) return entries;
// fall through to stdout for user messages without recognized blocks
}
if (type === "result") {
const usage = asRecord(parsed.usage) ?? {};
const inputTokens = asNumber(usage.input_tokens);
const outputTokens = asNumber(usage.output_tokens);
const cachedTokens = asNumber(usage.cache_read_input_tokens);
const costUsd = asNumber(parsed.total_cost_usd);
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
const isError = parsed.is_error === true;
const errors = Array.isArray(parsed.errors) ? parsed.errors.map(errorText).filter(Boolean) : [];
const text = typeof parsed.result === "string" ? parsed.result : "";
return [{
kind: "result",
ts,
text,
inputTokens,
outputTokens,
cachedTokens,
costUsd,
subtype,
isError,
errors,
}];
}
return [{ kind: "stdout", ts, text: line }];
}
@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
-51
View File
@@ -1,51 +0,0 @@
{
"name": "@agentcompany/adapter-codex-local",
"version": "0.3.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",
"skills"
],
"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"
}
}
@@ -1,202 +0,0 @@
import pc from "picocolors";
import { asRecord, errorText, asString, asNumber } from "@agentcompany/adapter-utils/parse-utils";
function printItemStarted(item: Record<string, unknown>): boolean {
const itemType = asString(item.type);
if (itemType === "command_execution") {
const command = asString(item.command);
console.log(pc.yellow("tool_call: command_execution"));
if (command) console.log(pc.gray(command));
return true;
}
if (itemType === "tool_use") {
const name = asString(item.name, "unknown");
console.log(pc.yellow(`tool_call: ${name}`));
if (item.input !== undefined) {
try {
console.log(pc.gray(JSON.stringify(item.input, null, 2)));
} catch {
console.log(pc.gray(String(item.input)));
}
}
return true;
}
return false;
}
function printItemCompleted(item: Record<string, unknown>): boolean {
const itemType = asString(item.type);
if (itemType === "agent_message") {
const text = asString(item.text);
if (text) console.log(pc.green(`assistant: ${text}`));
return true;
}
if (itemType === "reasoning") {
const text = asString(item.text);
if (text) console.log(pc.gray(`thinking: ${text}`));
return true;
}
if (itemType === "tool_use") {
const name = asString(item.name, "unknown");
console.log(pc.yellow(`tool_call: ${name}`));
if (item.input !== undefined) {
try {
console.log(pc.gray(JSON.stringify(item.input, null, 2)));
} catch {
console.log(pc.gray(String(item.input)));
}
}
return true;
}
if (itemType === "command_execution") {
const command = asString(item.command);
const status = asString(item.status);
const exitCode = typeof item.exit_code === "number" && Number.isFinite(item.exit_code) ? item.exit_code : null;
const output = asString(item.aggregated_output).replace(/\s+$/, "");
const isError =
(exitCode !== null && exitCode !== 0) ||
status === "failed" ||
status === "errored" ||
status === "error" ||
status === "cancelled";
const summaryParts = [
"tool_result: command_execution",
command ? `command="${command}"` : "",
status ? `status=${status}` : "",
exitCode !== null ? `exit_code=${exitCode}` : "",
].filter(Boolean);
console.log((isError ? pc.red : pc.cyan)(summaryParts.join(" ")));
if (output) console.log((isError ? pc.red : pc.gray)(output));
return true;
}
if (itemType === "file_change") {
const changes = Array.isArray(item.changes) ? item.changes : [];
const entries = changes
.map((changeRaw) => asRecord(changeRaw))
.filter((change): change is Record<string, unknown> => Boolean(change))
.map((change) => {
const kind = asString(change.kind, "update");
const path = asString(change.path, "unknown");
return `${kind} ${path}`;
});
const preview = entries.length > 0 ? entries.slice(0, 6).join(", ") : "none";
const more = entries.length > 6 ? ` (+${entries.length - 6} more)` : "";
console.log(pc.cyan(`file_change: ${preview}${more}`));
return true;
}
if (itemType === "error") {
const message = errorText(item.message ?? item.error ?? item);
if (message) console.log(pc.red(`error: ${message}`));
return true;
}
if (itemType === "tool_result") {
const isError = item.is_error === true || asString(item.status) === "error";
const text = asString(item.content) || asString(item.result) || asString(item.output);
console.log((isError ? pc.red : pc.cyan)(`tool_result${isError ? " (error)" : ""}`));
if (text) console.log((isError ? pc.red : pc.gray)(text));
return true;
}
return false;
}
export function printCodexStreamEvent(raw: string, debug: boolean): void {
const line = raw.trim();
if (!line) return;
let parsed: Record<string, unknown> | null = null;
try {
parsed = JSON.parse(line) as Record<string, unknown>;
} catch {
console.log(line);
return;
}
const type = asString(parsed.type);
if (type === "thread.started") {
const threadId = asString(parsed.thread_id);
const model = asString(parsed.model);
const details = [threadId ? `session: ${threadId}` : "", model ? `model: ${model}` : ""].filter(Boolean).join(", ");
console.log(pc.blue(`Codex thread started${details ? ` (${details})` : ""}`));
return;
}
if (type === "turn.started") {
console.log(pc.blue("turn started"));
return;
}
if (type === "item.started" || type === "item.completed") {
const item = asRecord(parsed.item);
if (item) {
const handled =
type === "item.started"
? printItemStarted(item)
: printItemCompleted(item);
if (!handled) {
const itemType = asString(item.type, "unknown");
const id = asString(item.id);
const status = asString(item.status);
const meta = [id ? `id=${id}` : "", status ? `status=${status}` : ""].filter(Boolean).join(" ");
console.log(pc.gray(`${type}: ${itemType}${meta ? ` (${meta})` : ""}`));
}
} else {
console.log(pc.gray(type));
}
return;
}
if (type === "turn.completed") {
const usage = asRecord(parsed.usage);
const input = asNumber(usage?.input_tokens);
const output = asNumber(usage?.output_tokens);
const cached = asNumber(usage?.cached_input_tokens, asNumber(usage?.cache_read_input_tokens));
const cost = asNumber(parsed.total_cost_usd);
const isError = parsed.is_error === true;
const subtype = asString(parsed.subtype);
const errors = Array.isArray(parsed.errors) ? parsed.errors.map(errorText).filter(Boolean) : [];
console.log(
pc.blue(`tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`),
);
if (subtype || isError || errors.length > 0) {
console.log(
pc.red(`result: subtype=${subtype || "unknown"} is_error=${isError ? "true" : "false"}`),
);
if (errors.length > 0) console.log(pc.red(`errors: ${errors.join(" | ")}`));
}
return;
}
if (type === "turn.failed") {
const usage = asRecord(parsed.usage);
const input = asNumber(usage?.input_tokens);
const output = asNumber(usage?.output_tokens);
const cached = asNumber(usage?.cached_input_tokens, asNumber(usage?.cache_read_input_tokens));
const message = errorText(parsed.error ?? parsed.message);
console.log(pc.red(`turn failed${message ? `: ${message}` : ""}`));
console.log(pc.blue(`tokens: in=${input} out=${output} cached=${cached}`));
return;
}
if (type === "error") {
const message = errorText(parsed.message ?? parsed.error ?? parsed);
if (message) console.log(pc.red(`error: ${message}`));
return;
}
if (debug) {
console.log(pc.gray(line));
}
}
@@ -1 +0,0 @@
export { printCodexStreamEvent } from "./format-event.js";
-46
View File
@@ -1,46 +0,0 @@
export const type = "codex_local";
export const label = "Codex (local)";
export const DEFAULT_CODEX_LOCAL_MODEL = "gpt-5.3-codex";
export const DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX = true;
export const models = [
{ id: "gpt-5.4", label: "gpt-5.4" },
{ id: DEFAULT_CODEX_LOCAL_MODEL, label: DEFAULT_CODEX_LOCAL_MODEL },
{ id: "gpt-5.3-codex-spark", label: "gpt-5.3-codex-spark" },
{ id: "gpt-5", label: "gpt-5" },
{ id: "o3", label: "o3" },
{ id: "o4-mini", label: "o4-mini" },
{ id: "gpt-5-mini", label: "gpt-5-mini" },
{ id: "gpt-5-nano", label: "gpt-5-nano" },
{ id: "o3-mini", label: "o3-mini" },
{ id: "codex-mini-latest", label: "Codex Mini" },
];
export const agentConfigurationDoc = `# codex_local agent configuration
Adapter: codex_local
Core fields:
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to stdin prompt at runtime
- model (string, optional): Codex model id
- modelReasoningEffort (string, optional): reasoning effort override (minimal|low|medium|high) passed via -c model_reasoning_effort=...
- promptTemplate (string, optional): run prompt template
- search (boolean, optional): run codex with --search
- dangerouslyBypassApprovalsAndSandbox (boolean, optional): run with bypass flag
- command (string, optional): defaults to "codex"
- extraArgs (string[], optional): additional CLI args
- env (object, optional): KEY=VALUE environment variables
- workspaceStrategy (object, optional): execution workspace strategy; currently supports { type: "git_worktree", baseRef?, branchTemplate?, worktreeParentDir? }
- workspaceRuntime (object, optional): workspace runtime service intents; local host-managed services are realized before Codex starts and exposed back via context/env
Operational fields:
- timeoutSec (number, optional): run timeout in seconds
- graceSec (number, optional): SIGTERM grace period in seconds
Notes:
- Prompts are piped via stdin (Codex receives "-" prompt argument).
- AgentCompany auto-injects local skills into Codex personal skills dir ("$CODEX_HOME/skills" or "~/.codex/skills") when missing, so Codex can discover "$agentcompany" and related skills.
- Some model/tool combinations reject certain effort levels (for example minimal with web search enabled).
- When AgentCompany realizes a workspace/runtime for a run, it injects AGENT_COMPANY_WORKSPACE_* and AGENT_COMPANY_RUNTIME_* env vars for agent-side tooling.
`;
@@ -1,96 +0,0 @@
import os from "node:os";
import path from "node:path";
import type { AdapterExecutionContext, ManagedSkillRef } from "@agentcompany/adapter-utils";
import {
hasNonEmptyEnvValue,
buildSkillsCandidates,
resolveSkillsDir,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
} from "@agentcompany/adapter-utils";
import {
createManagedSkillSymlinks,
} from "@agentcompany/adapter-utils/server-utils";
import fs from "node:fs/promises";
export {
hasNonEmptyEnvValue,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
};
export const SKILLS_CANDIDATES = buildSkillsCandidates(import.meta.url);
export const CODEX_ROLLOUT_NOISE_RE =
/^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i;
export function stripCodexRolloutNoise(text: string): string {
const parts = text.split(/\r?\n/);
const kept: string[] = [];
for (const part of parts) {
const trimmed = part.trim();
if (!trimmed) {
kept.push(part);
continue;
}
if (CODEX_ROLLOUT_NOISE_RE.test(trimmed)) continue;
kept.push(part);
}
return kept.join("\n");
}
export function resolveCodexBillingType(env: Record<string, string>): "api" | "subscription" {
return hasNonEmptyEnvValue(env, "OPENAI_API_KEY") ? "api" : "subscription";
}
export function codexHomeDir(): string {
const fromEnv = process.env.CODEX_HOME;
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) return fromEnv.trim();
return path.join(os.homedir(), ".codex");
}
export async function resolveAgentCompanySkillsDir(): Promise<string | null> {
return resolveSkillsDir(SKILLS_CANDIDATES);
}
export async function ensureCodexSkillsInjected(
managedSkills: ManagedSkillRef[],
onLog: AdapterExecutionContext["onLog"],
) {
const skillsHome = path.join(codexHomeDir(), "skills");
if (managedSkills.length > 0) {
await createManagedSkillSymlinks(managedSkills, skillsHome, onLog);
return;
}
const skillsDir = await resolveSkillsDir(SKILLS_CANDIDATES);
if (!skillsDir) return;
await fs.mkdir(skillsHome, { recursive: true });
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const source = path.join(skillsDir, entry.name);
const target = path.join(skillsHome, entry.name);
const existing = await fs.lstat(target).catch(() => null);
if (existing) continue;
try {
await fs.symlink(source, target);
await onLog(
"stderr",
`[agent-company] Injected Codex skill "${entry.name}" into ${skillsHome}\n`,
);
} catch (err) {
await onLog(
"stderr",
`[agent-company] Failed to inject Codex skill "${entry.name}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`,
);
}
}
}
@@ -1,258 +0,0 @@
import path from "node:path";
import fs from "node:fs/promises";
import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils";
import {
asString,
asNumber,
asBoolean,
redactEnvForLogs,
ensureAbsoluteDirectory,
ensureCommandResolvable,
ensurePathInEnv,
filterConflictingArgs,
renderTemplate,
runChildProcess,
} from "@agentcompany/adapter-utils/server-utils";
import { firstNonEmptyLine } from "@agentcompany/adapter-utils/test-utils";
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
import {
stripCodexRolloutNoise,
resolveCodexBillingType,
ensureCodexSkillsInjected,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
} from "./execute-helpers.js";
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
"{{context.taskTitle}}\n\n{{context.taskDescription}}",
);
const command = asString(config.command, "codex");
const model = asString(config.model, "");
const modelReasoningEffort = asString(
config.modelReasoningEffort,
asString(config.reasoningEffort, ""),
);
const search = asBoolean(config.search, false);
const bypass = asBoolean(
config.dangerouslyBypassApprovalsAndSandbox,
asBoolean(config.dangerouslyBypassSandbox, false),
);
const { env, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef } =
await buildCommonAgentCompanyEnv(
{ runId, agent, config, context, authToken },
{ ensureAbsoluteDirectory },
);
const managedSkills = parseManagedSkills(config);
await ensureCodexSkillsInjected(managedSkills, onLog);
const billingType = resolveCodexBillingType(env);
const runtimeEnv = ensurePathInEnv(env);
await ensureCommandResolvable(command, cwd, runtimeEnv);
const timeoutSec = asNumber(config.timeoutSec, 0);
const graceSec = asNumber(config.graceSec, 20);
const extraArgs = parseExtraArgs(config);
const { sessionId, runtimeSessionId, runtimeSessionCwd, canResumeSession } =
resolveSessionResumeState(runtime, cwd);
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
`[agent-company] Codex session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
let instructionsPrefix = "";
if (instructionsFilePath) {
try {
const instructionsContents = await fs.readFile(instructionsFilePath, "utf8");
instructionsPrefix =
`${instructionsContents}\n\n` +
`The above agent instructions were loaded from ${instructionsFilePath}. ` +
`Resolve any relative file references from ${instructionsDir}.\n\n`;
await onLog(
"stderr",
`[agent-company] Loaded agent instructions file: ${instructionsFilePath}\n`,
);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
await onLog(
"stderr",
`[agent-company] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`,
);
}
}
const commandNotes = (() => {
if (!instructionsFilePath) return [] as string[];
if (instructionsPrefix.length > 0) {
return [
`Loaded agent instructions from ${instructionsFilePath}`,
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
];
}
return [
`Configured instructionsFilePath ${instructionsFilePath}, but file could not be read; continuing without injected instructions.`,
];
})();
const renderedPrompt = renderTemplate(promptTemplate, {
agentId: agent.id,
companyId: agent.companyId,
runId,
company: { id: agent.companyId },
agent,
run: { id: runId, source: "on_demand" },
context,
});
const prompt = `${instructionsPrefix}${renderedPrompt}`;
const buildArgs = (resumeSessionId: string | null) => {
const args = ["exec", "--json"];
if (search) args.unshift("--search");
if (bypass) args.push("--dangerously-bypass-approvals-and-sandbox");
if (model) args.push("--model", model);
if (modelReasoningEffort) args.push("-c", `model_reasoning_effort=${JSON.stringify(modelReasoningEffort)}`);
if (extraArgs.length > 0) {
const managedFlags = [
"--dangerously-bypass-approvals-and-sandbox",
"--json",
"--model",
"--sandbox",
"--skip-git-repo-check",
"--ask-for-approval",
];
const cleaned = filterConflictingArgs(extraArgs, managedFlags);
if (cleaned.length > 0) args.push(...cleaned);
}
if (resumeSessionId) args.push("resume", resumeSessionId, "-");
else args.push("-");
return args;
};
const runAttempt = async (resumeSessionId: string | null) => {
const args = buildArgs(resumeSessionId);
if (onMeta) {
await onMeta({
adapterType: "codex_local",
command,
cwd,
commandNotes,
commandArgs: args.map((value, idx) => {
if (idx === args.length - 1 && value !== "-") return `<prompt ${prompt.length} chars>`;
return value;
}),
env: redactEnvForLogs(env),
prompt,
context,
});
}
const proc = await runChildProcess(runId, command, args, {
cwd,
env,
stdin: prompt,
timeoutSec,
graceSec,
onLog: async (stream, chunk) => {
if (stream !== "stderr") {
await onLog(stream, chunk);
return;
}
const cleaned = stripCodexRolloutNoise(chunk);
if (!cleaned.trim()) return;
await onLog(stream, cleaned);
},
});
const cleanedStderr = stripCodexRolloutNoise(proc.stderr);
return {
proc: {
...proc,
stderr: cleanedStderr,
},
rawStderr: proc.stderr,
parsed: parseCodexJsonl(proc.stdout),
};
};
const toResult = (
attempt: { proc: { exitCode: number | null; signal: string | null; timedOut: boolean; stdout: string; stderr: string }; rawStderr: string; parsed: ReturnType<typeof parseCodexJsonl> },
clearSessionOnMissingSession = false,
): AdapterExecutionResult => {
if (attempt.proc.timedOut) {
return {
exitCode: attempt.proc.exitCode,
signal: attempt.proc.signal,
timedOut: true,
errorMessage: `Timed out after ${timeoutSec}s`,
clearSession: clearSessionOnMissingSession,
};
}
const resolvedSessionId = attempt.parsed.sessionId ?? runtimeSessionId ?? runtime.sessionId ?? null;
const resolvedSessionParams = resolvedSessionId
? ({
sessionId: resolvedSessionId,
cwd,
...(workspaceId ? { workspaceId } : {}),
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
} as Record<string, unknown>)
: null;
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
const fallbackErrorMessage =
parsedError ||
stderrLine ||
`Codex exited with code ${attempt.proc.exitCode ?? -1}`;
return {
exitCode: attempt.proc.exitCode,
signal: attempt.proc.signal,
timedOut: false,
errorMessage:
(attempt.proc.exitCode ?? 0) === 0
? null
: fallbackErrorMessage,
usage: attempt.parsed.usage,
sessionId: resolvedSessionId,
sessionParams: resolvedSessionParams,
sessionDisplayId: resolvedSessionId,
provider: "openai",
model,
billingType,
costUsd: attempt.parsed.costUsd,
resultJson: {
stdout: attempt.proc.stdout,
stderr: attempt.proc.stderr,
},
summary: attempt.parsed.summary,
clearSession: Boolean(clearSessionOnMissingSession && !resolvedSessionId),
};
};
const initial = await runAttempt(sessionId);
if (
sessionId &&
!initial.proc.timedOut &&
(initial.proc.exitCode ?? 0) !== 0 &&
isCodexUnknownSessionError(initial.proc.stdout, initial.rawStderr)
) {
await onLog(
"stderr",
`[agent-company] Codex resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
);
const retry = await runAttempt(null);
return toResult(retry, true);
}
return toResult(initial);
}
@@ -1,7 +0,0 @@
export { execute } from "./execute.js";
export { testEnvironment } from "./test.js";
export { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
export { listCodexSkills, syncCodexSkills } from "./skills.js";
import { createStandardSessionCodec } from "@agentcompany/adapter-utils/session-codec";
export const sessionCodec = createStandardSessionCodec();
@@ -1,79 +0,0 @@
import { asString, asNumber, parseObject, parseJson } from "@agentcompany/adapter-utils/server-utils";
export function parseCodexJsonl(stdout: string) {
let sessionId: string | null = null;
const messages: string[] = [];
let errorMessage: string | null = null;
let costUsd: number | null = null;
const usage = {
inputTokens: 0,
cachedInputTokens: 0,
outputTokens: 0,
};
for (const rawLine of stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
const event = parseJson(line);
if (!event) continue;
const type = asString(event.type, "");
if (type === "thread.started") {
sessionId = asString(event.thread_id, sessionId ?? "") || sessionId;
continue;
}
if (type === "error") {
const msg = asString(event.message, "").trim();
if (msg) errorMessage = msg;
continue;
}
if (type === "item.completed") {
const item = parseObject(event.item);
if (asString(item.type, "") === "agent_message") {
const text = asString(item.text, "");
if (text) messages.push(text);
}
continue;
}
if (type === "turn.completed") {
const usageObj = parseObject(event.usage);
usage.inputTokens = asNumber(usageObj.input_tokens, usage.inputTokens);
usage.cachedInputTokens = asNumber(usageObj.cached_input_tokens, usage.cachedInputTokens);
usage.outputTokens = asNumber(usageObj.output_tokens, usage.outputTokens);
const rawCost = event.total_cost_usd;
if (typeof rawCost === "number" && Number.isFinite(rawCost)) costUsd = rawCost;
continue;
}
if (type === "turn.failed") {
const err = parseObject(event.error);
const msg = asString(err.message, "").trim();
if (msg) errorMessage = msg;
const rawCost = event.total_cost_usd;
if (typeof rawCost === "number" && Number.isFinite(rawCost)) costUsd = rawCost;
}
}
return {
sessionId,
summary: messages.join("\n\n").trim(),
usage,
costUsd,
errorMessage,
};
}
export function isCodexUnknownSessionError(stdout: string, stderr: string): boolean {
const haystack = `${stdout}\n${stderr}`
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.join("\n");
return /unknown (session|thread)|session .* not found|thread .* not found|conversation .* not found|missing rollout path for thread|state db missing rollout path/i.test(
haystack,
);
}
@@ -1,99 +0,0 @@
// ---------------------------------------------------------------------------
// Codex adapter skill listing / sync -- ephemeral mode
// Skills are linked into the workspace .agents/skills directory at run time.
// ---------------------------------------------------------------------------
import path from "node:path";
import type {
AdapterSkillContext,
AdapterSkillEntry,
AdapterSkillSnapshot,
} from "@agentcompany/adapter-utils";
import {
readAgentCompanyRuntimeSkillEntries,
resolveAgentCompanyDesiredSkillNames,
} from "@agentcompany/adapter-utils/server-utils";
async function buildCodexSkillSnapshot(
config: Record<string, unknown>,
): Promise<AdapterSkillSnapshot> {
const moduleDir = path.resolve(import.meta.dirname ?? __dirname);
const availableEntries = await readAgentCompanyRuntimeSkillEntries(config, moduleDir);
const desiredSkills = resolveAgentCompanyDesiredSkillNames(config, availableEntries);
const desiredSet = new Set(desiredSkills);
const entries: AdapterSkillEntry[] = [];
const warnings: string[] = [];
for (const available of availableEntries) {
const desired = desiredSet.has(available.key);
entries.push({
key: available.key,
runtimeName: available.runtimeName,
desired,
managed: true,
state: desired ? "configured" : "available",
sourcePath: available.source,
targetPath: null,
detail: desired
? "Will be linked into the workspace .agents/skills directory on the next run."
: null,
required: Boolean(available.required),
requiredReason: available.requiredReason ?? null,
origin: available.required ? "paperclip_required" : "company_managed",
originLabel: available.required ? "Required by AgentCompany" : "Managed by AgentCompany",
readOnly: false,
});
}
// Check for desired skills not in available
for (const desiredSkill of desiredSkills) {
if (availableEntries.some((entry) => entry.key === desiredSkill)) continue;
warnings.push(`Desired skill "${desiredSkill}" is not available from the AgentCompany skills directory.`);
entries.push({
key: desiredSkill,
runtimeName: null,
desired: true,
managed: true,
state: "missing",
sourcePath: null,
targetPath: null,
detail: "AgentCompany cannot find this skill in the local runtime skills directory.",
origin: "external_unknown",
originLabel: "External or unavailable",
readOnly: false,
});
}
entries.sort((left, right) => left.key.localeCompare(right.key));
return {
adapterType: "codex_local",
supported: true,
mode: "ephemeral",
desiredSkills,
entries,
warnings,
};
}
/**
* List the current skill state for the Codex adapter.
* Ephemeral mode: skills are linked into workspace at run time.
*/
export async function listCodexSkills(
ctx: AdapterSkillContext,
): Promise<AdapterSkillSnapshot> {
return buildCodexSkillSnapshot(ctx.config);
}
/**
* Sync skills for the Codex adapter.
* Ephemeral mode: no on-disk sync needed; skills are linked at run time.
* Returns the current snapshot for informational purposes.
*/
export async function syncCodexSkills(
ctx: AdapterSkillContext,
_desiredSkills: string[],
): Promise<AdapterSkillSnapshot> {
return buildCodexSkillSnapshot(ctx.config);
}
@@ -1,122 +0,0 @@
import type {
AdapterEnvironmentCheck,
AdapterEnvironmentTestContext,
AdapterEnvironmentTestResult,
} from "@agentcompany/adapter-utils";
import {
asString,
asBoolean,
asStringArray,
parseObject,
ensureAbsoluteDirectory,
ensureCommandResolvable,
ensurePathInEnv,
runChildProcess,
} from "@agentcompany/adapter-utils/server-utils";
import { summarizeStatus, isNonEmpty, commandLooksLike, summarizeProbeDetail } from "@agentcompany/adapter-utils/test-utils";
import { parseCodexJsonl } from "./parse.js";
const CODEX_AUTH_REQUIRED_RE =
/(?:not\s+logged\s+in|login\s+required|authentication\s+required|unauthorized|invalid(?:\s+or\s+missing)?\s+api(?:[_\s-]?key)?|openai[_\s-]?api[_\s-]?key|api[_\s-]?key.*required|please\s+run\s+`?codex\s+login`?)/i;
export async function testEnvironment(
ctx: AdapterEnvironmentTestContext,
): Promise<AdapterEnvironmentTestResult> {
const checks: AdapterEnvironmentCheck[] = [];
const config = parseObject(ctx.config);
const command = asString(config.command, "codex");
const cwd = asString(config.cwd, process.cwd());
try {
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
checks.push({ code: "codex_cwd_valid", level: "info", message: `Working directory is valid: ${cwd}` });
} catch (err) {
checks.push({ code: "codex_cwd_invalid", level: "error", message: err instanceof Error ? err.message : "Invalid working directory", detail: cwd });
}
const envConfig = parseObject(config.env);
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(envConfig)) {
if (typeof value === "string") env[key] = value;
}
const runtimeEnv = ensurePathInEnv(env);
try {
await ensureCommandResolvable(command, cwd, runtimeEnv);
checks.push({ code: "codex_command_resolvable", level: "info", message: `Command is executable: ${command}` });
} catch (err) {
checks.push({ code: "codex_command_unresolvable", level: "error", message: err instanceof Error ? err.message : "Command is not executable", detail: command });
}
const configOpenAiKey = env.OPENAI_API_KEY;
if (isNonEmpty(configOpenAiKey)) {
checks.push({ code: "codex_openai_api_key_present", level: "info", message: "OPENAI_API_KEY is set for Codex authentication." });
} else {
checks.push({ code: "codex_openai_api_key_missing", level: "info", message: "OPENAI_API_KEY is not set. Codex may use its own auth configuration." });
}
// Hello probe: wrapped in try-catch so probe failures never crash the test.
const canRunProbe = checks.every((c) => c.code !== "codex_cwd_invalid" && c.code !== "codex_command_unresolvable");
if (canRunProbe && commandLooksLike(command, "codex")) {
try {
const model = asString(config.model, "").trim();
const modelReasoningEffort = asString(config.modelReasoningEffort, asString(config.reasoningEffort, "")).trim();
const search = asBoolean(config.search, false);
const bypass = asBoolean(config.dangerouslyBypassApprovalsAndSandbox, asBoolean(config.dangerouslyBypassSandbox, false));
const extraArgs = (() => {
const fromExtraArgs = asStringArray(config.extraArgs);
if (fromExtraArgs.length > 0) return fromExtraArgs;
return asStringArray(config.args);
})();
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);
if (modelReasoningEffort) args.push("-c", `model_reasoning_effort=${JSON.stringify(modelReasoningEffort)}`);
if (extraArgs.length > 0) args.push(...extraArgs);
args.push("-");
const probe = await runChildProcess(
`codex-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
command,
args,
{ cwd, env, timeoutSec: 45, graceSec: 5, stdin: "Respond with hello.", onLog: async () => {} },
);
const parsed = parseCodexJsonl(probe.stdout);
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
if (probe.timedOut) {
checks.push({ code: "codex_hello_probe_timed_out", level: "info", message: "Codex hello probe timed out." });
} else if ((probe.exitCode ?? 1) === 0) {
const summary = parsed.summary.trim();
const hasHello = /\bhello\b/i.test(summary);
checks.push({
code: hasHello ? "codex_hello_probe_passed" : "codex_hello_probe_unexpected_output",
level: "info",
message: hasHello ? "Codex hello probe succeeded." : "Codex probe completed.",
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
});
} else if (CODEX_AUTH_REQUIRED_RE.test(authEvidence)) {
checks.push({
code: "codex_hello_probe_auth_required", level: "warn",
message: "Codex CLI is installed, but authentication is not ready.",
...(detail ? { detail } : {}),
hint: "Configure OPENAI_API_KEY or run `codex login`.",
});
} else {
checks.push({ code: "codex_hello_probe_failed", level: "info", message: "Codex hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
}
} catch {
checks.push({ code: "codex_hello_probe_error", level: "info", message: "Codex hello probe could not run." });
}
}
return {
adapterType: ctx.adapterType,
status: summarizeStatus(checks),
checks,
testedAt: new Date().toISOString(),
};
}
@@ -1,45 +0,0 @@
import type { CreateConfigValues } from "@agentcompany/adapter-utils";
import { parseCommaArgs, parseEnvVars, parseEnvBindings, parseJsonObject } from "@agentcompany/adapter-utils/config-utils";
import {
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
DEFAULT_CODEX_LOCAL_MODEL,
} from "../index.js";
export function buildCodexLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
if (v.cwd) ac.cwd = v.cwd;
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
ac.model = v.model || DEFAULT_CODEX_LOCAL_MODEL;
if (v.thinkingEffort) ac.modelReasoningEffort = v.thinkingEffort;
ac.timeoutSec = 0;
ac.graceSec = 15;
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) ac.env = env;
ac.search = v.search;
ac.dangerouslyBypassApprovalsAndSandbox =
typeof v.dangerouslyBypassSandbox === "boolean"
? v.dangerouslyBypassSandbox
: DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX;
if (v.workspaceStrategyType === "git_worktree") {
ac.workspaceStrategy = {
type: "git_worktree",
...(v.workspaceBaseRef ? { baseRef: v.workspaceBaseRef } : {}),
...(v.workspaceBranchTemplate ? { branchTemplate: v.workspaceBranchTemplate } : {}),
...(v.worktreeParentDir ? { worktreeParentDir: v.worktreeParentDir } : {}),
};
}
const runtimeServices = parseJsonObject(v.runtimeServicesJson ?? "");
if (runtimeServices && Array.isArray(runtimeServices.services)) {
ac.workspaceRuntime = runtimeServices;
}
if (v.command) ac.command = v.command;
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
return ac;
}
@@ -1,2 +0,0 @@
export { parseCodexStdoutLine } from "./parse-stdout.js";
export { buildCodexLocalConfig } from "./build-config.js";
@@ -1,234 +0,0 @@
import {
redactHomePathUserSegments,
redactHomePathUserSegmentsInValue,
type TranscriptEntry,
} from "@agentcompany/adapter-utils";
import { asRecord, safeJsonParse, errorText } from "@agentcompany/adapter-utils/parse-utils";
function asString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function asNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function stringifyUnknown(value: unknown): string {
if (typeof value === "string") return redactHomePathUserSegments(value);
if (value === null || value === undefined) return "";
try {
return JSON.stringify(redactHomePathUserSegmentsInValue(value), null, 2);
} catch {
return redactHomePathUserSegments(String(value));
}
}
function parseCommandExecutionItem(
item: Record<string, unknown>,
ts: string,
phase: "started" | "completed",
): TranscriptEntry[] {
const id = asString(item.id);
const command = asString(item.command);
const status = asString(item.status);
const exitCode = typeof item.exit_code === "number" && Number.isFinite(item.exit_code) ? item.exit_code : null;
const safeCommand = redactHomePathUserSegments(command);
const output = redactHomePathUserSegments(asString(item.aggregated_output)).replace(/\s+$/, "");
if (phase === "started") {
return [{
kind: "tool_call",
ts,
name: "command_execution",
toolUseId: id || command || "command_execution",
input: {
id,
command: safeCommand,
},
}];
}
const lines: string[] = [];
if (safeCommand) lines.push(`command: ${safeCommand}`);
if (status) lines.push(`status: ${status}`);
if (exitCode !== null) lines.push(`exit_code: ${exitCode}`);
if (output) {
if (lines.length > 0) lines.push("");
lines.push(output);
}
const isError =
(exitCode !== null && exitCode !== 0) ||
status === "failed" ||
status === "errored" ||
status === "error" ||
status === "cancelled";
return [{
kind: "tool_result",
ts,
toolUseId: id || command || "command_execution",
content: lines.join("\n").trim() || "command completed",
isError,
}];
}
function parseFileChangeItem(item: Record<string, unknown>, ts: string): TranscriptEntry[] {
const changes = Array.isArray(item.changes) ? item.changes : [];
const entries = changes
.map((changeRaw) => asRecord(changeRaw))
.filter((change): change is Record<string, unknown> => Boolean(change))
.map((change) => {
const kind = asString(change.kind, "update");
const path = redactHomePathUserSegments(asString(change.path, "unknown"));
return `${kind} ${path}`;
});
if (entries.length === 0) {
return [{ kind: "system", ts, text: "file changes applied" }];
}
const preview = entries.slice(0, 6).join(", ");
const more = entries.length > 6 ? ` (+${entries.length - 6} more)` : "";
return [{ kind: "system", ts, text: `file changes: ${preview}${more}` }];
}
function parseCodexItem(
item: Record<string, unknown>,
ts: string,
phase: "started" | "completed",
): TranscriptEntry[] {
const itemType = asString(item.type);
if (itemType === "agent_message") {
const text = asString(item.text);
if (text) return [{ kind: "assistant", ts, text: redactHomePathUserSegments(text) }];
return [];
}
if (itemType === "reasoning") {
const text = asString(item.text);
if (text) return [{ kind: "thinking", ts, text: redactHomePathUserSegments(text) }];
return [{ kind: "system", ts, text: phase === "started" ? "reasoning started" : "reasoning completed" }];
}
if (itemType === "command_execution") {
return parseCommandExecutionItem(item, ts, phase);
}
if (itemType === "file_change" && phase === "completed") {
return parseFileChangeItem(item, ts);
}
if (itemType === "tool_use") {
return [{
kind: "tool_call",
ts,
name: redactHomePathUserSegments(asString(item.name, "unknown")),
toolUseId: asString(item.id),
input: redactHomePathUserSegmentsInValue(item.input ?? {}),
}];
}
if (itemType === "tool_result" && phase === "completed") {
const toolUseId = asString(item.tool_use_id, asString(item.id));
const content =
asString(item.content) ||
asString(item.output) ||
asString(item.result) ||
stringifyUnknown(item.content ?? item.output ?? item.result);
const isError = item.is_error === true || asString(item.status) === "error";
return [{ kind: "tool_result", ts, toolUseId, content: redactHomePathUserSegments(content), isError }];
}
if (itemType === "error" && phase === "completed") {
const text = errorText(item.message ?? item.error ?? item);
return [{ kind: "stderr", ts, text: redactHomePathUserSegments(text || "error") }];
}
const id = asString(item.id);
const status = asString(item.status);
const meta = [id ? `id=${id}` : "", status ? `status=${status}` : ""].filter(Boolean).join(" ");
return [{
kind: "system",
ts,
text: redactHomePathUserSegments(`item ${phase}: ${itemType || "unknown"}${meta ? ` (${meta})` : ""}`),
}];
}
export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[] {
const parsed = asRecord(safeJsonParse(line));
if (!parsed) {
return [{ kind: "stdout", ts, text: redactHomePathUserSegments(line) }];
}
const type = asString(parsed.type);
if (type === "thread.started") {
const threadId = asString(parsed.thread_id);
return [{
kind: "init",
ts,
model: redactHomePathUserSegments(asString(parsed.model, "codex")),
sessionId: redactHomePathUserSegments(threadId),
}];
}
if (type === "turn.started") {
return [{ kind: "system", ts, text: "turn started" }];
}
if (type === "item.started" || type === "item.completed") {
const item = asRecord(parsed.item);
if (!item) return [{ kind: "system", ts, text: type.replace(".", " ") }];
return parseCodexItem(item, ts, type === "item.started" ? "started" : "completed");
}
if (type === "turn.completed") {
const usage = asRecord(parsed.usage);
const inputTokens = asNumber(usage?.input_tokens);
const outputTokens = asNumber(usage?.output_tokens);
const cachedTokens = asNumber(usage?.cached_input_tokens, asNumber(usage?.cache_read_input_tokens));
return [{
kind: "result",
ts,
text: redactHomePathUserSegments(asString(parsed.result)),
inputTokens,
outputTokens,
cachedTokens,
costUsd: asNumber(parsed.total_cost_usd),
subtype: redactHomePathUserSegments(asString(parsed.subtype)),
isError: parsed.is_error === true,
errors: Array.isArray(parsed.errors)
? parsed.errors.map(errorText).map(redactHomePathUserSegments).filter(Boolean)
: [],
}];
}
if (type === "turn.failed") {
const usage = asRecord(parsed.usage);
const inputTokens = asNumber(usage?.input_tokens);
const outputTokens = asNumber(usage?.output_tokens);
const cachedTokens = asNumber(usage?.cached_input_tokens, asNumber(usage?.cache_read_input_tokens));
const message = errorText(parsed.error ?? parsed.message);
return [{
kind: "result",
ts,
text: redactHomePathUserSegments(asString(parsed.result)),
inputTokens,
outputTokens,
cachedTokens,
costUsd: asNumber(parsed.total_cost_usd),
subtype: redactHomePathUserSegments(asString(parsed.subtype, "turn.failed")),
isError: true,
errors: message ? [redactHomePathUserSegments(message)] : [],
}];
}
if (type === "error") {
const message = errorText(parsed.message ?? parsed.error ?? parsed);
return [{ kind: "stderr", ts, text: redactHomePathUserSegments(message || line) }];
}
return [{ kind: "stdout", ts, text: redactHomePathUserSegments(line) }];
}
@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
@@ -1,51 +0,0 @@
{
"name": "@agentcompany/adapter-gemini-local",
"version": "0.3.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",
"skills"
],
"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"
}
}
@@ -1,171 +0,0 @@
import pc from "picocolors";
import { asRecord, errorText, asString, asNumber, stringifyUnknown } from "@agentcompany/adapter-utils/parse-utils";
function printTextMessage(prefix: string, colorize: (text: string) => string, messageRaw: unknown): void {
if (typeof messageRaw === "string") {
const text = messageRaw.trim();
if (text) console.log(colorize(`${prefix}: ${text}`));
return;
}
const message = asRecord(messageRaw);
if (!message) return;
const directText = asString(message.text).trim();
if (directText) console.log(colorize(`${prefix}: ${directText}`));
const content = Array.isArray(message.content) ? message.content : [];
for (const partRaw of content) {
const part = asRecord(partRaw);
if (!part) continue;
const type = asString(part.type).trim();
if (type === "output_text" || type === "text" || type === "content") {
const text = asString(part.text).trim() || asString(part.content).trim();
if (text) console.log(colorize(`${prefix}: ${text}`));
continue;
}
if (type === "thinking") {
const text = asString(part.text).trim();
if (text) console.log(pc.gray(`thinking: ${text}`));
continue;
}
if (type === "tool_call") {
const name = asString(part.name, asString(part.tool, "tool"));
console.log(pc.yellow(`tool_call: ${name}`));
const input = part.input ?? part.arguments ?? part.args;
if (input !== undefined) console.log(pc.gray(stringifyUnknown(input)));
continue;
}
if (type === "tool_result" || type === "tool_response") {
const isError = part.is_error === true || asString(part.status).toLowerCase() === "error";
const contentText =
asString(part.output) ||
asString(part.text) ||
asString(part.result) ||
stringifyUnknown(part.output ?? part.result ?? part.text ?? part.response);
console.log((isError ? pc.red : pc.cyan)(`tool_result${isError ? " (error)" : ""}`));
if (contentText) console.log((isError ? pc.red : pc.gray)(contentText));
}
}
}
function printUsage(parsed: Record<string, unknown>) {
const usage = asRecord(parsed.usage) ?? asRecord(parsed.usageMetadata);
const usageMetadata = asRecord(usage?.usageMetadata);
const source = usageMetadata ?? usage ?? {};
const input = asNumber(source.input_tokens, asNumber(source.inputTokens, asNumber(source.promptTokenCount)));
const output = asNumber(source.output_tokens, asNumber(source.outputTokens, asNumber(source.candidatesTokenCount)));
const cached = asNumber(
source.cached_input_tokens,
asNumber(source.cachedInputTokens, asNumber(source.cachedContentTokenCount)),
);
const cost = asNumber(parsed.total_cost_usd, asNumber(parsed.cost_usd, asNumber(parsed.cost)));
console.log(pc.blue(`tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`));
}
export function printGeminiStreamEvent(raw: string, debug: boolean): void {
const line = raw.trim();
if (!line) return;
let parsed: Record<string, unknown> | null = null;
try {
parsed = JSON.parse(line) as Record<string, unknown>;
} catch {
console.log(line);
return;
}
const type = asString(parsed.type);
if (type === "system") {
const subtype = asString(parsed.subtype);
if (subtype === "init") {
const sessionId =
asString(parsed.session_id) ||
asString(parsed.sessionId) ||
asString(parsed.sessionID) ||
asString(parsed.checkpoint_id);
const model = asString(parsed.model);
const details = [sessionId ? `session: ${sessionId}` : "", model ? `model: ${model}` : ""]
.filter(Boolean)
.join(", ");
console.log(pc.blue(`Gemini init${details ? ` (${details})` : ""}`));
return;
}
if (subtype === "error") {
const text = errorText(parsed.error ?? parsed.message ?? parsed.detail);
if (text) console.log(pc.red(`error: ${text}`));
return;
}
console.log(pc.blue(`system: ${subtype || "event"}`));
return;
}
if (type === "assistant") {
printTextMessage("assistant", pc.green, parsed.message);
return;
}
if (type === "user") {
printTextMessage("user", pc.gray, parsed.message);
return;
}
if (type === "thinking") {
const text = asString(parsed.text).trim() || asString(asRecord(parsed.delta)?.text).trim();
if (text) console.log(pc.gray(`thinking: ${text}`));
return;
}
if (type === "tool_call") {
const subtype = asString(parsed.subtype).trim().toLowerCase();
const toolCall = asRecord(parsed.tool_call ?? parsed.toolCall);
const [toolName] = toolCall ? Object.keys(toolCall) : [];
if (!toolCall || !toolName) {
console.log(pc.yellow(`tool_call${subtype ? `: ${subtype}` : ""}`));
return;
}
const payload = asRecord(toolCall[toolName]) ?? {};
if (subtype === "started" || subtype === "start") {
console.log(pc.yellow(`tool_call: ${toolName}`));
console.log(pc.gray(stringifyUnknown(payload.args ?? payload.input ?? payload.arguments ?? payload)));
return;
}
if (subtype === "completed" || subtype === "complete" || subtype === "finished") {
const isError =
parsed.is_error === true ||
payload.is_error === true ||
payload.error !== undefined ||
asString(payload.status).toLowerCase() === "error";
console.log((isError ? pc.red : pc.cyan)(`tool_result${isError ? " (error)" : ""}`));
console.log((isError ? pc.red : pc.gray)(stringifyUnknown(payload.result ?? payload.output ?? payload.error)));
return;
}
console.log(pc.yellow(`tool_call: ${toolName}${subtype ? ` (${subtype})` : ""}`));
return;
}
if (type === "result") {
printUsage(parsed);
const subtype = asString(parsed.subtype, "result");
const isError = parsed.is_error === true;
if (subtype || isError) {
console.log((isError ? pc.red : pc.blue)(`result: subtype=${subtype} is_error=${isError ? "true" : "false"}`));
}
return;
}
if (type === "error") {
const text = errorText(parsed.error ?? parsed.message ?? parsed.detail);
if (text) console.log(pc.red(`error: ${text}`));
return;
}
if (debug) {
console.log(pc.gray(line));
}
}
@@ -1 +0,0 @@
export { printGeminiStreamEvent } from "./format-event.js";
@@ -1,47 +0,0 @@
export const type = "gemini_local";
export const label = "Gemini CLI (local)";
export const DEFAULT_GEMINI_LOCAL_MODEL = "auto";
export const models = [
{ id: DEFAULT_GEMINI_LOCAL_MODEL, label: "Auto" },
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
{ id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" },
{ id: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
{ id: "gemini-2.0-flash-lite", label: "Gemini 2.0 Flash Lite" },
];
export const agentConfigurationDoc = `# gemini_local agent configuration
Adapter: gemini_local
Use when:
- You want AgentCompany to run the Gemini CLI locally on the host machine
- You want Gemini chat sessions resumed across heartbeats with --resume
- You want AgentCompany skills injected locally without polluting the global environment
Don't use when:
- You need webhook-style external invocation (use http or openclaw_gateway)
- You only need a one-shot script without an AI coding agent loop (use process)
- Gemini CLI is not installed on the machine that runs AgentCompany
Core fields:
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
- promptTemplate (string, optional): run prompt template
- model (string, optional): Gemini model id. Defaults to auto.
- sandbox (boolean, optional): run in sandbox mode (default: false, passes --sandbox=none)
- command (string, optional): defaults to "gemini"
- 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): SIGTERM grace period in seconds
Notes:
- Runs use positional prompt arguments, not stdin.
- Sessions resume with --resume when stored session cwd matches the current cwd.
- AgentCompany auto-injects local skills into \`~/.gemini/skills/\` via symlinks, so the CLI can discover both credentials and skills in their natural location.
- Authentication can use GEMINI_API_KEY / GOOGLE_API_KEY or local Gemini CLI login.
`;
@@ -1,127 +0,0 @@
import fs from "node:fs/promises";
import type { Dirent } from "node:fs";
import os from "node:os";
import path from "node:path";
import type { AdapterExecutionContext, ManagedSkillRef } from "@agentcompany/adapter-utils";
import {
hasNonEmptyEnvValue,
buildSkillsCandidates,
resolveSkillsDir,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
} from "@agentcompany/adapter-utils";
import {
createManagedSkillSymlinks,
} from "@agentcompany/adapter-utils/server-utils";
export {
hasNonEmptyEnvValue,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
};
export const SKILLS_CANDIDATES = buildSkillsCandidates(import.meta.url);
export function resolveGeminiBillingType(env: Record<string, string>): "api" | "subscription" {
return hasNonEmptyEnvValue(env, "GEMINI_API_KEY") || hasNonEmptyEnvValue(env, "GOOGLE_API_KEY")
? "api"
: "subscription";
}
export function renderEnvNote(env: Record<string, string>): string {
const agentCompanyKeys = Object.keys(env)
.filter((key) => key.startsWith("AGENT_COMPANY_"))
.sort();
if (agentCompanyKeys.length === 0) return "";
return [
"Runtime note:",
`The following AGENT_COMPANY_* environment variables are available in this run: ${agentCompanyKeys.join(", ")}`,
"Do not assume these variables are missing without checking your shell environment.",
"",
"",
].join("\n");
}
export function renderApiAccessNote(env: Record<string, string>): string {
if (!hasNonEmptyEnvValue(env, "AGENT_COMPANY_API_URL") || !hasNonEmptyEnvValue(env, "AGENT_COMPANY_API_KEY")) return "";
return [
"API access note:",
"Use run_shell_command with curl to make API requests.",
"GET example:",
` run_shell_command({ command: "curl -s -H \\"Authorization: Bearer $AGENT_COMPANY_API_KEY\\" \\"$AGENT_COMPANY_API_URL/api/agents/me\\"" })`,
"POST/PATCH example:",
` run_shell_command({ command: "curl -s -X POST -H \\"Authorization: Bearer $AGENT_COMPANY_API_KEY\\" -H 'Content-Type: application/json' -H \\"X-Agent-Company-Run-Id: $AGENT_COMPANY_RUN_ID\\" -d '{...}' \\"$AGENT_COMPANY_API_URL/api/issues/{id}/checkout\\"" })`,
"",
"",
].join("\n");
}
export async function resolveAgentCompanySkillsDir(): Promise<string | null> {
return resolveSkillsDir(SKILLS_CANDIDATES);
}
export function geminiSkillsHome(): string {
return path.join(os.homedir(), ".gemini", "skills");
}
/**
* Inject AgentCompany skills directly into `~/.gemini/skills/` via symlinks.
* Uses managed skills when available; falls back to static repo scan.
*/
export async function ensureGeminiSkillsInjected(
managedSkills: ManagedSkillRef[],
onLog: AdapterExecutionContext["onLog"],
): Promise<void> {
const skillsHome = geminiSkillsHome();
if (managedSkills.length > 0) {
await createManagedSkillSymlinks(managedSkills, skillsHome, onLog);
return;
}
const skillsDir = await resolveSkillsDir(SKILLS_CANDIDATES);
if (!skillsDir) return;
try {
await fs.mkdir(skillsHome, { recursive: true });
} catch (err) {
await onLog(
"stderr",
`[agent-company] Failed to prepare Gemini skills directory ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`,
);
return;
}
let entries: Dirent[];
try {
entries = await fs.readdir(skillsDir, { withFileTypes: true });
} catch (err) {
await onLog(
"stderr",
`[agent-company] Failed to read AgentCompany skills from ${skillsDir}: ${err instanceof Error ? err.message : String(err)}\n`,
);
return;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const source = path.join(skillsDir, entry.name);
const target = path.join(skillsHome, entry.name);
const existing = await fs.lstat(target).catch(() => null);
if (existing) continue;
try {
await fs.symlink(source, target);
await onLog("stderr", `[agent-company] Linked Gemini skill: ${entry.name}\n`);
} catch (err) {
await onLog(
"stderr",
`[agent-company] Failed to link Gemini skill "${entry.name}": ${err instanceof Error ? err.message : String(err)}\n`,
);
}
}
}
@@ -1,294 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils";
import {
asBoolean,
asNumber,
asString,
ensureAbsoluteDirectory,
ensureCommandResolvable,
ensurePathInEnv,
filterConflictingArgs,
redactEnvForLogs,
renderTemplate,
runChildProcess,
} from "@agentcompany/adapter-utils/server-utils";
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
import {
describeGeminiFailure,
detectGeminiAuthRequired,
isGeminiTurnLimitResult,
isGeminiUnknownSessionError,
parseGeminiJsonl,
} from "./parse.js";
import { firstNonEmptyLine } from "@agentcompany/adapter-utils/test-utils";
import {
resolveGeminiBillingType,
renderEnvNote,
renderApiAccessNote,
ensureGeminiSkillsInjected,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
} from "./execute-helpers.js";
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
"{{context.taskTitle}}\n\n{{context.taskDescription}}",
);
const command = asString(config.command, "gemini");
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
const sandbox = asBoolean(config.sandbox, false);
const { env, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef } =
await buildCommonAgentCompanyEnv(
{ runId, agent, config, context, authToken },
{ ensureAbsoluteDirectory },
);
const managedSkills = parseManagedSkills(config);
await ensureGeminiSkillsInjected(managedSkills, onLog);
const billingType = resolveGeminiBillingType(env);
const runtimeEnv = ensurePathInEnv(env);
await ensureCommandResolvable(command, cwd, runtimeEnv);
const timeoutSec = asNumber(config.timeoutSec, 0);
const graceSec = asNumber(config.graceSec, 20);
const extraArgs = parseExtraArgs(config);
const { sessionId, runtimeSessionId, runtimeSessionCwd, canResumeSession } =
resolveSessionResumeState(runtime, cwd);
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
`[agent-company] Gemini session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
let instructionsPrefix = "";
if (instructionsFilePath) {
try {
const instructionsContents = await fs.readFile(instructionsFilePath, "utf8");
instructionsPrefix =
`${instructionsContents}\n\n` +
`The above agent instructions were loaded from ${instructionsFilePath}. ` +
`Resolve any relative file references from ${instructionsDir}.\n\n`;
await onLog(
"stderr",
`[agent-company] Loaded agent instructions file: ${instructionsFilePath}\n`,
);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
await onLog(
"stderr",
`[agent-company] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`,
);
}
}
const renderedPrompt = renderTemplate(promptTemplate, {
agentId: agent.id,
companyId: agent.companyId,
runId,
company: { id: agent.companyId },
agent,
run: { id: runId, source: "on_demand" },
context,
});
const envNote = renderEnvNote(env);
const apiAccessNote = renderApiAccessNote(env);
const prompt = `${instructionsPrefix}${envNote}${apiAccessNote}${renderedPrompt}`;
const SAFE_ARG_LIMIT = 30_000;
const useStdin = prompt.length > SAFE_ARG_LIMIT;
const commandNotes = (() => {
const notes: string[] = [useStdin
? "Prompt passed via stdin (exceeds command-line length limit)."
: "Prompt is passed to Gemini as the final positional argument."];
notes.push("Added --approval-mode yolo for unattended execution.");
if (!instructionsFilePath) return notes;
if (instructionsPrefix.length > 0) {
notes.push(
`Loaded agent instructions from ${instructionsFilePath}`,
`Prepended instructions + path directive to prompt (relative references from ${instructionsDir}).`,
);
return notes;
}
notes.push(
`Configured instructionsFilePath ${instructionsFilePath}, but file could not be read; continuing without injected instructions.`,
);
return notes;
})();
let promptFilePath: string | null = null;
if (useStdin) {
promptFilePath = path.join(os.tmpdir(), `agentcompany-gemini-prompt-${runId}.txt`);
await fs.writeFile(promptFilePath, prompt, "utf8");
}
const buildArgs = (resumeSessionId: string | null) => {
const args = ["--output-format", "stream-json"];
if (resumeSessionId) args.push("--resume", resumeSessionId);
if (model && model !== DEFAULT_GEMINI_LOCAL_MODEL) args.push("--model", model);
args.push("--approval-mode", "yolo");
if (sandbox) {
args.push("--sandbox");
} else {
args.push("--sandbox=none");
}
if (extraArgs.length > 0) {
const managedFlags = [
"--approval-mode",
"--output-format",
"--model",
"--sandbox",
];
const cleaned = filterConflictingArgs(extraArgs, managedFlags);
if (cleaned.length > 0) args.push(...cleaned);
}
if (!useStdin) {
args.push(prompt);
}
return args;
};
const runAttempt = async (resumeSessionId: string | null) => {
const args = buildArgs(resumeSessionId);
if (onMeta) {
await onMeta({
adapterType: "gemini_local",
command,
cwd,
commandNotes,
commandArgs: args.map((value, index) => (
index === args.length - 1 ? `<prompt ${prompt.length} chars>` : value
)),
env: redactEnvForLogs(env),
prompt,
context,
});
}
const proc = await runChildProcess(runId, command, args, {
cwd,
env,
timeoutSec,
graceSec,
onLog,
...(useStdin ? { stdin: prompt } : {}),
});
return {
proc,
parsed: parseGeminiJsonl(proc.stdout),
};
};
const toResult = (
attempt: {
proc: {
exitCode: number | null;
signal: string | null;
timedOut: boolean;
stdout: string;
stderr: string;
};
parsed: ReturnType<typeof parseGeminiJsonl>;
},
clearSessionOnMissingSession = false,
isRetry = false,
): AdapterExecutionResult => {
const authMeta = detectGeminiAuthRequired({
parsed: attempt.parsed.resultEvent,
stdout: attempt.proc.stdout,
stderr: attempt.proc.stderr,
});
if (attempt.proc.timedOut) {
return {
exitCode: attempt.proc.exitCode,
signal: attempt.proc.signal,
timedOut: true,
errorMessage: `Timed out after ${timeoutSec}s`,
errorCode: authMeta.requiresAuth ? "gemini_auth_required" : null,
clearSession: clearSessionOnMissingSession,
};
}
const clearSessionForTurnLimit = isGeminiTurnLimitResult(attempt.parsed.resultEvent, attempt.proc.exitCode);
const canFallbackToRuntimeSession = !isRetry;
const resolvedSessionId = attempt.parsed.sessionId
?? (canFallbackToRuntimeSession ? (runtimeSessionId ?? runtime.sessionId ?? null) : null);
const resolvedSessionParams = resolvedSessionId
? ({
sessionId: resolvedSessionId,
cwd,
...(workspaceId ? { workspaceId } : {}),
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
} as Record<string, unknown>)
: null;
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
const structuredFailure = attempt.parsed.resultEvent
? describeGeminiFailure(attempt.parsed.resultEvent)
: null;
const fallbackErrorMessage =
parsedError ||
structuredFailure ||
stderrLine ||
`Gemini exited with code ${attempt.proc.exitCode ?? -1}`;
return {
exitCode: attempt.proc.exitCode,
signal: attempt.proc.signal,
timedOut: false,
errorMessage: (attempt.proc.exitCode ?? 0) === 0 ? null : fallbackErrorMessage,
errorCode: (attempt.proc.exitCode ?? 0) !== 0 && authMeta.requiresAuth ? "gemini_auth_required" : null,
usage: attempt.parsed.usage,
sessionId: resolvedSessionId,
sessionParams: resolvedSessionParams,
sessionDisplayId: resolvedSessionId,
provider: "google",
model,
billingType,
costUsd: attempt.parsed.costUsd,
resultJson: attempt.parsed.resultEvent ?? {
stdout: attempt.proc.stdout,
stderr: attempt.proc.stderr,
},
summary: attempt.parsed.summary,
question: attempt.parsed.question,
clearSession: clearSessionForTurnLimit || Boolean(clearSessionOnMissingSession && !resolvedSessionId),
};
};
try {
const initial = await runAttempt(sessionId);
if (
sessionId &&
!initial.proc.timedOut &&
(initial.proc.exitCode ?? 0) !== 0 &&
isGeminiUnknownSessionError(initial.proc.stdout, initial.proc.stderr)
) {
await onLog(
"stderr",
`[agent-company] Gemini resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
);
const retry = await runAttempt(null);
return toResult(retry, true, true);
}
return toResult(initial);
} finally {
if (promptFilePath) {
await fs.unlink(promptFilePath).catch(() => {});
}
}
}
@@ -1,13 +0,0 @@
export { execute } from "./execute.js";
export { testEnvironment } from "./test.js";
export {
parseGeminiJsonl,
isGeminiUnknownSessionError,
describeGeminiFailure,
detectGeminiAuthRequired,
isGeminiTurnLimitResult,
} from "./parse.js";
export { listGeminiSkills, syncGeminiSkills } from "./skills.js";
import { createStandardSessionCodec } from "@agentcompany/adapter-utils/session-codec";
export const sessionCodec = createStandardSessionCodec({ extraSessionIdKeys: ["sessionID"] });
@@ -1,263 +0,0 @@
import { asNumber, asString, parseJson, parseObject } from "@agentcompany/adapter-utils/server-utils";
function collectMessageText(message: unknown): string[] {
if (typeof message === "string") {
const trimmed = message.trim();
return trimmed ? [trimmed] : [];
}
const record = parseObject(message);
const direct = asString(record.text, "").trim();
const lines: string[] = direct ? [direct] : [];
const content = Array.isArray(record.content) ? record.content : [];
for (const partRaw of content) {
const part = parseObject(partRaw);
const type = asString(part.type, "").trim();
if (type === "output_text" || type === "text" || type === "content") {
const text = asString(part.text, "").trim() || asString(part.content, "").trim();
if (text) lines.push(text);
}
}
return lines;
}
function readSessionId(event: Record<string, unknown>): string | null {
return (
asString(event.session_id, "").trim() ||
asString(event.sessionId, "").trim() ||
asString(event.sessionID, "").trim() ||
asString(event.checkpoint_id, "").trim() ||
asString(event.thread_id, "").trim() ||
null
);
}
function asErrorText(value: unknown): string {
if (typeof value === "string") return value;
const rec = parseObject(value);
const message =
asString(rec.message, "") ||
asString(rec.error, "") ||
asString(rec.code, "") ||
asString(rec.detail, "");
if (message) return message;
try {
return JSON.stringify(rec);
} catch {
return "";
}
}
function accumulateUsage(
target: { inputTokens: number; cachedInputTokens: number; outputTokens: number },
usageRaw: unknown,
) {
const usage = parseObject(usageRaw);
const usageMetadata = parseObject(usage.usageMetadata);
const source = Object.keys(usageMetadata).length > 0 ? usageMetadata : usage;
target.inputTokens += asNumber(
source.input_tokens,
asNumber(source.inputTokens, asNumber(source.promptTokenCount, 0)),
);
target.cachedInputTokens += asNumber(
source.cached_input_tokens,
asNumber(source.cachedInputTokens, asNumber(source.cachedContentTokenCount, 0)),
);
target.outputTokens += asNumber(
source.output_tokens,
asNumber(source.outputTokens, asNumber(source.candidatesTokenCount, 0)),
);
}
export function parseGeminiJsonl(stdout: string) {
let sessionId: string | null = null;
const messages: string[] = [];
let errorMessage: string | null = null;
let costUsd: number | null = null;
let resultEvent: Record<string, unknown> | null = null;
let question: { prompt: string; choices: Array<{ key: string; label: string; description?: string }> } | null = null;
const usage = {
inputTokens: 0,
cachedInputTokens: 0,
outputTokens: 0,
};
for (const rawLine of stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
const event = parseJson(line);
if (!event) continue;
const foundSessionId = readSessionId(event);
if (foundSessionId) sessionId = foundSessionId;
const type = asString(event.type, "").trim();
if (type === "assistant") {
messages.push(...collectMessageText(event.message));
const messageObj = parseObject(event.message);
const content = Array.isArray(messageObj.content) ? messageObj.content : [];
for (const partRaw of content) {
const part = parseObject(partRaw);
if (asString(part.type, "").trim() === "question") {
question = {
prompt: asString(part.prompt, "").trim(),
choices: (Array.isArray(part.choices) ? part.choices : []).map((choiceRaw) => {
const choice = parseObject(choiceRaw);
return {
key: asString(choice.key, "").trim(),
label: asString(choice.label, "").trim(),
description: asString(choice.description, "").trim() || undefined,
};
}),
};
break; // only one question per message
}
}
continue;
}
if (type === "result") {
resultEvent = event;
accumulateUsage(usage, event.usage ?? event.usageMetadata);
const resultText =
asString(event.result, "").trim() ||
asString(event.text, "").trim() ||
asString(event.response, "").trim();
if (resultText && messages.length === 0) messages.push(resultText);
costUsd = asNumber(event.total_cost_usd, asNumber(event.cost_usd, asNumber(event.cost, costUsd ?? 0))) || costUsd;
const isError = event.is_error === true || asString(event.subtype, "").toLowerCase() === "error";
if (isError) {
const text = asErrorText(event.error ?? event.message ?? event.result).trim();
if (text) errorMessage = text;
}
continue;
}
if (type === "error") {
const text = asErrorText(event.error ?? event.message ?? event.detail).trim();
if (text) errorMessage = text;
continue;
}
if (type === "system") {
const subtype = asString(event.subtype, "").trim().toLowerCase();
if (subtype === "error") {
const text = asErrorText(event.error ?? event.message ?? event.detail).trim();
if (text) errorMessage = text;
}
continue;
}
if (type === "text") {
const part = parseObject(event.part);
const text = asString(part.text, "").trim();
if (text) messages.push(text);
continue;
}
if (type === "step_finish" || event.usage || event.usageMetadata) {
accumulateUsage(usage, event.usage ?? event.usageMetadata);
costUsd = asNumber(event.total_cost_usd, asNumber(event.cost_usd, asNumber(event.cost, costUsd ?? 0))) || costUsd;
continue;
}
}
return {
sessionId,
summary: messages.join("\n\n").trim(),
usage,
costUsd,
errorMessage,
resultEvent,
question,
};
}
export function isGeminiUnknownSessionError(stdout: string, stderr: string): boolean {
const haystack = `${stdout}\n${stderr}`
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.join("\n");
return /unknown\s+session|session\s+.*\s+not\s+found|resume\s+.*\s+not\s+found|checkpoint\s+.*\s+not\s+found|cannot\s+resume|failed\s+to\s+resume/i.test(
haystack,
);
}
function extractGeminiErrorMessages(parsed: Record<string, unknown>): string[] {
const messages: string[] = [];
const errorMsg = asString(parsed.error, "").trim();
if (errorMsg) messages.push(errorMsg);
const raw = Array.isArray(parsed.errors) ? parsed.errors : [];
for (const entry of raw) {
if (typeof entry === "string") {
const msg = entry.trim();
if (msg) messages.push(msg);
continue;
}
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
const obj = entry as Record<string, unknown>;
const msg = asString(obj.message, "") || asString(obj.error, "") || asString(obj.code, "");
if (msg) {
messages.push(msg);
continue;
}
try {
messages.push(JSON.stringify(obj));
} catch {
// skip non-serializable entry
}
}
return messages;
}
export function describeGeminiFailure(parsed: Record<string, unknown>): string | null {
const status = asString(parsed.status, "");
const errors = extractGeminiErrorMessages(parsed);
const detail = errors[0] ?? "";
const parts = ["Gemini run failed"];
if (status) parts.push(`status=${status}`);
if (detail) parts.push(detail);
return parts.length > 1 ? parts.join(": ") : null;
}
const GEMINI_AUTH_REQUIRED_RE = /(?:not\s+authenticated|please\s+authenticate|api[_ ]?key\s+(?:required|missing|invalid)|authentication\s+required|unauthorized|invalid\s+credentials|not\s+logged\s+in|login\s+required|run\s+`?gemini\s+auth(?:\s+login)?`?\s+first)/i;
export function detectGeminiAuthRequired(input: {
parsed: Record<string, unknown> | null;
stdout: string;
stderr: string;
}): { requiresAuth: boolean } {
const errors = extractGeminiErrorMessages(input.parsed ?? {});
const messages = [...errors, input.stdout, input.stderr]
.join("\n")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const requiresAuth = messages.some((line) => GEMINI_AUTH_REQUIRED_RE.test(line));
return { requiresAuth };
}
export function isGeminiTurnLimitResult(
parsed: Record<string, unknown> | null | undefined,
exitCode?: number | null,
): boolean {
if (exitCode === 53) return true;
if (!parsed) return false;
const status = asString(parsed.status, "").trim().toLowerCase();
if (status === "turn_limit" || status === "max_turns") return true;
const error = asString(parsed.error, "").trim();
return /turn\s*limit|max(?:imum)?\s+turns?/i.test(error);
}
@@ -1,113 +0,0 @@
// ---------------------------------------------------------------------------
// Gemini adapter skill listing / sync -- persistent mode
// Skills are symlinked into ~/.gemini/skills/ and persist across runs.
// ---------------------------------------------------------------------------
import { promises as fs } from "node:fs";
import path from "node:path";
import type {
AdapterSkillContext,
AdapterSkillSnapshot,
} from "@agentcompany/adapter-utils";
import {
parseObject,
asString,
readAgentCompanyRuntimeSkillEntries,
readInstalledSkillTargets,
resolveAgentCompanyDesiredSkillNames,
buildPersistentSkillSnapshot,
ensureAgentCompanySkillSymlink,
removeMaintainerOnlySkillSymlinks,
} from "@agentcompany/adapter-utils/server-utils";
function resolveGeminiSkillsHome(config: Record<string, unknown>): string {
const env = parseObject(config.env);
const home = asString(env.HOME, asString(env.USERPROFILE, ""));
const base = home || (process.env.HOME ?? process.env.USERPROFILE ?? "");
return path.join(base, ".gemini", "skills");
}
async function buildGeminiSkillSnapshot(
config: Record<string, unknown>,
): Promise<AdapterSkillSnapshot> {
const moduleDir = path.resolve(import.meta.dirname ?? __dirname);
const availableEntries = await readAgentCompanyRuntimeSkillEntries(config, moduleDir);
const desiredSkills = resolveAgentCompanyDesiredSkillNames(config, availableEntries);
const skillsHome = resolveGeminiSkillsHome(config);
const installed = await readInstalledSkillTargets(skillsHome);
return buildPersistentSkillSnapshot({
adapterType: "gemini_local",
availableEntries,
desiredSkills,
installed,
skillsHome,
locationLabel: "~/.gemini/skills",
missingDetail: "Configured but not currently linked into the Gemini skills home.",
externalConflictDetail: "Skill name is occupied by an external installation.",
externalDetail: "Installed outside AgentCompany management.",
});
}
/**
* List the current skill state for the Gemini adapter.
* Persistent mode: reports actual installed state in ~/.gemini/skills/.
*/
export async function listGeminiSkills(
ctx: AdapterSkillContext,
): Promise<AdapterSkillSnapshot> {
return buildGeminiSkillSnapshot(ctx.config);
}
/**
* Sync skills for the Gemini adapter.
* Persistent mode: creates symlinks for desired skills, removes stale ones,
* then returns a fresh snapshot.
*/
export async function syncGeminiSkills(
ctx: AdapterSkillContext,
desiredSkills: string[],
): Promise<AdapterSkillSnapshot> {
const moduleDir = path.resolve(import.meta.dirname ?? __dirname);
const config = ctx.config;
const availableEntries = await readAgentCompanyRuntimeSkillEntries(config, moduleDir);
const skillsHome = resolveGeminiSkillsHome(config);
// Ensure the skills home directory exists
await fs.mkdir(skillsHome, { recursive: true });
// Build a set of desired skill keys for quick lookup
const desiredSet = new Set(desiredSkills);
// Symlink type for Windows compatibility
const symlinkType = process.platform === "win32" ? ("junction" as const) : undefined;
const linkSkill = async (source: string, target: string) => {
await fs.symlink(source, target, symlinkType);
};
// Create/repair symlinks for desired skills
const syncedRuntimeNames = new Set<string>();
for (const available of availableEntries) {
if (!desiredSet.has(available.key)) continue;
const target = path.join(skillsHome, available.runtimeName);
await ensureAgentCompanySkillSymlink(available.source, target, linkSkill);
syncedRuntimeNames.add(available.runtimeName);
}
// Remove stale maintainer-only symlinks that are no longer desired
await removeMaintainerOnlySkillSymlinks(skillsHome, syncedRuntimeNames);
// Return fresh snapshot reflecting the new state
const installed = await readInstalledSkillTargets(skillsHome);
return buildPersistentSkillSnapshot({
adapterType: "gemini_local",
availableEntries,
desiredSkills,
installed,
skillsHome,
locationLabel: "~/.gemini/skills",
missingDetail: "Configured but not currently linked into the Gemini skills home.",
externalConflictDetail: "Skill name is occupied by an external installation.",
externalDetail: "Installed outside AgentCompany management.",
});
}
@@ -1,152 +0,0 @@
import type {
AdapterEnvironmentCheck,
AdapterEnvironmentTestContext,
AdapterEnvironmentTestResult,
} from "@agentcompany/adapter-utils";
import {
asBoolean,
asString,
asStringArray,
ensureAbsoluteDirectory,
ensureCommandResolvable,
ensurePathInEnv,
parseObject,
runChildProcess,
} from "@agentcompany/adapter-utils/server-utils";
import { summarizeStatus, isNonEmpty, commandLooksLike, summarizeProbeDetail } from "@agentcompany/adapter-utils/test-utils";
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
import { detectGeminiAuthRequired, parseGeminiJsonl } from "./parse.js";
export async function testEnvironment(
ctx: AdapterEnvironmentTestContext,
): Promise<AdapterEnvironmentTestResult> {
const checks: AdapterEnvironmentCheck[] = [];
const config = parseObject(ctx.config);
const command = asString(config.command, "gemini");
const cwd = asString(config.cwd, process.cwd());
try {
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
checks.push({
code: "gemini_cwd_valid",
level: "info",
message: `Working directory is valid: ${cwd}`,
});
} catch (err) {
checks.push({
code: "gemini_cwd_invalid",
level: "error",
message: err instanceof Error ? err.message : "Invalid working directory",
detail: cwd,
});
}
const envConfig = parseObject(config.env);
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(envConfig)) {
if (typeof value === "string") env[key] = value;
}
const runtimeEnv = ensurePathInEnv(env);
try {
await ensureCommandResolvable(command, cwd, runtimeEnv);
checks.push({
code: "gemini_command_resolvable",
level: "info",
message: `Command is executable: ${command}`,
});
} catch (err) {
checks.push({
code: "gemini_command_unresolvable",
level: "error",
message: err instanceof Error ? err.message : "Command is not executable",
detail: command,
});
}
const configGeminiApiKey = env.GEMINI_API_KEY;
const configGoogleApiKey = env.GOOGLE_API_KEY;
const hasGca = env.GOOGLE_GENAI_USE_GCA === "true";
if (
isNonEmpty(configGeminiApiKey) ||
isNonEmpty(configGoogleApiKey) ||
hasGca
) {
const source = hasGca
? "Google account login (GCA)"
: "adapter runtime env";
checks.push({
code: "gemini_api_key_present",
level: "info",
message: "Gemini API credentials are set for CLI authentication.",
detail: `Detected in ${source}.`,
});
} else {
checks.push({
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 connector secret bindings, or run `gemini auth login`.",
});
}
const canRunProbe =
checks.every((check) => check.code !== "gemini_cwd_invalid" && check.code !== "gemini_command_unresolvable");
if (canRunProbe && commandLooksLike(command, "gemini")) {
try {
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, false) ? "yolo" : "default");
const sandbox = asBoolean(config.sandbox, false);
const extraArgs = (() => {
const fromExtraArgs = asStringArray(config.extraArgs);
if (fromExtraArgs.length > 0) return fromExtraArgs;
return asStringArray(config.args);
})();
const args = ["--output-format", "stream-json"];
if (model && model !== DEFAULT_GEMINI_LOCAL_MODEL) args.push("--model", model);
if (approvalMode !== "default") args.push("--approval-mode", approvalMode);
if (sandbox) {
args.push("--sandbox");
} else {
args.push("--sandbox=none");
}
if (extraArgs.length > 0) args.push(...extraArgs);
args.push("Respond with hello.");
const probe = await runChildProcess(
`gemini-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
command,
args,
{ cwd, env, timeoutSec: 45, graceSec: 5, onLog: async () => { } },
);
const parsed = parseGeminiJsonl(probe.stdout);
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
const authMeta = detectGeminiAuthRequired({ parsed: parsed.resultEvent, stdout: probe.stdout, stderr: probe.stderr });
if (probe.timedOut) {
checks.push({ code: "gemini_hello_probe_timed_out", level: "info", message: "Gemini hello probe timed out." });
} else if ((probe.exitCode ?? 1) === 0) {
const summary = parsed.summary.trim();
checks.push({ code: "gemini_hello_probe_passed", level: "info", message: "Gemini probe completed.", ...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}) });
} else if (authMeta.requiresAuth) {
checks.push({
code: "gemini_hello_probe_auth_required", level: "warn",
message: "Gemini CLI is installed, but authentication is not ready.",
...(detail ? { detail } : {}),
hint: "Run `gemini auth` or configure GEMINI_API_KEY / GOOGLE_API_KEY.",
});
} else {
checks.push({ code: "gemini_hello_probe_failed", level: "info", message: "Gemini hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
}
} catch {
checks.push({ code: "gemini_hello_probe_error", level: "info", message: "Gemini hello probe could not run." });
}
}
return {
adapterType: ctx.adapterType,
status: summarizeStatus(checks),
checks,
testedAt: new Date().toISOString(),
};
}
@@ -1,26 +0,0 @@
import type { CreateConfigValues } from "@agentcompany/adapter-utils";
import { parseCommaArgs, parseEnvVars, parseEnvBindings } from "@agentcompany/adapter-utils/config-utils";
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
export function buildGeminiLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
if (v.cwd) ac.cwd = v.cwd;
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
ac.model = v.model || DEFAULT_GEMINI_LOCAL_MODEL;
ac.timeoutSec = 0;
ac.graceSec = 15;
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) ac.env = env;
ac.sandbox = !v.dangerouslyBypassSandbox;
if (v.command) ac.command = v.command;
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
return ac;
}
@@ -1,2 +0,0 @@
export { parseGeminiStdoutLine } from "./parse-stdout.js";
export { buildGeminiLocalConfig } from "./build-config.js";
@@ -1,245 +0,0 @@
import type { TranscriptEntry } from "@agentcompany/adapter-utils";
import { asRecord, safeJsonParse, errorText } from "@agentcompany/adapter-utils/parse-utils";
function asString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function asNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function stringifyUnknown(value: unknown): string {
if (typeof value === "string") return value;
if (value === null || value === undefined) return "";
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function collectTextEntries(messageRaw: unknown, ts: string, kind: "assistant" | "user"): TranscriptEntry[] {
if (typeof messageRaw === "string") {
const text = messageRaw.trim();
return text ? [{ kind, ts, text }] : [];
}
const message = asRecord(messageRaw);
if (!message) return [];
const entries: TranscriptEntry[] = [];
const directText = asString(message.text).trim();
if (directText) entries.push({ kind, ts, text: directText });
const content = Array.isArray(message.content) ? message.content : [];
for (const partRaw of content) {
const part = asRecord(partRaw);
if (!part) continue;
const type = asString(part.type).trim();
if (type !== "output_text" && type !== "text" && type !== "content") continue;
const text = asString(part.text).trim() || asString(part.content).trim();
if (text) entries.push({ kind, ts, text });
}
return entries;
}
function parseAssistantMessage(messageRaw: unknown, ts: string): TranscriptEntry[] {
if (typeof messageRaw === "string") {
const text = messageRaw.trim();
return text ? [{ kind: "assistant", ts, text }] : [];
}
const message = asRecord(messageRaw);
if (!message) return [];
const entries: TranscriptEntry[] = [];
const directText = asString(message.text).trim();
if (directText) entries.push({ kind: "assistant", ts, text: directText });
const content = Array.isArray(message.content) ? message.content : [];
for (const partRaw of content) {
const part = asRecord(partRaw);
if (!part) continue;
const type = asString(part.type).trim();
if (type === "output_text" || type === "text" || type === "content") {
const text = asString(part.text).trim() || asString(part.content).trim();
if (text) entries.push({ kind: "assistant", ts, text });
continue;
}
if (type === "thinking") {
const text = asString(part.text).trim();
if (text) entries.push({ kind: "thinking", ts, text });
continue;
}
if (type === "tool_call") {
const name = asString(part.name, asString(part.tool, "tool"));
entries.push({
kind: "tool_call",
ts,
name,
input: part.input ?? part.arguments ?? part.args ?? {},
});
continue;
}
if (type === "tool_result" || type === "tool_response") {
const toolUseId =
asString(part.tool_use_id) ||
asString(part.toolUseId) ||
asString(part.call_id) ||
asString(part.id) ||
"tool_result";
const contentText =
asString(part.output) ||
asString(part.text) ||
asString(part.result) ||
stringifyUnknown(part.output ?? part.result ?? part.text ?? part.response);
const isError = part.is_error === true || asString(part.status).toLowerCase() === "error";
entries.push({
kind: "tool_result",
ts,
toolUseId,
content: contentText,
isError,
});
}
}
return entries;
}
function parseTopLevelToolEvent(parsed: Record<string, unknown>, ts: string): TranscriptEntry[] {
const subtype = asString(parsed.subtype).trim().toLowerCase();
const callId = asString(parsed.call_id, asString(parsed.callId, asString(parsed.id, "tool_call")));
const toolCall = asRecord(parsed.tool_call ?? parsed.toolCall);
if (!toolCall) {
return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}` }];
}
const [toolName] = Object.keys(toolCall);
if (!toolName) {
return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}` }];
}
const payload = asRecord(toolCall[toolName]) ?? {};
if (subtype === "started" || subtype === "start") {
return [{
kind: "tool_call",
ts,
name: toolName,
input: payload.args ?? payload.input ?? payload.arguments ?? payload,
}];
}
if (subtype === "completed" || subtype === "complete" || subtype === "finished") {
const result = payload.result ?? payload.output ?? payload.error;
const isError =
parsed.is_error === true ||
payload.is_error === true ||
payload.error !== undefined ||
asString(payload.status).toLowerCase() === "error";
return [{
kind: "tool_result",
ts,
toolUseId: callId,
content: result !== undefined ? stringifyUnknown(result) : `${toolName} completed`,
isError,
}];
}
return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}: ${toolName}` }];
}
function readSessionId(parsed: Record<string, unknown>): string {
return (
asString(parsed.session_id) ||
asString(parsed.sessionId) ||
asString(parsed.sessionID) ||
asString(parsed.checkpoint_id) ||
asString(parsed.thread_id)
);
}
function readUsage(parsed: Record<string, unknown>) {
const usage = asRecord(parsed.usage) ?? asRecord(parsed.usageMetadata);
const usageMetadata = asRecord(usage?.usageMetadata);
const source = usageMetadata ?? usage ?? {};
return {
inputTokens: asNumber(source.input_tokens, asNumber(source.inputTokens, asNumber(source.promptTokenCount))),
outputTokens: asNumber(source.output_tokens, asNumber(source.outputTokens, asNumber(source.candidatesTokenCount))),
cachedTokens: asNumber(
source.cached_input_tokens,
asNumber(source.cachedInputTokens, asNumber(source.cachedContentTokenCount)),
),
};
}
export function parseGeminiStdoutLine(line: string, ts: string): TranscriptEntry[] {
const parsed = asRecord(safeJsonParse(line));
if (!parsed) {
return [{ kind: "stdout", ts, text: line }];
}
const type = asString(parsed.type);
if (type === "system") {
const subtype = asString(parsed.subtype);
if (subtype === "init") {
const sessionId = readSessionId(parsed);
return [{ kind: "init", ts, model: asString(parsed.model, "gemini"), sessionId }];
}
if (subtype === "error") {
const text = errorText(parsed.error ?? parsed.message ?? parsed.detail);
return [{ kind: "stderr", ts, text: text || "error" }];
}
return [{ kind: "system", ts, text: `system: ${subtype || "event"}` }];
}
if (type === "assistant") {
return parseAssistantMessage(parsed.message, ts);
}
if (type === "user") {
return collectTextEntries(parsed.message, ts, "user");
}
if (type === "thinking") {
const text = asString(parsed.text).trim() || asString(asRecord(parsed.delta)?.text).trim();
return text ? [{ kind: "thinking", ts, text }] : [];
}
if (type === "tool_call") {
return parseTopLevelToolEvent(parsed, ts);
}
if (type === "result") {
const usage = readUsage(parsed);
const errors = parsed.is_error === true
? [errorText(parsed.error ?? parsed.message ?? parsed.result)].filter(Boolean)
: [];
return [{
kind: "result",
ts,
text: asString(parsed.result) || asString(parsed.text) || asString(parsed.response),
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
cachedTokens: usage.cachedTokens,
costUsd: asNumber(parsed.total_cost_usd, asNumber(parsed.cost_usd, asNumber(parsed.cost))),
subtype: asString(parsed.subtype, "result"),
isError: parsed.is_error === true,
errors,
}];
}
if (type === "error") {
const text = errorText(parsed.error ?? parsed.message ?? parsed.detail);
return [{ kind: "stderr", ts, text: text || "error" }];
}
return [{ kind: "stdout", ts, text: line }];
}
@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
-36
View File
@@ -1,36 +0,0 @@
{
"name": "@agentcompany/adapter-utils",
"version": "0.3.0",
"type": "module",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
},
"publishConfig": {
"access": "public",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"import": "./dist/*.js"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^24.6.0",
"typescript": "^5.7.3"
}
}
@@ -1,61 +0,0 @@
export function parseCommaArgs(value: string): string[] {
return value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
export 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;
}
export 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 parseJsonObject(text: string): Record<string, unknown> | null {
const trimmed = text.trim();
if (!trimmed) return null;
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
return parsed as Record<string, unknown>;
} catch {
return null;
}
}
@@ -1,232 +0,0 @@
import { fileURLToPath } from "node:url";
import path from "node:path";
import fs from "node:fs/promises";
import type { ManagedSkillRef } from "./types.js";
import { asString, asStringArray, parseObject } from "./server-utils-parsers.js";
import { buildAgentCompanyEnv } from "./server-utils-parsers.js";
// ---------------------------------------------------------------------------
// hasNonEmptyEnvValue
// ---------------------------------------------------------------------------
export function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
const raw = env[key];
return typeof raw === "string" && raw.trim().length > 0;
}
// ---------------------------------------------------------------------------
// Skills directory resolution
// ---------------------------------------------------------------------------
/**
* Build the standard two-candidate list for a given adapter package.
* `importMetaUrl` should be `import.meta.url` from the adapter's execute-helpers.ts.
* - published: <pkg>/dist/server/ -> <pkg>/skills/
* - dev: src/server/ -> repo root/skills/
*/
export function buildSkillsCandidates(importMetaUrl: string): string[] {
const moduleDir = path.dirname(fileURLToPath(importMetaUrl));
return [
path.resolve(moduleDir, "../../skills"),
path.resolve(moduleDir, "../../../../../skills"),
];
}
/**
* Resolve the first existing skills directory from the given candidates.
*/
export async function resolveSkillsDir(candidates: string[]): Promise<string | null> {
for (const candidate of candidates) {
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
if (isDir) return candidate;
}
return null;
}
// ---------------------------------------------------------------------------
// ManagedSkills type guard + parser
// ---------------------------------------------------------------------------
export function parseManagedSkills(config: Record<string, unknown>): ManagedSkillRef[] {
if (!Array.isArray(config.managedSkills)) return [];
return (config.managedSkills as ManagedSkillRef[]).filter(
(s) => typeof s.runtimeName === "string" && typeof s.sourcePath === "string",
);
}
// ---------------------------------------------------------------------------
// extraArgs parser
// ---------------------------------------------------------------------------
export function parseExtraArgs(config: Record<string, unknown>): string[] {
const fromExtraArgs = asStringArray(config.extraArgs);
if (fromExtraArgs.length > 0) return fromExtraArgs;
return asStringArray(config.args);
}
// ---------------------------------------------------------------------------
// Common env building
// ---------------------------------------------------------------------------
export interface CommonAgentCompanyEnvInput {
runId: string;
agent: { id: string; companyId: string };
config: Record<string, unknown>;
context: Record<string, unknown>;
authToken?: string;
}
export interface CommonAgentCompanyEnvResult {
env: Record<string, string>;
cwd: string;
effectiveWorkspaceCwd: string;
workspaceId: string | null;
workspaceRepoUrl: string | null;
workspaceRepoRef: string | null;
hasExplicitApiKey: boolean;
}
/**
* Build the AGENT_COMPANY_* environment block common to all local adapters.
* Returns the env map plus resolved workspace fields needed for session params.
*/
export async function buildCommonAgentCompanyEnv(
input: CommonAgentCompanyEnvInput,
opts: {
ensureAbsoluteDirectory: (dir: string, options: { createIfMissing: boolean }) => Promise<void>;
},
): Promise<CommonAgentCompanyEnvResult> {
const { runId, agent, config, context, authToken } = input;
const workspaceContext = parseObject(context.agentCompanyWorkspace);
const workspaceCwd = asString(workspaceContext.cwd, "");
const workspaceSource = asString(workspaceContext.source, "");
const workspaceStrategy = asString(workspaceContext.strategy, "");
const workspaceId = asString(workspaceContext.workspaceId, "") || null;
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "") || null;
const workspaceRepoRef = asString(workspaceContext.repoRef, "") || null;
const workspaceBranch = asString(workspaceContext.branchName, "") || null;
const workspaceWorktreePath = asString(workspaceContext.worktreePath, "") || null;
const workspaceHints = Array.isArray(context.agentCompanyWorkspaces)
? context.agentCompanyWorkspaces.filter(
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
)
: [];
const runtimeServiceIntents = Array.isArray(context.agentCompanyRuntimeServiceIntents)
? context.agentCompanyRuntimeServiceIntents.filter(
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
)
: [];
const runtimeServices = Array.isArray(context.agentCompanyRuntimeServices)
? context.agentCompanyRuntimeServices.filter(
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
)
: [];
const runtimePrimaryUrl = asString(context.agentCompanyRuntimePrimaryUrl as unknown, "");
const configuredCwd = asString(config.cwd, "");
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
await opts.ensureAbsoluteDirectory(cwd, { createIfMissing: true });
const envConfig = parseObject(config.env);
const hasExplicitApiKey =
typeof envConfig.AGENT_COMPANY_API_KEY === "string" && envConfig.AGENT_COMPANY_API_KEY.trim().length > 0;
const env: Record<string, string> = { ...buildAgentCompanyEnv(agent) };
env.AGENT_COMPANY_RUN_ID = runId;
const wakeTaskId =
(typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
(typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
null;
const wakeReason =
typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
? context.wakeReason.trim()
: null;
const wakeCommentId =
(typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
(typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
null;
const approvalId =
typeof context.approvalId === "string" && context.approvalId.trim().length > 0
? context.approvalId.trim()
: null;
const approvalStatus =
typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
? context.approvalStatus.trim()
: null;
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
if (wakeTaskId) env.AGENT_COMPANY_TASK_ID = wakeTaskId;
if (wakeReason) env.AGENT_COMPANY_WAKE_REASON = wakeReason;
if (wakeCommentId) env.AGENT_COMPANY_WAKE_COMMENT_ID = wakeCommentId;
if (approvalId) env.AGENT_COMPANY_APPROVAL_ID = approvalId;
if (approvalStatus) env.AGENT_COMPANY_APPROVAL_STATUS = approvalStatus;
if (linkedIssueIds.length > 0) env.AGENT_COMPANY_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
if (effectiveWorkspaceCwd) env.AGENT_COMPANY_WORKSPACE_CWD = effectiveWorkspaceCwd;
if (workspaceSource) env.AGENT_COMPANY_WORKSPACE_SOURCE = workspaceSource;
if (workspaceStrategy) env.AGENT_COMPANY_WORKSPACE_STRATEGY = workspaceStrategy;
if (workspaceId) env.AGENT_COMPANY_WORKSPACE_ID = workspaceId;
if (workspaceRepoUrl) env.AGENT_COMPANY_WORKSPACE_REPO_URL = workspaceRepoUrl;
if (workspaceRepoRef) env.AGENT_COMPANY_WORKSPACE_REPO_REF = workspaceRepoRef;
if (workspaceBranch) env.AGENT_COMPANY_WORKSPACE_BRANCH = workspaceBranch;
if (workspaceWorktreePath) env.AGENT_COMPANY_WORKSPACE_WORKTREE_PATH = workspaceWorktreePath;
if (workspaceHints.length > 0) env.AGENT_COMPANY_WORKSPACES_JSON = JSON.stringify(workspaceHints);
if (runtimeServiceIntents.length > 0) env.AGENT_COMPANY_RUNTIME_SERVICE_INTENTS_JSON = JSON.stringify(runtimeServiceIntents);
if (runtimeServices.length > 0) env.AGENT_COMPANY_RUNTIME_SERVICES_JSON = JSON.stringify(runtimeServices);
if (runtimePrimaryUrl) env.AGENT_COMPANY_RUNTIME_PRIMARY_URL = runtimePrimaryUrl;
// Merge caller-configured env overrides
for (const [key, value] of Object.entries(envConfig)) {
if (typeof value === "string") env[key] = value;
}
if (!hasExplicitApiKey && authToken) {
env.AGENT_COMPANY_API_KEY = authToken;
}
return {
env,
cwd,
effectiveWorkspaceCwd,
workspaceId,
workspaceRepoUrl,
workspaceRepoRef,
hasExplicitApiKey,
};
}
// ---------------------------------------------------------------------------
// Session resume validation
// ---------------------------------------------------------------------------
export interface SessionResumeState {
sessionId: string | null;
runtimeSessionId: string;
runtimeSessionCwd: string;
canResumeSession: boolean;
}
export function resolveSessionResumeState(
runtime: { sessionId?: string | null; sessionParams?: Record<string, unknown> | null },
cwd: string,
): SessionResumeState {
const runtimeSessionParams = parseObject(runtime.sessionParams);
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
const canResumeSession =
runtimeSessionId.length > 0 &&
(runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
return {
sessionId: canResumeSession ? runtimeSessionId : null,
runtimeSessionId,
runtimeSessionCwd,
canResumeSession,
};
}
-50
View File
@@ -1,50 +0,0 @@
export type {
AdapterAgent,
AdapterRuntime,
UsageSummary,
AdapterBillingType,
AdapterRuntimeServiceReport,
AdapterExecutionResult,
AdapterInvocationMeta,
AdapterExecutionContext,
AdapterEnvironmentCheckLevel,
AdapterEnvironmentCheck,
AdapterEnvironmentTestStatus,
AdapterEnvironmentTestResult,
AdapterEnvironmentTestContext,
AdapterSkillSyncMode,
AdapterSkillState,
AdapterSkillOrigin,
AdapterSkillEntry,
AdapterSkillSnapshot,
AdapterSkillContext,
AdapterSessionCodec,
AdapterModel,
HireApprovedPayload,
HireApprovedHookResult,
ServerAdapterModule,
TranscriptEntry,
StdoutLineParser,
CLIAdapterModule,
CreateConfigValues,
ManagedSkillRef,
} from "./types.js";
export {
REDACTED_HOME_PATH_USER,
redactHomePathUserSegments,
redactHomePathUserSegmentsInValue,
} from "./log-redaction.js";
export type {
CommonAgentCompanyEnvInput,
CommonAgentCompanyEnvResult,
SessionResumeState,
} from "./execute-helpers.js";
export {
hasNonEmptyEnvValue,
buildSkillsCandidates,
resolveSkillsDir,
parseManagedSkills,
parseExtraArgs,
buildCommonAgentCompanyEnv,
resolveSessionResumeState,
} from "./execute-helpers.js";
@@ -1,49 +0,0 @@
export const REDACTED_HOME_PATH_USER = "[]";
const HOME_PATH_PATTERNS = [
{
regex: /\/Users\/[^/\\\s]+/g,
replace: `/Users/${REDACTED_HOME_PATH_USER}`,
},
{
regex: /\/home\/[^/\\\s]+/g,
replace: `/home/${REDACTED_HOME_PATH_USER}`,
},
{
regex: /([A-Za-z]:\\Users\\)[^\\/\s]+/g,
replace: `$1${REDACTED_HOME_PATH_USER}`,
},
] as const;
function isPlainObject(value: unknown): value is Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
export function redactHomePathUserSegments(text: string): string {
let result = text;
for (const pattern of HOME_PATH_PATTERNS) {
result = result.replace(pattern.regex, pattern.replace);
}
return result;
}
export function redactHomePathUserSegmentsInValue<T>(value: T): T {
if (typeof value === "string") {
return redactHomePathUserSegments(value) as T;
}
if (Array.isArray(value)) {
return value.map((entry) => redactHomePathUserSegmentsInValue(entry)) as T;
}
if (!isPlainObject(value)) {
return value;
}
const redacted: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value)) {
redacted[key] = redactHomePathUserSegmentsInValue(entry);
}
return redacted as T;
}
-47
View File
@@ -1,47 +0,0 @@
export function asRecord(value: unknown): Record<string, unknown> | null {
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
export function safeJsonParse(text: string): unknown {
try {
return JSON.parse(text);
} catch {
return null;
}
}
export function errorText(value: unknown): string {
if (typeof value === "string") return value;
const rec = asRecord(value);
if (!rec) return "";
const msg =
(typeof rec.message === "string" && rec.message) ||
(typeof rec.error === "string" && rec.error) ||
(typeof rec.code === "string" && rec.code) ||
"";
if (msg) return msg;
try {
return JSON.stringify(rec);
} catch {
return "";
}
}
export function asString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
export function asNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
export function stringifyUnknown(value: unknown): string {
if (typeof value === "string") return value;
if (value === null || value === undefined) return "";
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
@@ -1,138 +0,0 @@
export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
export const MAX_EXCERPT_BYTES = 32 * 1024;
const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i;
export function parseObject(value: unknown): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return {};
}
return value as Record<string, unknown>;
}
export function asString(value: unknown, fallback: string): string {
return typeof value === "string" && value.length > 0 ? value : fallback;
}
export function asNumber(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
export function asBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
export function asStringArray(value: unknown): string[] {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
}
export function parseJson(value: string): Record<string, unknown> | null {
try {
return JSON.parse(value) as Record<string, unknown>;
} catch {
return null;
}
}
export function appendWithCap(prev: string, chunk: string, cap = MAX_CAPTURE_BYTES) {
const combined = prev + chunk;
return combined.length > cap ? combined.slice(combined.length - cap) : combined;
}
export function resolvePathValue(obj: Record<string, unknown>, dottedPath: string) {
const parts = dottedPath.split(".");
let cursor: unknown = obj;
for (const part of parts) {
if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) {
return "";
}
cursor = (cursor as Record<string, unknown>)[part];
}
if (cursor === null || cursor === undefined) return "";
if (typeof cursor === "string") return cursor;
if (typeof cursor === "number" || typeof cursor === "boolean") return String(cursor);
try {
return JSON.stringify(cursor);
} catch {
return "";
}
}
export function renderTemplate(template: string, data: Record<string, unknown>) {
return template.replace(/{{\s*([a-zA-Z0-9_.-]+)\s*}}/g, (_, path) => resolvePathValue(data, path));
}
/**
* Remove flags from `extraArgs` that the adapter will already add itself.
* Handles both `--flag value` pairs and `--flag=value` forms.
* This prevents duplicate/conflicting CLI arguments when users have shell
* aliases or wrapper functions that inject the same flags.
*/
export function filterConflictingArgs(extraArgs: string[], adapterFlags: string[]): string[] {
const flagSet = new Set(adapterFlags.map((f) => f.replace(/=.*$/, "")));
const filtered: string[] = [];
for (let i = 0; i < extraArgs.length; i++) {
const arg = extraArgs[i];
const bareFlag = arg.replace(/=.*$/, "");
if (flagSet.has(bareFlag)) {
// Skip this flag and its value if the value is a separate argument
if (!arg.includes("=") && i + 1 < extraArgs.length && !extraArgs[i + 1].startsWith("-")) {
i++; // skip the value argument too
}
continue;
}
filtered.push(arg);
}
return filtered;
}
export function redactEnvForLogs(env: Record<string, string>): Record<string, string> {
const redacted: Record<string, string> = {};
for (const [key, value] of Object.entries(env)) {
redacted[key] = SENSITIVE_ENV_KEY.test(key) ? "***REDACTED***" : value;
}
return redacted;
}
export function buildAgentCompanyEnv(agent: { id: string; companyId: string }): Record<string, string> {
const resolveHostForUrl = (rawHost: string): string => {
const host = rawHost.trim();
if (!host || host === "0.0.0.0" || host === "::") return "localhost";
if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) return `[${host}]`;
return host;
};
const vars: Record<string, string> = {
AGENT_COMPANY_AGENT_ID: agent.id,
AGENT_COMPANY_COMPANY_ID: agent.companyId,
};
const runtimeHost = resolveHostForUrl(
process.env.AGENT_COMPANY_LISTEN_HOST ?? process.env.HOST ?? "localhost",
);
const runtimePort = process.env.AGENT_COMPANY_LISTEN_PORT ?? process.env.PORT ?? "3100";
const apiUrl = process.env.AGENT_COMPANY_API_URL ?? `http://${runtimeHost}:${runtimePort}`;
vars.AGENT_COMPANY_API_URL = apiUrl;
return vars;
}
export function defaultPathForPlatform() {
if (process.platform === "win32") {
const home = process.env.USERPROFILE ?? process.env.HOME ?? "";
const appData = process.env.APPDATA ?? (home ? `${home}\\AppData\\Roaming` : "");
const localAppData = process.env.LOCALAPPDATA ?? (home ? `${home}\\AppData\\Local` : "");
const parts = [
"C:\\Windows\\System32",
"C:\\Windows",
"C:\\Windows\\System32\\Wbem",
appData ? `${appData}\\npm` : "",
localAppData ? `${localAppData}\\pnpm` : "",
home ? `${home}\\.local\\bin` : "",
home ? `${home}\\.cargo\\bin` : "",
"C:\\Program Files\\nodejs",
].filter(Boolean);
return parts.join(";");
}
return "/usr/local/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin";
}
@@ -1,394 +0,0 @@
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { constants as fsConstants, promises as fs } from "node:fs";
import path from "node:path";
import { StringDecoder } from "node:string_decoder";
import { appendWithCap, defaultPathForPlatform } from "./server-utils-parsers.js";
/**
* Decode a Buffer as GBK (CP936). Used as a fallback when UTF-8 decoding
* produces replacement characters on Chinese Windows systems where the
* system ANSI code page is GBK and chcp 65001 is ineffective for piped I/O.
*/
function decodeGbkBuffer(buf: Buffer): string {
try {
return new TextDecoder("gbk").decode(buf);
} catch {
return buf.toString("utf8");
}
}
export interface RunProcessResult {
exitCode: number | null;
signal: string | null;
timedOut: boolean;
stdout: string;
stderr: string;
}
interface RunningProcess {
child: ChildProcess;
graceSec: number;
}
interface SpawnTarget {
command: string;
args: string[];
}
const WINDOWS_CMD_SCRIPT_EXTENSIONS = new Set([".bat", ".cmd"]);
type ChildProcessWithEvents = ChildProcess & {
on(event: "error", listener: (err: Error) => void): ChildProcess;
on(
event: "close",
listener: (code: number | null, signal: NodeJS.Signals | null) => void,
): ChildProcess;
};
export const runningProcesses = new Map<string, RunningProcess>();
export function normalizeCommandValue(command: string): string {
let normalized = command.trim();
while (normalized.length >= 2) {
const quote = normalized[0];
if ((quote !== '"' && quote !== "'") || normalized.at(-1) !== quote) {
break;
}
normalized = normalized.slice(1, -1).trim();
}
return normalized;
}
function windowsPathExts(env: NodeJS.ProcessEnv): string[] {
return (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean);
}
async function pathExists(candidate: string) {
try {
await fs.access(candidate, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
return true;
} catch {
return false;
}
}
async function resolveCommandPath(command: string, cwd: string, env: NodeJS.ProcessEnv): Promise<string | null> {
const normalizedCommand = normalizeCommandValue(command);
const hasPathSeparator = normalizedCommand.includes("/") || normalizedCommand.includes("\\");
if (hasPathSeparator) {
const absolute = path.isAbsolute(normalizedCommand) ? normalizedCommand : path.resolve(cwd, normalizedCommand);
return (await pathExists(absolute)) ? absolute : null;
}
const pathValue = env.PATH ?? env.Path ?? "";
const delimiter = process.platform === "win32" ? ";" : ":";
const dirs = pathValue.split(delimiter).filter(Boolean);
const exts = process.platform === "win32" ? windowsPathExts(env) : [""];
const hasExtension = process.platform === "win32" && path.extname(command).length > 0;
for (const dir of dirs) {
const candidates =
process.platform === "win32"
? hasExtension
? [path.join(dir, normalizedCommand)]
: exts.map((ext) => path.join(dir, `${normalizedCommand}${ext}`))
: [path.join(dir, normalizedCommand)];
for (const candidate of candidates) {
if (await pathExists(candidate)) return candidate;
}
}
return null;
}
function quoteForCmd(arg: string) {
if (!arg.length) return '""';
const escaped = arg.replace(/"/g, '""');
return /[\s"&<>|^()]/.test(escaped) ? `"${escaped}"` : escaped;
}
export function buildWindowsSpawnTarget(
executable: string,
args: string[],
shell: string,
): SpawnTarget {
const extension = path.extname(executable).toLowerCase();
if (!WINDOWS_CMD_SCRIPT_EXTENSIONS.has(extension)) {
return { command: executable, args };
}
const commandLine = [quoteForCmd(executable), ...args.map(quoteForCmd)].join(" ");
return {
command: shell,
args: ["/d", "/c", `chcp 65001 >nul && ${commandLine}`],
};
}
export async function resolveSpawnTarget(
command: string,
args: string[],
cwd: string,
env: NodeJS.ProcessEnv,
): Promise<SpawnTarget> {
const normalizedCommand = normalizeCommandValue(command);
const resolved = await resolveCommandPath(normalizedCommand, cwd, env);
const executable = resolved ?? normalizedCommand;
if (process.platform !== "win32") {
return { command: executable, args };
}
// On Windows, ALL spawned processes need UTF-8 code page (65001) to prevent
// Chinese/CJK character corruption. The default Windows code page (e.g. 936/GBK)
// corrupts multi-byte UTF-8 characters passed through cmd.exe, curl, etc.
const shell = env.ComSpec || process.env.ComSpec || "cmd.exe";
return buildWindowsSpawnTarget(executable, args, shell);
}
export function ensurePathInEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
if (typeof env.PATH === "string" && env.PATH.length > 0) return env;
if (typeof env.Path === "string" && env.Path.length > 0) return env;
return { ...env, PATH: defaultPathForPlatform() };
}
export async function ensureAbsoluteDirectory(
cwd: string,
opts: { createIfMissing?: boolean } = {},
) {
if (!path.isAbsolute(cwd)) {
throw new Error(`Working directory must be an absolute path: "${cwd}"`);
}
const assertDirectory = async () => {
const stats = await fs.stat(cwd);
if (!stats.isDirectory()) {
throw new Error(`Working directory is not a directory: "${cwd}"`);
}
};
try {
await assertDirectory();
return;
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (!opts.createIfMissing || code !== "ENOENT") {
if (code === "ENOENT") {
throw new Error(`Working directory does not exist: "${cwd}"`);
}
throw err instanceof Error ? err : new Error(String(err));
}
}
try {
await fs.mkdir(cwd, { recursive: true });
await assertDirectory();
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
throw new Error(`Could not create working directory "${cwd}": ${reason}`);
}
}
export async function ensureCommandResolvable(command: string, cwd: string, env: NodeJS.ProcessEnv) {
const normalizedCommand = normalizeCommandValue(command);
const resolved = await resolveCommandPath(normalizedCommand, cwd, env);
if (resolved) return;
if (normalizedCommand.includes("/") || normalizedCommand.includes("\\")) {
const absolute = path.isAbsolute(normalizedCommand) ? normalizedCommand : path.resolve(cwd, normalizedCommand);
throw new Error(`Command is not executable: "${command}" (resolved: "${absolute}")`);
}
throw new Error(`Command not found in PATH: "${command}"`);
}
export async function runChildProcess(
runId: string,
command: string,
args: string[],
opts: {
cwd: string;
env: Record<string, string>;
timeoutSec: number;
graceSec: number;
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
onLogError?: (err: unknown, runId: string, message: string) => void;
stdin?: string;
},
): Promise<RunProcessResult> {
const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg));
return new Promise<RunProcessResult>((resolve, reject) => {
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".
// These vars leak in when the AgentCompany server itself is started from
// within a Claude Code session (e.g. `npx agentcompany run` in a terminal
// owned by Claude Code) or when cron inherits a contaminated shell env.
const CLAUDE_CODE_NESTING_VARS = [
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CLAUDE_CODE_SESSION",
"CLAUDE_CODE_PARENT_SESSION",
] as const;
for (const key of CLAUDE_CODE_NESTING_VARS) {
delete rawMerged[key];
}
const mergedEnv = ensurePathInEnv(rawMerged);
// On Windows, force UTF-8 encoding in child processes to prevent Chinese/CJK
// character corruption caused by the default system code page (e.g. 936/GBK).
if (process.platform === "win32") {
mergedEnv.PYTHONUTF8 = "1";
mergedEnv.PYTHONIOENCODING = "utf-8";
if (!mergedEnv.LANG) mergedEnv.LANG = "en_US.UTF-8";
}
void resolveSpawnTarget(command, args, opts.cwd, mergedEnv)
.then((target) => {
const child = spawn(target.command, target.args, {
cwd: opts.cwd,
env: mergedEnv,
shell: false,
windowsHide: true,
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
}) as ChildProcessWithEvents;
if (opts.stdin != null && child.stdin) {
child.stdin.write(opts.stdin);
child.stdin.end();
}
runningProcesses.set(runId, { child, graceSec: opts.graceSec });
let timedOut = false;
let stdout = "";
let stderr = "";
let logChain: Promise<void> = Promise.resolve();
// Use StringDecoder to handle multi-byte characters split across chunks.
// On Windows, child process stdout may arrive in GBK (CP936) instead
// of UTF-8 when chcp 65001 is ineffective (piped stdout has no console).
// We detect this per-stream and fall back to GBK decoding if needed.
const stdoutDecoder = new StringDecoder("utf8");
const stderrDecoder = new StringDecoder("utf8");
let stdoutIsGbk = false;
let stderrIsGbk = false;
const timeout =
opts.timeoutSec > 0
? setTimeout(() => {
timedOut = true;
child.kill("SIGTERM");
setTimeout(() => {
if (!child.killed) {
child.kill("SIGKILL");
}
}, Math.max(1, opts.graceSec) * 1000);
}, opts.timeoutSec * 1000)
: null;
child.stdout?.on("data", (chunk: unknown) => {
let text: string;
if (chunk instanceof Buffer) {
if (stdoutIsGbk) {
text = decodeGbkBuffer(chunk);
} else {
text = stdoutDecoder.write(chunk);
if (text.includes("\ufffd") && chunk.length > 0) {
stdoutIsGbk = true;
text = decodeGbkBuffer(chunk);
}
}
} else {
text = String(chunk);
}
stdout = appendWithCap(stdout, text);
logChain = logChain
.then(() => opts.onLog("stdout", text))
.catch((err) => onLogError(err, runId, "failed to append stdout log chunk"));
});
child.stderr?.on("data", (chunk: unknown) => {
let text: string;
if (chunk instanceof Buffer) {
if (stderrIsGbk) {
text = decodeGbkBuffer(chunk);
} else {
text = stderrDecoder.write(chunk);
if (text.includes("\ufffd") && chunk.length > 0) {
stderrIsGbk = true;
text = decodeGbkBuffer(chunk);
}
}
} else {
text = String(chunk);
}
stderr = appendWithCap(stderr, text);
logChain = logChain
.then(() => opts.onLog("stderr", text))
.catch((err) => onLogError(err, runId, "failed to append stderr log chunk"));
});
child.on("error", (err: Error) => {
if (timeout) clearTimeout(timeout);
runningProcesses.delete(runId);
const errno = (err as NodeJS.ErrnoException).code;
const pathValue = mergedEnv.PATH ?? mergedEnv.Path ?? "";
const msg =
errno === "ENOENT"
? `Failed to start command "${command}" in "${opts.cwd}". Verify adapter command, working directory, and PATH (${pathValue}).`
: `Failed to start command "${command}" in "${opts.cwd}": ${err.message}`;
reject(new Error(msg));
});
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (timeout) clearTimeout(timeout);
runningProcesses.delete(runId);
// Flush any remaining partial multi-byte characters from the decoders.
const stdoutTail = stdoutDecoder.end();
const stderrTail = stderrDecoder.end();
if (stdoutTail) stdout = appendWithCap(stdout, stdoutTail);
if (stderrTail) stderr = appendWithCap(stderr, stderrTail);
void logChain.finally(() => {
resolve({
exitCode: code,
signal,
timedOut,
stdout,
stderr,
});
});
});
})
.catch(reject);
});
}
/**
* Kill a process and all its descendants.
* Windows: `taskkill /T /F` kills the full process tree.
* Unix: SIGKILL the process directly (CLI tools are leaf processes).
*/
function killProcessTree(pid: number): void {
if (process.platform === "win32") {
try {
spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], { stdio: "ignore", timeout: 5000 });
} catch { /* process already exited */ }
} else {
try {
process.kill(pid, "SIGKILL");
} catch { /* already dead */ }
}
}
/**
* Force-kill every child process tracked in `runningProcesses` and clear the map.
* Called by the worker during shutdown to ensure no CLI processes survive app quit.
*/
export function shutdownAllRunning(): void {
for (const [runId, entry] of runningProcesses) {
const pid = entry.child.pid;
if (pid != null) {
killProcessTree(pid);
}
runningProcesses.delete(runId);
}
}
@@ -1,305 +0,0 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import type { AdapterSkillEntry, AdapterSkillSnapshot } from "./types.js";
import type { AgentCompanySkillEntry, InstalledSkillTarget } from "./server-utils-skills.js";
interface PersistentSkillSnapshotOptions {
adapterType: string;
availableEntries: AgentCompanySkillEntry[];
desiredSkills: string[];
installed: Map<string, InstalledSkillTarget>;
skillsHome: string;
locationLabel?: string | null;
installedDetail?: string | null;
missingDetail: string;
externalConflictDetail: string;
externalDetail: string;
warnings?: string[];
}
function normalizePathSlashes(value: string): string {
return value.replaceAll("\\", "/");
}
function isMaintainerOnlySkillTarget(candidate: string): boolean {
return normalizePathSlashes(candidate).includes("/.agents/skills/");
}
function skillLocationLabel(value: string | null | undefined): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function buildManagedSkillOrigin(entry: { required?: boolean }): Pick<
AdapterSkillEntry,
"origin" | "originLabel" | "readOnly"
> {
if (entry.required) {
return {
origin: "paperclip_required",
originLabel: "Required by AgentCompany",
readOnly: false,
};
}
return {
origin: "company_managed",
originLabel: "Managed by AgentCompany",
readOnly: false,
};
}
function canonicalizeDesiredAgentCompanySkillReference(
reference: string,
availableEntries: Array<{ key: string; runtimeName?: string | null }>,
): string {
const normalizedReference = reference.trim().toLowerCase();
if (!normalizedReference) return "";
const exactKey = availableEntries.find((entry) => entry.key.trim().toLowerCase() === normalizedReference);
if (exactKey) return exactKey.key;
const byRuntimeName = availableEntries.filter((entry) =>
typeof entry.runtimeName === "string" && entry.runtimeName.trim().toLowerCase() === normalizedReference,
);
if (byRuntimeName.length === 1) return byRuntimeName[0]!.key;
const slugMatches = availableEntries.filter((entry) =>
entry.key.trim().toLowerCase().split("/").pop() === normalizedReference,
);
if (slugMatches.length === 1) return slugMatches[0]!.key;
return normalizedReference;
}
/**
* Build an AdapterSkillSnapshot for persistent-mode adapters (Gemini, Codex).
* Compares available entries against installed state and desired skill list.
*/
export function buildPersistentSkillSnapshot(
options: PersistentSkillSnapshotOptions,
): AdapterSkillSnapshot {
const {
adapterType,
availableEntries,
desiredSkills,
installed,
skillsHome,
locationLabel,
installedDetail,
missingDetail,
externalConflictDetail,
externalDetail,
} = options;
const availableByKey = new Map(availableEntries.map((entry) => [entry.key, entry]));
const desiredSet = new Set(desiredSkills);
const entries: AdapterSkillEntry[] = [];
const warnings = [...(options.warnings ?? [])];
for (const available of availableEntries) {
const installedEntry = installed.get(available.runtimeName) ?? null;
const desired = desiredSet.has(available.key);
let state: AdapterSkillEntry["state"] = "available";
let managed = false;
let detail: string | null = null;
if (installedEntry?.targetPath === available.source) {
managed = true;
state = desired ? "installed" : "stale";
detail = installedDetail ?? null;
} else if (installedEntry) {
state = "external";
detail = desired ? externalConflictDetail : externalDetail;
} else if (desired) {
state = "missing";
detail = missingDetail;
}
entries.push({
key: available.key,
runtimeName: available.runtimeName,
desired,
managed,
state,
sourcePath: available.source,
targetPath: path.join(skillsHome, available.runtimeName),
detail,
required: Boolean(available.required),
requiredReason: available.requiredReason ?? null,
...buildManagedSkillOrigin(available),
});
}
for (const desiredSkill of desiredSkills) {
if (availableByKey.has(desiredSkill)) continue;
warnings.push(`Desired skill "${desiredSkill}" is not available from the AgentCompany skills directory.`);
entries.push({
key: desiredSkill,
runtimeName: null,
desired: true,
managed: true,
state: "missing",
sourcePath: null,
targetPath: null,
detail: "AgentCompany cannot find this skill in the local runtime skills directory.",
origin: "external_unknown",
originLabel: "External or unavailable",
readOnly: false,
});
}
for (const [name, installedEntry] of installed.entries()) {
if (availableEntries.some((entry) => entry.runtimeName === name)) continue;
entries.push({
key: name,
runtimeName: name,
desired: false,
managed: false,
state: "external",
origin: "user_installed",
originLabel: "User-installed",
locationLabel: skillLocationLabel(locationLabel),
readOnly: true,
sourcePath: null,
targetPath: installedEntry.targetPath ?? path.join(skillsHome, name),
detail: externalDetail,
});
}
entries.sort((left, right) => left.key.localeCompare(right.key));
return {
adapterType,
supported: true,
mode: "persistent",
desiredSkills,
entries,
warnings,
};
}
/**
* Read the desired skill sync preference from adapter config.
*/
export function readAgentCompanySkillSyncPreference(config: Record<string, unknown>): {
explicit: boolean;
desiredSkills: string[];
} {
const raw = config.agentCompanySkillSync;
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
return { explicit: false, desiredSkills: [] };
}
const syncConfig = raw as Record<string, unknown>;
const desiredValues = syncConfig.desiredSkills;
const desired = Array.isArray(desiredValues)
? desiredValues
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean)
: [];
return {
explicit: Object.prototype.hasOwnProperty.call(raw, "desiredSkills"),
desiredSkills: Array.from(new Set(desired)),
};
}
/**
* Resolve the canonical desired skill list from config and available entries.
* Required skills are always included; explicit preferences are canonicalized.
*/
export function resolveAgentCompanyDesiredSkillNames(
config: Record<string, unknown>,
availableEntries: Array<{ key: string; runtimeName?: string | null; required?: boolean }>,
): string[] {
const preference = readAgentCompanySkillSyncPreference(config);
const requiredSkills = availableEntries
.filter((entry) => entry.required)
.map((entry) => entry.key);
if (!preference.explicit) {
return Array.from(new Set(requiredSkills));
}
const desiredSkills = preference.desiredSkills
.map((reference) => canonicalizeDesiredAgentCompanySkillReference(reference, availableEntries))
.filter(Boolean);
return Array.from(new Set([...requiredSkills, ...desiredSkills]));
}
/**
* Create or repair a symlink from source to target skill directory.
* Returns the action taken: "created", "repaired", or "skipped".
*/
export async function ensureAgentCompanySkillSymlink(
source: string,
target: string,
linkSkill: (source: string, target: string) => Promise<void> = (linkSource, linkTarget) =>
fs.symlink(linkSource, linkTarget),
): Promise<"created" | "repaired" | "skipped"> {
const existing = await fs.lstat(target).catch(() => null);
if (!existing) {
await linkSkill(source, target);
return "created";
}
if (!existing.isSymbolicLink()) {
return "skipped";
}
const linkedPath = await fs.readlink(target).catch(() => null);
if (!linkedPath) return "skipped";
const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
if (resolvedLinkedPath === source) {
return "skipped";
}
const linkedPathExists = await fs.stat(resolvedLinkedPath).then(() => true).catch(() => false);
if (linkedPathExists) {
return "skipped";
}
await fs.unlink(target);
await linkSkill(source, target);
return "repaired";
}
/**
* Remove symlinks in skillsHome that point to maintainer-only paths
* (.agents/skills/) and are not in the allowed set.
*/
export async function removeMaintainerOnlySkillSymlinks(
skillsHome: string,
allowedSkillNames: Iterable<string>,
): Promise<string[]> {
const allowed = new Set(Array.from(allowedSkillNames));
try {
const entries = await fs.readdir(skillsHome, { withFileTypes: true });
const removed: string[] = [];
for (const entry of entries) {
if (allowed.has(entry.name)) continue;
const target = path.join(skillsHome, entry.name);
const existing = await fs.lstat(target).catch(() => null);
if (!existing?.isSymbolicLink()) continue;
const linkedPath = await fs.readlink(target).catch(() => null);
if (!linkedPath) continue;
const resolvedLinkedPath = path.isAbsolute(linkedPath)
? linkedPath
: path.resolve(path.dirname(target), linkedPath);
if (
!isMaintainerOnlySkillTarget(linkedPath) &&
!isMaintainerOnlySkillTarget(resolvedLinkedPath)
) {
continue;
}
await fs.unlink(target);
removed.push(entry.name);
}
return removed;
} catch {
return [];
}
}
@@ -1,176 +0,0 @@
import { promises as fs, type Dirent } from "node:fs";
import path from "node:path";
import type { ManagedSkillRef } from "./types.js";
import { parseObject, asString, asBoolean } from "./server-utils-parsers.js";
/**
* Create symlinks for managed skills in the target directory.
* Per D-13: uses 'junction' type on Windows to avoid Developer Mode requirement.
* Individual skill symlink failures are logged and reported but do not block other skills.
*/
export async function createManagedSkillSymlinks(
managedSkills: ManagedSkillRef[],
targetDir: string,
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>,
): Promise<void> {
await fs.mkdir(targetDir, { recursive: true });
const symlinkType = process.platform === "win32" ? ("junction" as const) : undefined;
for (const skill of managedSkills) {
const target = path.join(targetDir, skill.runtimeName);
const existing = await fs.lstat(target).catch(() => null);
if (existing) {
await fs.rm(target, { recursive: true, force: true });
}
try {
await fs.symlink(skill.sourcePath, target, symlinkType);
await onLog("stderr", `[agent-company] Injected managed skill "${skill.runtimeName}"\n`);
} catch (err) {
await onLog(
"stderr",
`[agent-company] Failed to inject skill "${skill.runtimeName}": ${err instanceof Error ? err.message : String(err)}\n`,
);
}
}
}
// ---------------------------------------------------------------------------
// Skill resolution helpers -- ported from Paperclip reference with
// AgentCompany naming. These utilities let each adapter discover, list,
// resolve, and synchronize skills from a shared skills directory.
// ---------------------------------------------------------------------------
export const AGENTCOMPANY_SKILL_ROOT_RELATIVE_CANDIDATES = [
"../../skills",
"../../../../../skills",
];
export interface AgentCompanySkillEntry {
key: string;
runtimeName: string;
source: string;
required?: boolean;
requiredReason?: string | null;
}
export interface InstalledSkillTarget {
targetPath: string | null;
kind: "symlink" | "directory" | "file";
}
export function resolveInstalledEntryTarget(
skillsHome: string,
entryName: string,
dirent: Dirent,
linkedPath: string | null,
): InstalledSkillTarget {
const fullPath = path.join(skillsHome, entryName);
if (dirent.isSymbolicLink()) {
return {
targetPath: linkedPath ? path.resolve(path.dirname(fullPath), linkedPath) : null,
kind: "symlink",
};
}
if (dirent.isDirectory()) {
return { targetPath: fullPath, kind: "directory" };
}
return { targetPath: fullPath, kind: "file" };
}
export function normalizeConfiguredAgentCompanyRuntimeSkills(value: unknown): AgentCompanySkillEntry[] {
if (!Array.isArray(value)) return [];
const out: AgentCompanySkillEntry[] = [];
for (const rawEntry of value) {
const entry = parseObject(rawEntry);
const key = asString(entry.key, asString(entry.name, "")).trim();
const runtimeName = asString(entry.runtimeName, asString(entry.name, "")).trim();
const source = asString(entry.source, "").trim();
if (!key || !runtimeName || !source) continue;
out.push({
key,
runtimeName,
source,
required: asBoolean(entry.required, false),
requiredReason:
typeof entry.requiredReason === "string" && entry.requiredReason.trim().length > 0
? entry.requiredReason.trim()
: null,
});
}
return out;
}
/**
* Resolve the AgentCompany skills root directory from relative path candidates.
*/
export async function resolveAgentCompanySkillsDir(
moduleDir: string,
additionalCandidates: string[] = [],
): Promise<string | null> {
const candidates = [
...AGENTCOMPANY_SKILL_ROOT_RELATIVE_CANDIDATES.map((relativePath) => path.resolve(moduleDir, relativePath)),
...additionalCandidates.map((candidate) => path.resolve(candidate)),
];
const seenRoots = new Set<string>();
for (const root of candidates) {
if (seenRoots.has(root)) continue;
seenRoots.add(root);
const isDirectory = await fs.stat(root).then((stats) => stats.isDirectory()).catch(() => false);
if (isDirectory) return root;
}
return null;
}
/**
* List skill entries from the AgentCompany skills directory.
*/
export async function listAgentCompanySkillEntries(
moduleDir: string,
additionalCandidates: string[] = [],
): Promise<AgentCompanySkillEntry[]> {
const root = await resolveAgentCompanySkillsDir(moduleDir, additionalCandidates);
if (!root) return [];
try {
const entries = await fs.readdir(root, { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => ({
key: `agentcompany/agentcompany/${entry.name}`,
runtimeName: entry.name,
source: path.join(root, entry.name),
required: true,
requiredReason: "Bundled AgentCompany skills are always available for local adapters.",
}));
} catch {
return [];
}
}
/**
* Read installed skill targets from a skill home directory (symlinks, dirs, files).
*/
export async function readInstalledSkillTargets(skillsHome: string): Promise<Map<string, InstalledSkillTarget>> {
const entries = await fs.readdir(skillsHome, { withFileTypes: true }).catch(() => []);
const out = new Map<string, InstalledSkillTarget>();
for (const entry of entries) {
const fullPath = path.join(skillsHome, entry.name);
const linkedPath = entry.isSymbolicLink() ? await fs.readlink(fullPath).catch(() => null) : null;
out.set(entry.name, resolveInstalledEntryTarget(skillsHome, entry.name, entry, linkedPath));
}
return out;
}
/**
* Read runtime skill entries from adapter config or discover from module directory.
*/
export async function readAgentCompanyRuntimeSkillEntries(
config: Record<string, unknown>,
moduleDir: string,
additionalCandidates: string[] = [],
): Promise<AgentCompanySkillEntry[]> {
const configuredEntries = normalizeConfiguredAgentCompanyRuntimeSkills(config.agentCompanyRuntimeSkills);
if (configuredEntries.length > 0) return configuredEntries;
return listAgentCompanySkillEntries(moduleDir, additionalCandidates);
}
@@ -1,4 +0,0 @@
export * from "./server-utils-parsers.js";
export * from "./server-utils-process.js";
export * from "./server-utils-skills.js";
export * from "./server-utils-skills-sync.js";
@@ -1,61 +0,0 @@
import type { AdapterSessionCodec } from "./types.js";
function readNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
interface SessionCodecOptions {
/** Additional key aliases for sessionId beyond "sessionId" and "session_id" (e.g. "sessionID" for Gemini/OpenCode). */
extraSessionIdKeys?: string[];
}
/**
* Creates a standard session codec shared across adapters.
* All adapters use the same serialize/deserialize/getDisplayId logic,
* differing only in which keys they accept for sessionId.
*/
export function createStandardSessionCodec(options?: SessionCodecOptions): AdapterSessionCodec {
const extraKeys = options?.extraSessionIdKeys ?? [];
function readSessionId(record: Record<string, unknown>): string | null {
return (
readNonEmptyString(record.sessionId) ??
readNonEmptyString(record.session_id) ??
extraKeys.reduce<string | null>((found, key) => found ?? readNonEmptyString(record[key]), null)
);
}
function extractFields(record: Record<string, unknown>) {
const sessionId = readSessionId(record);
if (!sessionId) return null;
const cwd =
readNonEmptyString(record.cwd) ??
readNonEmptyString(record.workdir) ??
readNonEmptyString(record.folder);
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
return {
sessionId,
...(cwd ? { cwd } : {}),
...(workspaceId ? { workspaceId } : {}),
...(repoUrl ? { repoUrl } : {}),
...(repoRef ? { repoRef } : {}),
};
}
return {
deserialize(raw: unknown) {
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
return extractFields(raw as Record<string, unknown>);
},
serialize(params: Record<string, unknown> | null) {
if (!params) return null;
return extractFields(params);
},
getDisplayId(params: Record<string, unknown> | null) {
if (!params) return null;
return readSessionId(params);
},
};
}
-37
View File
@@ -1,37 +0,0 @@
import path from "node:path";
import type { AdapterEnvironmentCheck, AdapterEnvironmentTestResult } from "./types.js";
import { normalizeCommandValue } from "./server-utils-process.js";
export function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
if (checks.some((check) => check.level === "error")) return "fail";
if (checks.some((check) => check.level === "warn")) return "warn";
return "pass";
}
export function isNonEmpty(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
export function firstNonEmptyLine(text: string): string {
return (
text
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean) ?? ""
);
}
export function commandLooksLike(command: string, expected: string): boolean {
const normalized = normalizeCommandValue(command).replace(/\\/g, "/");
const base = path.posix.basename(normalized).toLowerCase();
return base === expected || base === `${expected}.cmd` || base === `${expected}.exe`;
}
export function summarizeProbeDetail(stdout: string, stderr: string, parsedError?: string | null): string | null {
const raw = parsedError?.trim() || firstNonEmptyLine(stderr) || firstNonEmptyLine(stdout);
if (!raw) return null;
const clean = raw.replace(/\s+/g, " ").trim();
const max = 240;
return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean;
}
-291
View File
@@ -1,291 +0,0 @@
export interface AdapterAgent {
id: string;
companyId: string;
name: string;
adapterType: string | null;
adapterConfig: unknown;
}
export interface AdapterRuntime {
/**
* Legacy single session id view. Prefer `sessionParams` + `sessionDisplayId`.
*/
sessionId: string | null;
sessionParams: Record<string, unknown> | null;
sessionDisplayId: string | null;
taskKey: string | null;
}
export interface UsageSummary {
inputTokens: number;
outputTokens: number;
cachedInputTokens?: number;
}
export type AdapterBillingType = "api" | "subscription" | "unknown";
export interface AdapterRuntimeServiceReport {
id?: string | null;
projectId?: string | null;
projectWorkspaceId?: string | null;
issueId?: string | null;
scopeType?: "project_workspace" | "execution_workspace" | "run" | "agent";
scopeId?: string | null;
serviceName: string;
status?: "starting" | "running" | "stopped" | "failed";
lifecycle?: "shared" | "ephemeral";
reuseKey?: string | null;
command?: string | null;
cwd?: string | null;
port?: number | null;
url?: string | null;
providerRef?: string | null;
ownerAgentId?: string | null;
stopPolicy?: Record<string, unknown> | null;
healthStatus?: "unknown" | "healthy" | "unhealthy";
}
export interface AdapterExecutionResult {
exitCode: number | null;
signal: string | null;
timedOut: boolean;
errorMessage?: string | null;
errorCode?: string | null;
errorMeta?: Record<string, unknown>;
usage?: UsageSummary;
/**
* Legacy single session id output. Prefer `sessionParams` + `sessionDisplayId`.
*/
sessionId?: string | null;
sessionParams?: Record<string, unknown> | null;
sessionDisplayId?: string | null;
provider?: string | null;
model?: string | null;
billingType?: AdapterBillingType | null;
costUsd?: number | null;
resultJson?: Record<string, unknown> | null;
runtimeServices?: AdapterRuntimeServiceReport[];
summary?: string | null;
clearSession?: boolean;
question?: {
prompt: string;
choices: Array<{
key: string;
label: string;
description?: string;
}>;
} | null;
}
export interface AdapterSessionCodec {
deserialize(raw: unknown): Record<string, unknown> | null;
serialize(params: Record<string, unknown> | null): Record<string, unknown> | null;
getDisplayId?: (params: Record<string, unknown> | null) => string | null;
}
export interface AdapterInvocationMeta {
adapterType: string;
command: string;
cwd?: string;
commandArgs?: string[];
commandNotes?: string[];
env?: Record<string, string>;
prompt?: string;
context?: Record<string, unknown>;
}
export interface AdapterExecutionContext {
runId: string;
agent: AdapterAgent;
runtime: AdapterRuntime;
config: Record<string, unknown>;
context: Record<string, unknown>;
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
onMeta?: (meta: AdapterInvocationMeta) => Promise<void>;
authToken?: string;
}
export interface AdapterModel {
id: string;
label: string;
}
export type AdapterEnvironmentCheckLevel = "info" | "warn" | "error";
export interface AdapterEnvironmentCheck {
code: string;
level: AdapterEnvironmentCheckLevel;
message: string;
detail?: string | null;
hint?: string | null;
}
export type AdapterEnvironmentTestStatus = "pass" | "warn" | "fail";
export interface AdapterEnvironmentTestResult {
adapterType: string;
status: AdapterEnvironmentTestStatus;
checks: AdapterEnvironmentCheck[];
testedAt: string;
}
export type AdapterSkillSyncMode = "unsupported" | "persistent" | "ephemeral";
export type AdapterSkillState =
| "available"
| "configured"
| "installed"
| "missing"
| "stale"
| "external";
export type AdapterSkillOrigin =
| "company_managed"
| "paperclip_required"
| "user_installed"
| "external_unknown";
export interface AdapterSkillEntry {
key: string;
runtimeName: string | null;
desired: boolean;
managed: boolean;
required?: boolean;
requiredReason?: string | null;
state: AdapterSkillState;
origin?: AdapterSkillOrigin;
originLabel?: string | null;
locationLabel?: string | null;
readOnly?: boolean;
sourcePath?: string | null;
targetPath?: string | null;
detail?: string | null;
}
export interface AdapterSkillSnapshot {
adapterType: string;
supported: boolean;
mode: AdapterSkillSyncMode;
desiredSkills: string[];
entries: AdapterSkillEntry[];
warnings: string[];
}
export interface AdapterSkillContext {
agentId: string;
companyId: string;
adapterType: string;
config: Record<string, unknown>;
}
export interface AdapterEnvironmentTestContext {
companyId: string;
adapterType: string;
config: Record<string, unknown>;
deployment?: {
mode?: "local_trusted" | "authenticated";
exposure?: "private" | "public";
bindHost?: string | null;
allowedHostnames?: string[];
};
}
/** Payload for the onHireApproved adapter lifecycle hook (e.g. join-request or hire_agent approval). */
export interface HireApprovedPayload {
companyId: string;
agentId: string;
agentName: string;
adapterType: string;
/** "join_request" | "approval" */
source: "join_request" | "approval";
sourceId: string;
approvedAt: string;
/** Canonical operator-facing message for cloud adapters to show the user. */
message: string;
}
/** Result of onHireApproved hook; failures are non-fatal to the approval flow. */
export interface HireApprovedHookResult {
ok: boolean;
error?: string;
detail?: Record<string, unknown>;
}
export interface ServerAdapterModule {
type: string;
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
testEnvironment(ctx: AdapterEnvironmentTestContext): Promise<AdapterEnvironmentTestResult>;
listSkills?: (ctx: AdapterSkillContext) => Promise<AdapterSkillSnapshot>;
syncSkills?: (ctx: AdapterSkillContext, desiredSkills: string[]) => Promise<AdapterSkillSnapshot>;
sessionCodec?: AdapterSessionCodec;
supportsLocalAgentJwt?: boolean;
models?: AdapterModel[];
listModels?: () => Promise<AdapterModel[]>;
agentConfigurationDoc?: string;
/** Per D-04: If true, adapter manages its own context window (e.g. Claude, Codex). Session rotation is skipped but usage is still tracked. */
nativeContextManagement?: boolean;
/**
* Optional lifecycle hook when an agent is approved/hired (join-request or hire_agent approval).
* adapterConfig is the agent's adapter config so the adapter can e.g. send a callback to a configured URL.
*/
onHireApproved?: (
payload: HireApprovedPayload,
adapterConfig: Record<string, unknown>,
) => Promise<HireApprovedHookResult>;
}
export type TranscriptEntry =
| { kind: "assistant"; ts: string; text: string; delta?: boolean }
| { kind: "thinking"; ts: string; text: string; delta?: boolean }
| { kind: "user"; ts: string; text: string }
| { kind: "tool_call"; ts: string; name: string; input: unknown; toolUseId?: string }
| { kind: "tool_result"; ts: string; toolUseId: string; content: string; isError: boolean }
| { kind: "init"; ts: string; model: string; sessionId: string }
| { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] }
| { kind: "stderr"; ts: string; text: string }
| { kind: "system"; ts: string; text: string }
| { kind: "stdout"; ts: string; text: string };
export type StdoutLineParser = (line: string, ts: string) => TranscriptEntry[];
export interface CLIAdapterModule {
type: string;
formatStdoutEvent: (line: string, debug: boolean) => void;
}
/** Skill reference passed from main process to worker for adapter injection. */
export interface ManagedSkillRef {
/** Skill slug used as directory name in the adapter's skill directory */
runtimeName: string;
/** Absolute path to the materialized skill directory on disk */
sourcePath: string;
}
export interface CreateConfigValues {
adapterType: string;
cwd: string;
instructionsFilePath?: string;
promptTemplate: string;
model: string;
thinkingEffort: string;
chrome: boolean;
dangerouslySkipPermissions: boolean;
search: boolean;
dangerouslyBypassSandbox: boolean;
command: string;
args: string;
extraArgs: string;
envVars: string;
envBindings: Record<string, unknown>;
url: string;
bootstrapPrompt: string;
payloadTemplateJson?: string;
workspaceStrategyType?: string;
workspaceBaseRef?: string;
workspaceBranchTemplate?: string;
worktreeParentDir?: string;
runtimeServicesJson?: string;
maxTurnsPerRun: number;
heartbeatEnabled: boolean;
intervalSec: number;
}
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
+2063 -98
View File
File diff suppressed because it is too large. Load diff