mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-17 12:33:24 +00:00
Add WorkBuddy app integration
This commit is contained in:
parent
0d178f3732
commit
c8e5e2b53d
20 changed files with 866 additions and 82 deletions
|
|
@ -16,7 +16,7 @@ export type CodexAppLookupResult = {
|
|||
executable?: string;
|
||||
};
|
||||
|
||||
type CodexCompatibleAppKind = "codex" | "zcode";
|
||||
type CodexCompatibleAppKind = "codex" | "workbuddy" | "zcode";
|
||||
|
||||
type CodexCompatibleAppSpec = {
|
||||
bundledCliNames: string[];
|
||||
|
|
@ -49,9 +49,31 @@ export type CodexCompatibleAppModelCatalogWriteResult = {
|
|||
changed: boolean;
|
||||
file: string;
|
||||
userDataDir: string;
|
||||
workbuddyModelsConfig?: WorkbuddyModelsConfigWriteResult;
|
||||
workbuddyVirtualAuth?: WorkbuddyVirtualAuthResult;
|
||||
};
|
||||
|
||||
export type WorkbuddyModelsConfigWriteResult = {
|
||||
changed: boolean;
|
||||
file: string;
|
||||
model: string;
|
||||
};
|
||||
|
||||
export type WorkbuddyVirtualAuthResult = {
|
||||
authFile: string;
|
||||
changed: boolean;
|
||||
configDir: string;
|
||||
homeDir: string;
|
||||
userDataDir: string;
|
||||
};
|
||||
|
||||
export const codexDesktopAppName = "ChatGPT";
|
||||
export const workbuddyDesktopAppName = "WorkBuddy AI";
|
||||
|
||||
const workbuddyVirtualAuthId = "workbuddy-desktop-ai";
|
||||
const workbuddyVirtualAuthUserId = "ccr-local-profile";
|
||||
const workbuddyVirtualAuthToken = "ccr-local-profile";
|
||||
const workbuddyVirtualAuthExpiresAt = 1_999_999_999_999;
|
||||
|
||||
const codexAppSpec: CodexCompatibleAppSpec = {
|
||||
bundledCliNames: ["codex", "Codex", "OpenAI Codex"],
|
||||
|
|
@ -157,6 +179,56 @@ const zcodeAppSpec: CodexCompatibleAppSpec = {
|
|||
]
|
||||
};
|
||||
|
||||
const workbuddyAppSpec: CodexCompatibleAppSpec = {
|
||||
bundledCliNames: [
|
||||
"app.asar.unpacked/cli/bin/codebuddy",
|
||||
"app.asar.unpacked/cli/bin/cbc",
|
||||
"app.asar.unpacked/cli/bin/cbc-prewarm",
|
||||
"codebuddy",
|
||||
"CodeBuddy",
|
||||
"workbuddy",
|
||||
"WorkBuddy"
|
||||
],
|
||||
defaultCliCommand: "codebuddy",
|
||||
displayName: workbuddyDesktopAppName,
|
||||
envPathKeys: ["CCR_WORKBUDDY_APP_PATH", "WORKBUDDY_APP_PATH", "CODEXL_WORKBUDDY_PATH"],
|
||||
kind: "workbuddy",
|
||||
linuxCandidates: [
|
||||
"/opt/WorkBuddy AI/workbuddy-ai",
|
||||
"/opt/WorkBuddy AI/WorkBuddyAI",
|
||||
"/opt/WorkBuddy/workbuddy",
|
||||
"/opt/WorkBuddy/WorkBuddy",
|
||||
"/usr/local/bin/workbuddy-ai",
|
||||
"/usr/bin/workbuddy-ai",
|
||||
"/usr/local/bin/workbuddy",
|
||||
"/usr/bin/workbuddy"
|
||||
],
|
||||
macAppNames: ["WorkBuddy AI.app", "WorkBuddy.app", "WorkBuddyAI.app", "Workbuddy.app"],
|
||||
modelCatalogFilename: "ccr-workbuddy-model-catalog.json",
|
||||
userDataDirName: "workbuddy-app-user-data",
|
||||
windowsAppDirs: ["WorkBuddy AI", "WorkBuddyAI", "WorkBuddy", "Workbuddy", "CodeBuddy"],
|
||||
windowsExeNames: [
|
||||
"WorkBuddy AI.exe",
|
||||
"WorkBuddyAI.exe",
|
||||
"WorkBuddy.exe",
|
||||
"workbuddy-ai.exe",
|
||||
"workbuddy.exe",
|
||||
"CodeBuddy.exe",
|
||||
"codebuddy.exe"
|
||||
],
|
||||
windowsPackageKeywords: ["workbuddy", "workbuddyai", "workbuddy-ai", "codebuddy"],
|
||||
windowsVendorDirs: ["WorkBuddy", "WorkBuddy AI", "CodeBuddy"],
|
||||
windowsWhereNames: [
|
||||
"WorkBuddy AI",
|
||||
"WorkBuddyAI",
|
||||
"WorkBuddy",
|
||||
"workbuddy-ai",
|
||||
"workbuddy",
|
||||
"CodeBuddy",
|
||||
"codebuddy"
|
||||
]
|
||||
};
|
||||
|
||||
export function launchCodexAppProfile(configDir: string, profile: ProfileConfig, config?: AppConfig): CodexAppLaunchResult {
|
||||
return launchCodexCompatibleAppProfile(configDir, profile, codexAppSpec, config);
|
||||
}
|
||||
|
|
@ -173,12 +245,26 @@ export function launchZcodeAppProfile(configDir: string, profile: ProfileConfig,
|
|||
return launchCodexCompatibleAppProfile(configDir, profile, zcodeAppSpec, config);
|
||||
}
|
||||
|
||||
export function findInstalledWorkbuddyAppExecutable(profileAppPath?: string): CodexAppLookupResult {
|
||||
return findInstalledCodexCompatibleAppExecutable(workbuddyAppSpec, profileAppPath);
|
||||
}
|
||||
|
||||
export function launchWorkbuddyAppProfile(configDir: string, profile: ProfileConfig, config?: AppConfig): CodexAppLaunchResult {
|
||||
return launchCodexCompatibleAppProfile(configDir, profile, workbuddyAppSpec, config);
|
||||
}
|
||||
|
||||
export function refreshCodexCompatibleAppProfileFiles(
|
||||
configDir: string,
|
||||
profile: ProfileConfig,
|
||||
config?: AppConfig
|
||||
): { modelCatalogChanged: boolean; modelCatalogFile: string; userDataDir: string } {
|
||||
const spec = profile.agent === "zcode" ? zcodeAppSpec : codexAppSpec;
|
||||
): {
|
||||
modelCatalogChanged: boolean;
|
||||
modelCatalogFile: string;
|
||||
userDataDir: string;
|
||||
workbuddyModelsConfig?: WorkbuddyModelsConfigWriteResult;
|
||||
workbuddyVirtualAuth?: WorkbuddyVirtualAuthResult;
|
||||
} {
|
||||
const spec = codexCompatibleAppSpecForProfile(profile);
|
||||
if (spec.kind === "zcode" && config?.APIKEY) {
|
||||
writeZcodeGatewayConfig(config, profile, config.APIKEY, { backup: false });
|
||||
}
|
||||
|
|
@ -186,7 +272,9 @@ export function refreshCodexCompatibleAppProfileFiles(
|
|||
return {
|
||||
modelCatalogChanged: modelCatalog.changed,
|
||||
modelCatalogFile: modelCatalog.file,
|
||||
userDataDir: modelCatalog.userDataDir
|
||||
userDataDir: modelCatalog.userDataDir,
|
||||
workbuddyModelsConfig: modelCatalog.workbuddyModelsConfig,
|
||||
workbuddyVirtualAuth: modelCatalog.workbuddyVirtualAuth
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +283,7 @@ export function writeCodexCompatibleAppModelCatalog(
|
|||
profile: ProfileConfig,
|
||||
config?: AppConfig
|
||||
): CodexCompatibleAppModelCatalogWriteResult {
|
||||
const spec = profile.agent === "zcode" ? zcodeAppSpec : codexAppSpec;
|
||||
const spec = codexCompatibleAppSpecForProfile(profile);
|
||||
const configFile = resolveCodexConfigFile(configDir, profile);
|
||||
const codexHome = codexCompatibleHomeFromConfigFile(spec, configFile);
|
||||
if (spec.kind === "codex") {
|
||||
|
|
@ -209,7 +297,119 @@ export function writeCodexCompatibleAppModelCatalog(
|
|||
if (previous !== content) {
|
||||
writeFileSync(file, content, "utf8");
|
||||
}
|
||||
return { changed: previous !== content, file, userDataDir };
|
||||
const workbuddyModelsConfig = spec.kind === "workbuddy"
|
||||
? writeWorkbuddyModelsConfig(codexHome, profile, config)
|
||||
: undefined;
|
||||
const workbuddyVirtualAuth = spec.kind === "workbuddy"
|
||||
? prepareWorkbuddyAppVirtualAuth(codexHome, userDataDir, profile)
|
||||
: undefined;
|
||||
return {
|
||||
changed: previous !== content || Boolean(workbuddyModelsConfig?.changed),
|
||||
file,
|
||||
userDataDir,
|
||||
workbuddyModelsConfig,
|
||||
workbuddyVirtualAuth
|
||||
};
|
||||
}
|
||||
|
||||
export function writeWorkbuddyModelsConfig(
|
||||
workbuddyConfigDir: string,
|
||||
profile: Pick<ProfileConfig, "model" | "name" | "providerName">,
|
||||
config?: AppConfig
|
||||
): WorkbuddyModelsConfigWriteResult {
|
||||
const file = path.join(workbuddyConfigDir, "models.json");
|
||||
const model = workbuddySelectedModel(profile, config);
|
||||
const content = `${JSON.stringify(workbuddyModelsConfig(model, profile, config), null, 2)}\n`;
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
const previous = existsSync(file) ? readFileSync(file, "utf8") : undefined;
|
||||
if (previous !== content) {
|
||||
writeFileSync(file, content, "utf8");
|
||||
}
|
||||
return { changed: previous !== content, file, model };
|
||||
}
|
||||
|
||||
function workbuddySelectedModel(
|
||||
profile: Pick<ProfileConfig, "model">,
|
||||
config?: CodexCompatibleAppModelCatalogConfig
|
||||
): string {
|
||||
const configured = profile.model?.trim();
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
return codexCompatibleAppModelCatalog(config, undefined, "workbuddy").models[0]?.slug || "gpt-5-codex";
|
||||
}
|
||||
|
||||
function workbuddyModelsConfig(
|
||||
model: string,
|
||||
profile: Pick<ProfileConfig, "name" | "providerName">,
|
||||
config?: AppConfig
|
||||
): Record<string, unknown> {
|
||||
const catalogItem = codexCompatibleAppModelCatalog(config, model, "workbuddy")
|
||||
.models.find((item) => item.slug === model || item.id === model || item.model === model);
|
||||
const vendor = workbuddyModelVendor(model, profile.providerName);
|
||||
const displayName = profile.name?.trim()
|
||||
? `${profile.name.trim()} / ${model}`
|
||||
: `${vendor} / ${model}`;
|
||||
const workbuddyModel: Record<string, unknown> = {
|
||||
apiKey: "${CCR_PROFILE_API_KEY}",
|
||||
disabled: false,
|
||||
id: model,
|
||||
isDefault: true,
|
||||
name: displayName,
|
||||
supportsImages: Boolean(catalogItem?.supports_image_detail_original),
|
||||
supportsReasoning: Boolean(catalogItem?.supports_reasoning_summaries),
|
||||
supportsToolCall: true,
|
||||
tags: ["chat", "custom"],
|
||||
url: workbuddyGatewayBaseUrl(config),
|
||||
vendor
|
||||
};
|
||||
const maxInputTokens = catalogItem?.context_window;
|
||||
if (typeof maxInputTokens === "number" && Number.isFinite(maxInputTokens) && maxInputTokens > 0) {
|
||||
workbuddyModel.maxInputTokens = Math.trunc(maxInputTokens);
|
||||
}
|
||||
const reasoningEfforts = catalogItem?.supported_reasoning_efforts;
|
||||
if (Array.isArray(reasoningEfforts) && reasoningEfforts.length > 0) {
|
||||
workbuddyModel.reasoning = {
|
||||
...(catalogItem?.default_reasoning_effort ? { defaultEffort: catalogItem.default_reasoning_effort } : {}),
|
||||
supportedEfforts: reasoningEfforts
|
||||
};
|
||||
}
|
||||
return {
|
||||
availableModels: [model],
|
||||
models: [workbuddyModel]
|
||||
};
|
||||
}
|
||||
|
||||
function workbuddyModelVendor(model: string, providerName?: string): string {
|
||||
const slashIndex = model.indexOf("/");
|
||||
if (slashIndex > 0) {
|
||||
const provider = model.slice(0, slashIndex).trim();
|
||||
if (provider) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return providerName?.trim() || "Claude Code Router";
|
||||
}
|
||||
|
||||
function workbuddyGatewayBaseUrl(config?: Pick<AppConfig, "gateway">): string {
|
||||
if (!config?.gateway) {
|
||||
return "${CCR_WORKBUDDY_GATEWAY_BASE_URL}";
|
||||
}
|
||||
const host = config.gateway.host === "0.0.0.0" || config.gateway.host === "::"
|
||||
? "127.0.0.1"
|
||||
: config.gateway.host || "127.0.0.1";
|
||||
const formattedHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
||||
return `http://${formattedHost}:${config.gateway.port}/v1`;
|
||||
}
|
||||
|
||||
function codexCompatibleAppSpecForProfile(profile: Pick<ProfileConfig, "agent">): CodexCompatibleAppSpec {
|
||||
if (profile.agent === "zcode") {
|
||||
return zcodeAppSpec;
|
||||
}
|
||||
if (profile.agent === "workbuddy") {
|
||||
return workbuddyAppSpec;
|
||||
}
|
||||
return codexAppSpec;
|
||||
}
|
||||
|
||||
function codexCompatibleAppModelCatalogJson(
|
||||
|
|
@ -366,7 +566,7 @@ function launchCodexCompatibleAppProfile(
|
|||
|
||||
const configFile = resolveCodexConfigFile(configDir, profile);
|
||||
const codexHome = codexCompatibleHomeFromConfigFile(spec, configFile);
|
||||
const { modelCatalogFile, userDataDir } = refreshCodexCompatibleAppProfileFiles(configDir, profile, config);
|
||||
const { modelCatalogFile, userDataDir, workbuddyVirtualAuth } = refreshCodexCompatibleAppProfileFiles(configDir, profile, config);
|
||||
if (spec.kind === "codex") prepareCodexAppCdpUserDataDir(userDataDir);
|
||||
|
||||
const appEnv: Record<string, string> = {
|
||||
|
|
@ -375,7 +575,8 @@ function launchCodexCompatibleAppProfile(
|
|||
...codexProfileEnv(profile, lookup.executable, spec),
|
||||
CODEXL_PROFILE_SURFACE: "app",
|
||||
CCR_PROFILE_SURFACE: "app",
|
||||
...codexAppAgentEnv(spec, plan.command, codexHome, userDataDir, modelCatalogFile),
|
||||
...codexAppAgentEnv(spec, plan.command, codexHome, userDataDir, modelCatalogFile, workbuddyVirtualAuth),
|
||||
...codexCompatibleAppGatewayEnv(spec, config),
|
||||
ELECTRON_ENABLE_LOGGING: "1"
|
||||
};
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
|
|
@ -406,6 +607,19 @@ function launchCodexCompatibleAppProfile(
|
|||
};
|
||||
}
|
||||
|
||||
function codexCompatibleAppGatewayEnv(spec: CodexCompatibleAppSpec, config?: AppConfig): Record<string, string> {
|
||||
if (spec.kind !== "workbuddy" || !config) {
|
||||
return {};
|
||||
}
|
||||
const baseUrl = workbuddyGatewayBaseUrl(config);
|
||||
const apiKey = config.APIKEY?.trim() || "";
|
||||
return {
|
||||
...(apiKey ? { CCR_PROFILE_API_KEY: apiKey, CODEXL_PROFILE_API_KEY: apiKey } : {}),
|
||||
CCR_WORKBUDDY_GATEWAY_BASE_URL: baseUrl,
|
||||
CODEXL_WORKBUDDY_GATEWAY_BASE_URL: baseUrl
|
||||
};
|
||||
}
|
||||
|
||||
function codexProfileEnv(profile: ProfileConfig, appExecutable: string, spec: CodexCompatibleAppSpec): Record<string, string> {
|
||||
const providerId = sanitizeCodexProviderId(profile.providerId || "") || "claude-code-router";
|
||||
const realCliPath = profile.codexCliPath?.trim() || bundledCodexCliPath(appExecutable, spec) || spec.defaultCliCommand;
|
||||
|
|
@ -424,7 +638,7 @@ function codexProfileEnv(profile: ProfileConfig, appExecutable: string, spec: Co
|
|||
CODEXL_ZCODE_WORKSPACE_NAME: profile.name || providerId
|
||||
};
|
||||
}
|
||||
return {
|
||||
const codexEnv = {
|
||||
...(profile.model.trim() ? { CCR_CODEX_MODEL: profile.model.trim() } : {}),
|
||||
...(process.env.CCR_CODEX_CLI_MIDDLEWARE_LOG?.trim()
|
||||
? { CCR_CODEX_CLI_MIDDLEWARE_LOG: process.env.CCR_CODEX_CLI_MIDDLEWARE_LOG.trim() }
|
||||
|
|
@ -442,6 +656,22 @@ function codexProfileEnv(profile: ProfileConfig, appExecutable: string, spec: Co
|
|||
CODEXL_CODEX_WORKSPACE_NAME: profile.name || providerId,
|
||||
CODEXL_REAL_CODEX_CLI_PATH: realCliPath
|
||||
};
|
||||
if (spec.kind !== "workbuddy") {
|
||||
return codexEnv;
|
||||
}
|
||||
return {
|
||||
...codexEnv,
|
||||
...(profile.model.trim() ? { CCR_WORKBUDDY_MODEL: profile.model.trim() } : {}),
|
||||
CCR_REAL_WORKBUDDY_CLI_PATH: realCliPath,
|
||||
CCR_WORKBUDDY_MODEL_PROVIDER: providerId,
|
||||
CCR_WORKBUDDY_PROFILE: providerId,
|
||||
CCR_WORKBUDDY_REMOTE_FRONTEND_MODE: remoteFrontendMode,
|
||||
CODEXL_REAL_WORKBUDDY_CLI_PATH: realCliPath,
|
||||
CODEXL_WORKBUDDY_CORE_MODE: remoteFrontendMode,
|
||||
CODEXL_WORKBUDDY_MODEL_PROVIDER: providerId,
|
||||
CODEXL_WORKBUDDY_PROFILE: providerId,
|
||||
CODEXL_WORKBUDDY_WORKSPACE_NAME: profile.name || providerId
|
||||
};
|
||||
}
|
||||
|
||||
function codexSharedChatGptAuthEnv(): Record<string, string> {
|
||||
|
|
@ -468,28 +698,136 @@ function codexAppAgentEnv(
|
|||
launcher: string,
|
||||
home: string,
|
||||
userDataDir: string,
|
||||
modelCatalogFile: string
|
||||
modelCatalogFile: string,
|
||||
workbuddyVirtualAuth?: WorkbuddyVirtualAuthResult
|
||||
): Record<string, string> {
|
||||
return spec.kind === "zcode"
|
||||
? {
|
||||
if (spec.kind === "zcode") {
|
||||
return {
|
||||
CCR_ZCODE_MODEL_CATALOG_FILE: modelCatalogFile,
|
||||
CODEXL_ZCODE_MODEL_CATALOG_FILE: modelCatalogFile,
|
||||
ZCODE_CLI_PATH: launcher,
|
||||
ZCODE_ELECTRON_USER_DATA_PATH: userDataDir,
|
||||
ZCODE_HOME: home,
|
||||
ZCODE_STORAGE_DIR: home
|
||||
}
|
||||
: {
|
||||
CCR_CODEX_MODEL_CATALOG_FILE: modelCatalogFile,
|
||||
CODEX_CLI_PATH: launcher,
|
||||
CODEX_ELECTRON_USER_DATA_PATH: userDataDir,
|
||||
CODEX_HOME: home,
|
||||
CODEXL_CODEX_MODEL_CATALOG_FILE: modelCatalogFile
|
||||
};
|
||||
}
|
||||
const codexEnv = {
|
||||
CCR_CODEX_MODEL_CATALOG_FILE: modelCatalogFile,
|
||||
CODEX_CLI_PATH: launcher,
|
||||
CODEX_ELECTRON_USER_DATA_PATH: userDataDir,
|
||||
CODEX_HOME: home,
|
||||
CODEXL_CODEX_MODEL_CATALOG_FILE: modelCatalogFile
|
||||
};
|
||||
if (spec.kind !== "workbuddy") {
|
||||
return codexEnv;
|
||||
}
|
||||
return {
|
||||
...codexEnv,
|
||||
CCR_WORKBUDDY_MODEL_CATALOG_FILE: modelCatalogFile,
|
||||
...(workbuddyVirtualAuth ? workbuddyVirtualAuthEnv(workbuddyVirtualAuth) : {}),
|
||||
CODEBUDDY_CLI_PATH: launcher,
|
||||
CODEBUDDY_CONFIG_DIR: home,
|
||||
CODEBUDDY_ELECTRON_USER_DATA_PATH: userDataDir,
|
||||
CODEBUDDY_HOME: home,
|
||||
CODEXL_WORKBUDDY_MODEL_CATALOG_FILE: modelCatalogFile,
|
||||
WORKBUDDY_CLI_PATH: launcher,
|
||||
WORKBUDDY_CONFIG_DIR: home,
|
||||
WORKBUDDY_ELECTRON_USER_DATA_PATH: userDataDir,
|
||||
WORKBUDDY_HOME: home
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareWorkbuddyAppVirtualAuth(
|
||||
configDir: string,
|
||||
userDataDir: string,
|
||||
profile: Pick<ProfileConfig, "id" | "name">
|
||||
): WorkbuddyVirtualAuthResult {
|
||||
const homeDir = workbuddyAppVirtualHomeDir(configDir);
|
||||
const authFile = workbuddyAppVirtualAuthFile(homeDir);
|
||||
const account = workbuddyVirtualAuthAccount(profile);
|
||||
const session = {
|
||||
account,
|
||||
accounts: [account],
|
||||
allAccounts: [account],
|
||||
auth: {
|
||||
accessToken: workbuddyVirtualAuthToken,
|
||||
domain: "www.workbuddy.ai",
|
||||
expiresAt: workbuddyVirtualAuthExpiresAt,
|
||||
expiresIn: workbuddyVirtualAuthExpiresAt,
|
||||
lastRefreshTime: Date.now(),
|
||||
refreshExpiresAt: workbuddyVirtualAuthExpiresAt,
|
||||
refreshExpiresIn: workbuddyVirtualAuthExpiresAt,
|
||||
refreshToken: "",
|
||||
scope: "",
|
||||
tokenType: "Bearer"
|
||||
}
|
||||
};
|
||||
const content = `${JSON.stringify(session, null, 2)}\n`;
|
||||
mkdirSync(path.dirname(authFile), { recursive: true });
|
||||
const logoutMarker = `${authFile}.logged-out`;
|
||||
if (existsSync(logoutMarker)) {
|
||||
unlinkSync(logoutMarker);
|
||||
}
|
||||
const previous = existsSync(authFile) ? readFileSync(authFile, "utf8") : undefined;
|
||||
if (previous !== content) {
|
||||
writeFileSync(authFile, content, "utf8");
|
||||
}
|
||||
return {
|
||||
authFile,
|
||||
changed: previous !== content,
|
||||
configDir,
|
||||
homeDir,
|
||||
userDataDir
|
||||
};
|
||||
}
|
||||
|
||||
function workbuddyVirtualAuthAccount(profile: Pick<ProfileConfig, "id" | "name">): Record<string, unknown> {
|
||||
return {
|
||||
avatarUrl: "",
|
||||
lastLogin: true,
|
||||
nickname: profile.name?.trim() || "Claude Code Router",
|
||||
pluginEnabled: true,
|
||||
type: "personal",
|
||||
uid: workbuddyVirtualAuthUserId
|
||||
};
|
||||
}
|
||||
|
||||
function workbuddyVirtualAuthEnv(result: WorkbuddyVirtualAuthResult): Record<string, string> {
|
||||
return {
|
||||
APPDATA: path.join(result.homeDir, "AppData", "Roaming"),
|
||||
CCR_WORKBUDDY_VIRTUAL_AUTH_FILE: result.authFile,
|
||||
CODEXL_WORKBUDDY_VIRTUAL_AUTH_FILE: result.authFile,
|
||||
HOME: result.homeDir,
|
||||
LOCALAPPDATA: path.join(result.homeDir, "AppData", "Local"),
|
||||
USERPROFILE: result.homeDir,
|
||||
WORKBUDDY_USER_DATA_DIR: result.userDataDir
|
||||
};
|
||||
}
|
||||
|
||||
function workbuddyAppVirtualHomeDir(configDir: string): string {
|
||||
return path.join(configDir, ".claude-code-router", "workbuddy-app-home");
|
||||
}
|
||||
|
||||
function workbuddyAppVirtualAuthFile(homeDir: string): string {
|
||||
return path.join(workbuddySharedAuthDir(homeDir), `${workbuddyVirtualAuthId}.info`);
|
||||
}
|
||||
|
||||
function workbuddySharedAuthDir(homeDir: string): string {
|
||||
if (process.platform === "darwin") {
|
||||
return path.join(homeDir, "Library", "Application Support", "CodeBuddyExtension", "Data", "Public", "auth");
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
return path.join(homeDir, "AppData", "Local", "CodeBuddyExtension", "Data", "Public", "auth");
|
||||
}
|
||||
return path.join(homeDir, ".local", "share", "CodeBuddyExtension", "Data", "Public", "auth");
|
||||
}
|
||||
|
||||
function sanitizeCodexCompatibleAppEnv(env: NodeJS.ProcessEnv, kind: CodexCompatibleAppKind): void {
|
||||
const blockedPrefixes = kind === "zcode" ? ["CCR_CODEX_", "CODEXL_CODEX_"] : ["CCR_ZCODE_", "CODEXL_ZCODE_"];
|
||||
const blockedPrefixes = kind === "zcode"
|
||||
? ["CCR_CODEX_", "CODEXL_CODEX_", "CCR_WORKBUDDY_", "CODEXL_WORKBUDDY_"]
|
||||
: kind === "workbuddy"
|
||||
? ["CCR_ZCODE_", "CODEXL_ZCODE_"]
|
||||
: ["CCR_ZCODE_", "CODEXL_ZCODE_", "CCR_WORKBUDDY_", "CODEXL_WORKBUDDY_"];
|
||||
for (const key of Object.keys(env)) {
|
||||
if (blockedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
||||
delete env[key];
|
||||
|
|
@ -499,12 +837,28 @@ function sanitizeCodexCompatibleAppEnv(env: NodeJS.ProcessEnv, kind: CodexCompat
|
|||
delete env.CODEX_CLI_PATH;
|
||||
delete env.CODEX_ELECTRON_USER_DATA_PATH;
|
||||
delete env.CODEX_HOME;
|
||||
delete env.WORKBUDDY_CLI_PATH;
|
||||
delete env.WORKBUDDY_ELECTRON_USER_DATA_PATH;
|
||||
delete env.WORKBUDDY_HOME;
|
||||
delete env.CODEBUDDY_CLI_PATH;
|
||||
delete env.CODEBUDDY_CONFIG_DIR;
|
||||
delete env.CODEBUDDY_ELECTRON_USER_DATA_PATH;
|
||||
delete env.CODEBUDDY_HOME;
|
||||
return;
|
||||
}
|
||||
delete env.ZCODE_CLI_PATH;
|
||||
delete env.ZCODE_ELECTRON_USER_DATA_PATH;
|
||||
delete env.ZCODE_HOME;
|
||||
delete env.ZCODE_STORAGE_DIR;
|
||||
if (kind === "codex") {
|
||||
delete env.WORKBUDDY_CLI_PATH;
|
||||
delete env.WORKBUDDY_ELECTRON_USER_DATA_PATH;
|
||||
delete env.WORKBUDDY_HOME;
|
||||
delete env.CODEBUDDY_CLI_PATH;
|
||||
delete env.CODEBUDDY_CONFIG_DIR;
|
||||
delete env.CODEBUDDY_ELECTRON_USER_DATA_PATH;
|
||||
delete env.CODEBUDDY_HOME;
|
||||
}
|
||||
}
|
||||
|
||||
function bundledCodexCliPath(appExecutable: string, spec: CodexCompatibleAppSpec): string | undefined {
|
||||
|
|
|
|||
|
|
@ -740,7 +740,7 @@ function sanitizeProfileConfigForDisk(profile: AppConfig["profile"]): AppConfig[
|
|||
...synchronizedProfile,
|
||||
codex,
|
||||
profiles: synchronizedProfile.profiles.map((profileItem) => {
|
||||
if (profileItem.agent !== "codex" && profileItem.agent !== "opencode" && profileItem.agent !== "kilo" && profileItem.agent !== "zcode") {
|
||||
if (profileItem.agent !== "codex" && profileItem.agent !== "opencode" && profileItem.agent !== "kilo" && profileItem.agent !== "workbuddy" && profileItem.agent !== "zcode") {
|
||||
return profileItem;
|
||||
}
|
||||
const {
|
||||
|
|
@ -3568,7 +3568,7 @@ function parseProfiles(value: unknown): ProfileConfig[] | undefined {
|
|||
]);
|
||||
const env = parseStringRecord(item.env) ?? {};
|
||||
const parsedSurface = parseProfileSurface(readString(item.surface) || readString(item.entry) || readString(item.frontend)) || "auto";
|
||||
const surface = agent === "zcode" || agent === CLAUDE_DESIGN_PLUGIN_ID
|
||||
const surface = agent === "workbuddy" || agent === "zcode" || agent === CLAUDE_DESIGN_PLUGIN_ID
|
||||
? "app"
|
||||
: agent === "pi" || agent === "kilo"
|
||||
? "cli"
|
||||
|
|
@ -3636,7 +3636,7 @@ function parseProfiles(value: unknown): ProfileConfig[] | undefined {
|
|||
}
|
||||
|
||||
const appPath = readProfileAppPath(item, agent);
|
||||
const showAllSessions = agent === "zcode" || agent === "opencode" || agent === "kilo"
|
||||
const showAllSessions = agent === "zcode" || agent === "opencode" || agent === "kilo" || agent === "workbuddy"
|
||||
? false
|
||||
: typeof item.showAllSessions === "boolean"
|
||||
? item.showAllSessions
|
||||
|
|
@ -3720,7 +3720,9 @@ function readProfileAppPath(item: Record<string, unknown>, agent: ProfileConfig[
|
|||
? readString(item.chatgptAppPath) || readString(item.chatgpt_app_path) || readString(item.codexAppPath) || readString(item.codex_app_path)
|
||||
: agent === "opencode"
|
||||
? readString(item.openCodeAppPath) || readString(item.opencodeAppPath) || readString(item.opencode_app_path)
|
||||
: readString(item.zcodeAppPath) || readString(item.zcode_app_path));
|
||||
: agent === "workbuddy"
|
||||
? readString(item.workbuddyAppPath) || readString(item.workbuddy_app_path) || readString(item.workBuddyAppPath) || readString(item.work_buddy_app_path)
|
||||
: readString(item.zcodeAppPath) || readString(item.zcode_app_path));
|
||||
}
|
||||
|
||||
function parseProfileAgent(value: unknown): ProfileConfig["agent"] | undefined {
|
||||
|
|
@ -3749,6 +3751,9 @@ function parseProfileAgent(value: unknown): ProfileConfig["agent"] | undefined {
|
|||
if (normalized === "pi" || normalized === "pi-agent" || normalized === "pi agent" || normalized === "pi-coding-agent" || normalized === "pi coding agent") {
|
||||
return "pi";
|
||||
}
|
||||
if (normalized === "workbuddy" || normalized === "work-buddy" || normalized === "work buddy" || normalized === "workbuddy-agent" || normalized === "workbuddy agent") {
|
||||
return "workbuddy";
|
||||
}
|
||||
if (normalized === "zcode" || normalized === "z-code" || normalized === "z code") {
|
||||
return "zcode";
|
||||
}
|
||||
|
|
@ -3790,6 +3795,9 @@ function defaultProfileAgentName(agent: ProfileConfig["agent"]): string {
|
|||
if (agent === "pi") {
|
||||
return "Pi";
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return "Workbuddy";
|
||||
}
|
||||
if (agent === CLAUDE_DESIGN_PLUGIN_ID) {
|
||||
return "Claude Design";
|
||||
}
|
||||
|
|
@ -3803,6 +3811,8 @@ function defaultCodexConfigFile(agent: ProfileConfig["agent"]): string {
|
|||
? "~/.config/opencode/opencode.jsonc"
|
||||
: agent === "kilo"
|
||||
? "~/.config/kilo/kilo.jsonc"
|
||||
: agent === "workbuddy"
|
||||
? "~/.workbuddy/config.toml"
|
||||
: agent === "pi"
|
||||
? "~/.pi/agent"
|
||||
: agent === CLAUDE_DESIGN_PLUGIN_ID
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type AppInfo = {
|
|||
platform: string;
|
||||
usageDbFile: string;
|
||||
version: string;
|
||||
workbuddyAppPath?: string;
|
||||
};
|
||||
|
||||
export type AppDataExportResult = {
|
||||
|
|
@ -1401,7 +1402,7 @@ export const DEFAULT_TRAY_WIDGETS: TrayWidgetConfig[] = [
|
|||
{ id: "model-share", type: "model-share", variant: DEFAULT_TRAY_COMPONENT_VARIANTS.modelShare }
|
||||
];
|
||||
|
||||
export type ProfileClientKind = "claude-code" | "codex" | "grok" | "kimi" | "kilo" | "opencode" | "pi" | "zcode" | "claude-design";
|
||||
export type ProfileClientKind = "claude-code" | "codex" | "grok" | "kimi" | "kilo" | "opencode" | "pi" | "workbuddy" | "zcode" | "claude-design";
|
||||
export type CodexProfileConfigFormat = "legacy" | "separate_profile_files";
|
||||
export type CodexRemoteFrontendMode = "app" | "cli" | "claude-code";
|
||||
export type ProfileScope = "ccr" | "global" | "custom";
|
||||
|
|
@ -2190,7 +2191,7 @@ export type UsageStatsSnapshot = {
|
|||
totals: UsageTotals;
|
||||
};
|
||||
|
||||
export type AgentKind = "claude-code" | "codex" | "grok" | "kimi" | "kilo" | "opencode" | "pi" | "zcode" | "claude-design" | "unknown";
|
||||
export type AgentKind = "claude-code" | "codex" | "grok" | "kimi" | "kilo" | "opencode" | "pi" | "workbuddy" | "zcode" | "claude-design" | "unknown";
|
||||
|
||||
export type AgentAnalysisFilter = {
|
||||
agent?: AgentKind | "all";
|
||||
|
|
|
|||
|
|
@ -1651,6 +1651,14 @@ function inferAgentFromText(value: string, options: AgentTextSignalOptions = {})
|
|||
) {
|
||||
return "kilo";
|
||||
}
|
||||
if (
|
||||
normalized.includes("workbuddy") ||
|
||||
normalized.includes("work-buddy") ||
|
||||
normalized.includes("work buddy") ||
|
||||
/(^|[^a-z0-9])workbuddy([/_\s-]|$)/.test(normalized)
|
||||
) {
|
||||
return "workbuddy";
|
||||
}
|
||||
if (
|
||||
normalized.includes("openai-codex") ||
|
||||
normalized.includes("codex_cli") ||
|
||||
|
|
@ -1727,6 +1735,16 @@ function readAgentSessionHeader(headers: Record<string, string | string[]>, agen
|
|||
"x-z-code-session-id",
|
||||
"z-code-session-id"
|
||||
];
|
||||
const workbuddyHeaders = [
|
||||
"x-workbuddy-session-id",
|
||||
"workbuddy-session-id",
|
||||
"x-workbuddy-conversation-id",
|
||||
"workbuddy-conversation-id",
|
||||
"x-workbuddy-thread-id",
|
||||
"workbuddy-thread-id",
|
||||
"x-work-buddy-session-id",
|
||||
"work-buddy-session-id"
|
||||
];
|
||||
const piHeaders = [
|
||||
"x-pi-session-id",
|
||||
"pi-session-id",
|
||||
|
|
@ -1737,6 +1755,8 @@ function readAgentSessionHeader(headers: Record<string, string | string[]>, agen
|
|||
];
|
||||
const orderedHeaders = agent === "zcode"
|
||||
? [...zcodeHeaders, ...codexHeaders, ...commonHeaders, ...claudeCodeHeaders]
|
||||
: agent === "workbuddy"
|
||||
? [...workbuddyHeaders, ...codexHeaders, ...commonHeaders, ...claudeCodeHeaders]
|
||||
: agent === "pi"
|
||||
? [...piHeaders, ...commonHeaders, ...codexHeaders, ...claudeCodeHeaders]
|
||||
: agent === "codex"
|
||||
|
|
@ -3150,11 +3170,11 @@ function normalizeAgentAnalysisRange(value: UsageStatsRange | undefined): UsageS
|
|||
}
|
||||
|
||||
function normalizeAgentFilter(value: AgentAnalysisFilter["agent"] | undefined): AgentKind | "all" {
|
||||
return value === "claude-code" || value === "codex" || value === "grok" || value === "kimi" || value === "kilo" || value === "opencode" || value === "pi" || value === "zcode" || value === "claude-design" || value === "unknown" ? value : "all";
|
||||
return value === "claude-code" || value === "codex" || value === "grok" || value === "kimi" || value === "kilo" || value === "opencode" || value === "pi" || value === "workbuddy" || value === "zcode" || value === "claude-design" || value === "unknown" ? value : "all";
|
||||
}
|
||||
|
||||
function normalizeSessionAgentFilter(value: AgentAnalysisFilter["sessionAgent"] | undefined): AgentKind | undefined {
|
||||
return value === "claude-code" || value === "codex" || value === "grok" || value === "kimi" || value === "kilo" || value === "opencode" || value === "pi" || value === "zcode" || value === "claude-design" || value === "unknown" ? value : undefined;
|
||||
return value === "claude-code" || value === "codex" || value === "grok" || value === "kimi" || value === "kilo" || value === "opencode" || value === "pi" || value === "workbuddy" || value === "zcode" || value === "claude-design" || value === "unknown" ? value : undefined;
|
||||
}
|
||||
|
||||
function agentDisplayName(agent: AgentKind): string {
|
||||
|
|
@ -3182,6 +3202,9 @@ function agentDisplayName(agent: AgentKind): string {
|
|||
if (agent === "pi") {
|
||||
return "Pi";
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return "Workbuddy";
|
||||
}
|
||||
if (agent === "zcode") {
|
||||
return "ZCode";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function findProfileForOpen(config: Pick<AppConfig, "profile">, profileRe
|
|||
}
|
||||
|
||||
export function profileOpenSurfaces(profile: ProfileConfig): ProfileOpenSurface[] {
|
||||
if (profile.agent === "zcode" || profile.agent === "claude-design") {
|
||||
if (profile.agent === "workbuddy" || profile.agent === "zcode" || profile.agent === "claude-design") {
|
||||
return ["app"];
|
||||
}
|
||||
if (profile.agent === "grok" || profile.agent === "kimi" || profile.agent === "pi" || profile.agent === "kilo") {
|
||||
|
|
@ -77,7 +77,7 @@ export function resolveProfileOpenSurface(profile: ProfileConfig, surface?: Prof
|
|||
}
|
||||
|
||||
export function defaultProfileOpenSurface(profile: Pick<ProfileConfig, "agent">): ProfileOpenSurface {
|
||||
return profile.agent === "zcode" || profile.agent === "claude-design" ? "app" : "cli";
|
||||
return profile.agent === "workbuddy" || profile.agent === "zcode" || profile.agent === "claude-design" ? "app" : "cli";
|
||||
}
|
||||
|
||||
export function shouldAutoStartProfileGateway(
|
||||
|
|
@ -334,12 +334,14 @@ function buildClaudeCodeLaunchPlan(
|
|||
}
|
||||
|
||||
function isCodexCompatibleAgent(agent: ProfileConfig["agent"]): boolean {
|
||||
return agent === "codex" || agent === "zcode";
|
||||
return agent === "codex" || agent === "workbuddy" || agent === "zcode";
|
||||
}
|
||||
|
||||
function defaultCodexConfigFile(agent: ProfileConfig["agent"]): string {
|
||||
return agent === "zcode"
|
||||
? "~/.zcode/cli/config.json"
|
||||
: agent === "workbuddy"
|
||||
? "~/.workbuddy/config.toml"
|
||||
: agent === "pi"
|
||||
? "~/.pi/agent"
|
||||
: agent === "claude-design"
|
||||
|
|
@ -348,7 +350,7 @@ function defaultCodexConfigFile(agent: ProfileConfig["agent"]): string {
|
|||
}
|
||||
|
||||
function codexConfigSubdir(agent: ProfileConfig["agent"]): string {
|
||||
return agent === "zcode" ? "zcode" : "codex";
|
||||
return agent === "zcode" ? "zcode" : agent === "workbuddy" ? "workbuddy" : "codex";
|
||||
}
|
||||
|
||||
function claudeCodeWrapperFilename(profile: ProfileConfig): string {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
|
|||
import { applyClaudeAppGatewayConfig, readClaudeAppGatewayApiKeyCandidates } from "@ccr/core/agents/claude-app/gateway-service";
|
||||
import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "@ccr/core/agents/claude-app/launch";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
|
||||
import { codexDesktopAppName, launchCodexAppProfile, launchZcodeAppProfile, refreshCodexCompatibleAppProfileFiles } from "@ccr/core/agents/codex/app-launch";
|
||||
import { codexDesktopAppName, launchCodexAppProfile, launchWorkbuddyAppProfile, launchZcodeAppProfile, refreshCodexCompatibleAppProfileFiles, workbuddyDesktopAppName } from "@ccr/core/agents/codex/app-launch";
|
||||
import { CodexAppMediaPreviewBridge, shouldEnableCodexMediaPreviewBridge } from "@ccr/core/agents/codex/media-preview-bridge";
|
||||
import { findRunningOpenCodeAppPid, launchOpenCodeAppProfile, openCodeAppLaunchSignature } from "@ccr/core/agents/opencode/app-launch";
|
||||
import { writeOpenCodeGatewayConfig } from "@ccr/core/agents/opencode/profile-config";
|
||||
|
|
@ -127,7 +127,7 @@ export async function openProfileFromCcr(config: AppConfig, request: ProfileOpen
|
|||
if (profile.agent === "claude-code" && surface === "app") {
|
||||
return openClaudeAppProfile(config, profile);
|
||||
}
|
||||
if ((profile.agent === "codex" || profile.agent === "zcode") && surface === "app") {
|
||||
if ((profile.agent === "codex" || profile.agent === "workbuddy" || profile.agent === "zcode") && surface === "app") {
|
||||
return await openCodexAppProfile(config, profile);
|
||||
}
|
||||
if (profile.agent === "opencode" && surface === "app") {
|
||||
|
|
@ -260,7 +260,11 @@ async function openOpenCodeAppProfile(config: AppConfig, profile: ReturnType<typ
|
|||
}
|
||||
|
||||
async function openCodexAppProfile(config: AppConfig, profile: ReturnType<typeof findProfileForOpen>): Promise<ProfileOpenResult> {
|
||||
const appName = profile.agent === "zcode" ? "ZCode App" : codexDesktopAppName;
|
||||
const appName = profile.agent === "zcode"
|
||||
? "ZCode App"
|
||||
: profile.agent === "workbuddy"
|
||||
? workbuddyDesktopAppName
|
||||
: codexDesktopAppName;
|
||||
const profileGatewayConfig = await ensureProfileGateway(config, profile, appName);
|
||||
const existing = runningProfileApp(profile.id, "app");
|
||||
if (existing) {
|
||||
|
|
@ -297,7 +301,9 @@ async function openCodexAppProfile(config: AppConfig, profile: ReturnType<typeof
|
|||
}
|
||||
const launch = profile.agent === "zcode"
|
||||
? launchZcodeAppProfile(CONFIGDIR, profile, profileGatewayConfig)
|
||||
: launchCodexAppProfile(CONFIGDIR, profile, profileGatewayConfig);
|
||||
: profile.agent === "workbuddy"
|
||||
? launchWorkbuddyAppProfile(CONFIGDIR, profile, profileGatewayConfig)
|
||||
: launchCodexAppProfile(CONFIGDIR, profile, profileGatewayConfig);
|
||||
const entry = registerProfileApp(profile, "app", launch);
|
||||
const started = await waitForProfileAppStart(entry, 12000);
|
||||
if (!started) {
|
||||
|
|
|
|||
|
|
@ -424,7 +424,7 @@ function applyCodexProfile(config: AppConfig, profile: ProfileConfig, token: str
|
|||
const modelCatalogFile = codexModelCatalogFile(configFile);
|
||||
const modelCatalogResult = writeFileWithBackup(modelCatalogFile, codexModelCatalogJson(config, model));
|
||||
const appModelCatalogResult = writeCodexCompatibleAppModelCatalog(CONFIGDIR, { ...profile, model }, config);
|
||||
const showAllSessions = profile.agent === "zcode" ? false : Boolean(profile.showAllSessions);
|
||||
const showAllSessions = profile.agent === "zcode" || profile.agent === "workbuddy" ? false : Boolean(profile.showAllSessions);
|
||||
const toolHubMcpResult = writeCodexToolHubMcpRuntimeConfig(config, token);
|
||||
const contextArchiveMcp = profile.agent === "codex"
|
||||
? codexContextArchiveMcpConfig(config, profile, token)
|
||||
|
|
@ -466,6 +466,7 @@ function applyCodexProfile(config: AppConfig, profile: ProfileConfig, token: str
|
|||
const extras = [
|
||||
modelCatalogFile ? `catalog ${modelCatalogFile}` : "",
|
||||
appModelCatalogResult.file ? `app catalog ${appModelCatalogResult.file}` : "",
|
||||
appModelCatalogResult.workbuddyModelsConfig?.file ? `workbuddy models ${appModelCatalogResult.workbuddyModelsConfig.file}` : "",
|
||||
toolHubMcpResult.file ? `toolhub runtime ${toolHubMcpResult.file}` : "",
|
||||
contextArchiveMcp ? "context archive MCP" : "",
|
||||
separateProfileResult?.file ? `profile ${separateProfileResult.file}` : "",
|
||||
|
|
@ -2051,7 +2052,7 @@ function codexMiddlewareShellScript(
|
|||
const codexHome = profile.codexHome?.trim() || defaultCodexCompatibleHome(profile.agent, values.configFile);
|
||||
const resolvedCodexHome = resolveUserPath(codexHome);
|
||||
const remoteFrontendMode = normalizeCodexRemoteFrontendMode(profile.remoteFrontendMode);
|
||||
const surface = profile.agent === "zcode" ? "app" : normalizeProfileSurface(profile.surface);
|
||||
const surface = profile.agent === "workbuddy" || profile.agent === "zcode" ? "app" : normalizeProfileSurface(profile.surface);
|
||||
const envExports = Object.entries(profileEnv(profile)).map(([key, value]) => `export ${key}=${shellQuote(value)}`);
|
||||
const botEnvExports = shellBotGatewayEnvExports(config, profile);
|
||||
const agentEnvExports = profile.agent === "zcode"
|
||||
|
|
@ -2082,6 +2083,14 @@ function codexMiddlewareShellScript(
|
|||
]
|
||||
: [
|
||||
`export CODEX_HOME=${shellQuote(resolvedCodexHome)}`,
|
||||
...(profile.agent === "workbuddy"
|
||||
? [
|
||||
`export WORKBUDDY_HOME=${shellQuote(resolvedCodexHome)}`,
|
||||
`export WORKBUDDY_CONFIG_DIR=${shellQuote(resolvedCodexHome)}`,
|
||||
`export CODEBUDDY_CONFIG_DIR=${shellQuote(resolvedCodexHome)}`,
|
||||
`export CODEBUDDY_HOME=${shellQuote(resolvedCodexHome)}`
|
||||
]
|
||||
: []),
|
||||
"if [ -z \"${CCR_REAL_CODEX_CLI_PATH:-}\" ]; then",
|
||||
` CCR_REAL_CODEX_CLI_PATH=${shellQuote(codexCli)}`,
|
||||
"fi",
|
||||
|
|
@ -2190,7 +2199,7 @@ function codexMiddlewareCmdScript(
|
|||
const codexHome = profile.codexHome?.trim() || defaultCodexCompatibleHome(profile.agent, values.configFile);
|
||||
const resolvedCodexHome = resolveUserPath(codexHome);
|
||||
const remoteFrontendMode = normalizeCodexRemoteFrontendMode(profile.remoteFrontendMode);
|
||||
const surface = profile.agent === "zcode" ? "app" : normalizeProfileSurface(profile.surface);
|
||||
const surface = profile.agent === "workbuddy" || profile.agent === "zcode" ? "app" : normalizeProfileSurface(profile.surface);
|
||||
const workspaceName = profile.name || values.providerId;
|
||||
const envExports = Object.entries(profileEnv(profile)).map(([key, value]) => cmdSetLine(key, value));
|
||||
const botEnvExports = cmdBotGatewayEnvExports(config, profile);
|
||||
|
|
@ -2216,6 +2225,14 @@ function codexMiddlewareCmdScript(
|
|||
]
|
||||
: [
|
||||
cmdSetLine("CODEX_HOME", resolvedCodexHome),
|
||||
...(profile.agent === "workbuddy"
|
||||
? [
|
||||
cmdSetLine("WORKBUDDY_HOME", resolvedCodexHome),
|
||||
cmdSetLine("WORKBUDDY_CONFIG_DIR", resolvedCodexHome),
|
||||
cmdSetLine("CODEBUDDY_CONFIG_DIR", resolvedCodexHome),
|
||||
cmdSetLine("CODEBUDDY_HOME", resolvedCodexHome)
|
||||
]
|
||||
: []),
|
||||
`if not defined CCR_REAL_CODEX_CLI_PATH ${cmdSetLine("CCR_REAL_CODEX_CLI_PATH", codexCli)}`,
|
||||
"if not defined CCR_BUNDLED_CODEX_CLI_PATH set \"CCR_BUNDLED_CODEX_CLI_PATH=%CCR_REAL_CODEX_CLI_PATH%\"",
|
||||
cmdSetLine("CCR_CODEX_PROFILE", values.providerId),
|
||||
|
|
@ -2716,10 +2733,11 @@ function disabledProfileStatus(profile: ProfileConfig): ProfileClientApplyStatus
|
|||
);
|
||||
}
|
||||
const providerId = sanitizeCodexProviderId(profile.providerId || "") || "claude-code-router";
|
||||
const clientName = codexCompatibleClientName(profile.agent);
|
||||
return restoreDisabledGlobalProfile(
|
||||
profile,
|
||||
resolveCodexConfigFile(profile),
|
||||
"Codex profile is disabled.",
|
||||
`${clientName} profile is disabled.`,
|
||||
(content) => isManagedCodexConfigContent(content, providerId)
|
||||
);
|
||||
}
|
||||
|
|
@ -2745,11 +2763,12 @@ export function restoreInactiveGlobalProfileConfigs(profiles: ProfileConfig[]):
|
|||
}
|
||||
const codexProfiles = profiles.filter((profile) => profile.agent === "codex");
|
||||
if (codexProfiles.length > 0 && !codexProfiles.some((profile) => profile.enabled && isGlobalProfile(profile))) {
|
||||
const providerIds = codexCompatibleProviderIds(codexProfiles);
|
||||
for (const file of uniqueResolvedPaths([
|
||||
...codexProfiles.map(globalCodexConfigCandidate)
|
||||
])) {
|
||||
const restoreResult = restoreGlobalConfigFile(file, {
|
||||
isManagedContent: (content) => isManagedCodexConfigContent(content, "claude-code-router"),
|
||||
isManagedContent: (content) => providerIds.some((providerId) => isManagedCodexConfigContent(content, providerId)),
|
||||
mode: privateFileMode
|
||||
});
|
||||
if (restoreResult.changed || restoreResult.missingBackup) {
|
||||
|
|
@ -2757,6 +2776,21 @@ export function restoreInactiveGlobalProfileConfigs(profiles: ProfileConfig[]):
|
|||
}
|
||||
}
|
||||
}
|
||||
const workbuddyProfiles = profiles.filter((profile) => profile.agent === "workbuddy");
|
||||
if (workbuddyProfiles.length > 0 && !workbuddyProfiles.some((profile) => profile.enabled && isGlobalProfile(profile))) {
|
||||
const providerIds = codexCompatibleProviderIds(workbuddyProfiles);
|
||||
for (const file of uniqueResolvedPaths([
|
||||
...workbuddyProfiles.map(globalCodexConfigCandidate)
|
||||
])) {
|
||||
const restoreResult = restoreGlobalConfigFile(file, {
|
||||
isManagedContent: (content) => providerIds.some((providerId) => isManagedCodexConfigContent(content, providerId)),
|
||||
mode: privateFileMode
|
||||
});
|
||||
if (restoreResult.changed || restoreResult.missingBackup) {
|
||||
statuses.push(inactiveGlobalCleanupStatus("workbuddy", file, restoreResult));
|
||||
}
|
||||
}
|
||||
}
|
||||
const openCodeProfiles = profiles.filter((profile) => profile.agent === "opencode");
|
||||
if (openCodeProfiles.length > 0 && !openCodeProfiles.some((profile) => profile.enabled && isGlobalProfile(profile))) {
|
||||
const providerIds = [...new Set([
|
||||
|
|
@ -2823,7 +2857,14 @@ function globalCodexConfigCandidate(profile: ProfileConfig): string {
|
|||
if (codexHome) {
|
||||
return path.join(resolveUserPath(codexHome), "config.toml");
|
||||
}
|
||||
return profile.configFile || "~/.codex/config.toml";
|
||||
return profile.configFile || defaultCodexConfigFile(profile.agent);
|
||||
}
|
||||
|
||||
function codexCompatibleProviderIds(profiles: ProfileConfig[]): string[] {
|
||||
return [...new Set([
|
||||
"claude-code-router",
|
||||
...profiles.map((profile) => sanitizeCodexProviderId(profile.providerId || "")).filter(Boolean)
|
||||
])];
|
||||
}
|
||||
|
||||
function globalOpenCodeConfigCandidate(profile: ProfileConfig): string {
|
||||
|
|
@ -2931,7 +2972,7 @@ function readGlobalProfileTakeoverMarker(): GlobalProfileTakeoverRecord[] {
|
|||
}
|
||||
return parsed.profiles.filter((value): value is GlobalProfileTakeoverRecord =>
|
||||
isRecord(value) &&
|
||||
(value.agent === "claude-code" || value.agent === "codex" || value.agent === "opencode" || value.agent === "kilo" || value.agent === "zcode") &&
|
||||
(value.agent === "claude-code" || value.agent === "codex" || value.agent === "opencode" || value.agent === "kilo" || value.agent === "workbuddy" || value.agent === "zcode") &&
|
||||
typeof value.id === "string" &&
|
||||
typeof value.name === "string"
|
||||
);
|
||||
|
|
@ -3381,6 +3422,9 @@ function codexCompatibleClientName(agent: ProfileConfig["agent"]): string {
|
|||
if (agent === "pi") {
|
||||
return "Pi";
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return "Workbuddy";
|
||||
}
|
||||
if (agent === "claude-design") {
|
||||
return "Claude Design";
|
||||
}
|
||||
|
|
@ -3392,6 +3436,8 @@ function defaultCodexConfigFile(agent: ProfileConfig["agent"]): string {
|
|||
? "~/.zcode/cli/config.json"
|
||||
: agent === "kilo"
|
||||
? "~/.config/kilo/kilo.jsonc"
|
||||
: agent === "workbuddy"
|
||||
? "~/.workbuddy/config.toml"
|
||||
: agent === "pi"
|
||||
? "~/.pi/agent"
|
||||
: agent === "claude-design"
|
||||
|
|
@ -3400,11 +3446,11 @@ function defaultCodexConfigFile(agent: ProfileConfig["agent"]): string {
|
|||
}
|
||||
|
||||
function codexConfigSubdir(agent: ProfileConfig["agent"]): string {
|
||||
return agent === "zcode" ? "zcode" : "codex";
|
||||
return agent === "zcode" ? "zcode" : agent === "workbuddy" ? "workbuddy" : "codex";
|
||||
}
|
||||
|
||||
function defaultCodexCliCommand(agent: ProfileConfig["agent"]): string {
|
||||
return agent === "zcode" ? "zcode" : "codex";
|
||||
return agent === "zcode" ? "zcode" : agent === "workbuddy" ? "codebuddy" : "codex";
|
||||
}
|
||||
|
||||
function defaultCodexCompatibleHome(agent: ProfileConfig["agent"], configFile: string): string {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { loadOnboardingFinished, markOnboardingFinished } from "@ccr/core/config
|
|||
import { scanBotHandoffBluetoothTargets, scanBotHandoffWifiTargets } from "@ccr/core/agents/bot-gateway/handoff-scan-service";
|
||||
import { cancelBotGatewayQrLogin, startBotGatewayQrLogin, waitBotGatewayQrLogin } from "@ccr/core/agents/bot-gateway/qr-login-service";
|
||||
import { syncClaudeAppGatewayConfig, restoreClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-service";
|
||||
import { findInstalledCodexAppExecutable } from "@ccr/core/agents/codex/app-launch";
|
||||
import { findInstalledCodexAppExecutable, findInstalledWorkbuddyAppExecutable } from "@ccr/core/agents/codex/app-launch";
|
||||
import { findInstalledOpenCodeAppExecutable } from "@ccr/core/agents/opencode/app-launch";
|
||||
import { loadAppConfig, saveApiKeysConfig, saveAppConfig } from "@ccr/core/config/config";
|
||||
import {
|
||||
|
|
@ -516,6 +516,7 @@ function logProfileApplyResult(result: ProfileApplyResult): void {
|
|||
function getCliAppInfo(): AppInfo {
|
||||
const chatgptAppPath = findInstalledCodexAppExecutable().executable;
|
||||
const opencodeAppPath = findInstalledOpenCodeAppExecutable().executable;
|
||||
const workbuddyAppPath = findInstalledWorkbuddyAppExecutable().executable;
|
||||
return {
|
||||
...(chatgptAppPath ? { chatgptAppPath } : {}),
|
||||
configDbFile: APP_CONFIG_DB_FILE,
|
||||
|
|
@ -528,7 +529,8 @@ function getCliAppInfo(): AppInfo {
|
|||
platform: process.platform,
|
||||
requestLogsDbFile: REQUEST_LOGS_DB_FILE,
|
||||
usageDbFile: USAGE_DB_FILE,
|
||||
version: packageJson.version
|
||||
version: packageJson.version,
|
||||
...(workbuddyAppPath ? { workbuddyAppPath } : {})
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
codexDesktopAppName,
|
||||
codexSharedChatGptAuthEnvForTest,
|
||||
findInstalledCodexAppExecutable,
|
||||
findInstalledWorkbuddyAppExecutable,
|
||||
removeLegacyCodexVirtualAuthMarker,
|
||||
writeCodexCompatibleAppModelCatalog
|
||||
} from "@ccr/core/agents/codex/app-launch.ts";
|
||||
|
|
@ -288,6 +289,114 @@ test("ChatGPT profile appPath overrides process env discovery", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("WorkBuddy AI app path override discovers the Electron executable", () => {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "ccr-workbuddy-app-"));
|
||||
try {
|
||||
let configuredPath;
|
||||
let expectedExecutable;
|
||||
if (process.platform === "darwin") {
|
||||
configuredPath = path.join(root, "WorkBuddy AI.app");
|
||||
const macosDir = path.join(configuredPath, "Contents", "MacOS");
|
||||
mkdirSync(macosDir, { recursive: true });
|
||||
expectedExecutable = path.join(macosDir, "Electron");
|
||||
writeFileSync(expectedExecutable, "");
|
||||
writeFileSync(
|
||||
path.join(configuredPath, "Contents", "Info.plist"),
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>Electron</string></dict></plist>"
|
||||
);
|
||||
} else {
|
||||
expectedExecutable = path.join(root, process.platform === "win32" ? "WorkBuddyAI.exe" : "workbuddy-ai");
|
||||
configuredPath = expectedExecutable;
|
||||
writeFileSync(expectedExecutable, "");
|
||||
}
|
||||
|
||||
const result = findInstalledWorkbuddyAppExecutable(configuredPath);
|
||||
assert.equal(result.executable, expectedExecutable);
|
||||
assert.equal(result.checked[0], configuredPath);
|
||||
} finally {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("WorkBuddy AI app profile writes the virtual desktop auth session", () => {
|
||||
const configDir = mkdtempSync(path.join(os.tmpdir(), "ccr-workbuddy-app-auth-"));
|
||||
try {
|
||||
const profile = {
|
||||
agent: "workbuddy",
|
||||
enabled: true,
|
||||
id: "workbuddy-main",
|
||||
model: "Codex API/gpt-5-codex",
|
||||
name: "WorkBuddy Main",
|
||||
providerId: "claude-code-router",
|
||||
scope: "ccr",
|
||||
surface: "app"
|
||||
};
|
||||
|
||||
const workbuddyConfig = {
|
||||
gateway: {
|
||||
enabled: true,
|
||||
host: "0.0.0.0",
|
||||
mode: "process",
|
||||
port: 48765
|
||||
},
|
||||
Providers: [{
|
||||
modelMetadata: {
|
||||
"gpt-5-codex": {
|
||||
contextWindow: 272_000,
|
||||
defaultReasoningLevel: "medium",
|
||||
supportedReasoningLevels: [
|
||||
{ description: "Medium", effort: "medium" },
|
||||
{ description: "High", effort: "high" }
|
||||
],
|
||||
supportsReasoningSummaries: true
|
||||
}
|
||||
},
|
||||
models: ["gpt-5-codex"],
|
||||
name: "Codex API",
|
||||
type: "openai_responses"
|
||||
}]
|
||||
};
|
||||
const result = writeCodexCompatibleAppModelCatalog(configDir, profile, workbuddyConfig);
|
||||
|
||||
assert.equal(path.basename(result.file), "ccr-workbuddy-model-catalog.json");
|
||||
assert.ok(result.workbuddyModelsConfig);
|
||||
assert.equal(path.basename(result.workbuddyModelsConfig.file), "models.json");
|
||||
assert.equal(result.workbuddyModelsConfig.model, "Codex API/gpt-5-codex");
|
||||
const modelsConfig = JSON.parse(readFileSync(result.workbuddyModelsConfig.file, "utf8"));
|
||||
assert.deepEqual(modelsConfig.availableModels, ["Codex API/gpt-5-codex"]);
|
||||
assert.equal(modelsConfig.models.length, 1);
|
||||
assert.equal(modelsConfig.models[0].id, "Codex API/gpt-5-codex");
|
||||
assert.equal(modelsConfig.models[0].vendor, "Codex API");
|
||||
assert.equal(modelsConfig.models[0].url, "http://127.0.0.1:48765/v1");
|
||||
assert.equal(modelsConfig.models[0].apiKey, "${CCR_PROFILE_API_KEY}");
|
||||
assert.equal(modelsConfig.models[0].supportsToolCall, true);
|
||||
assert.equal(modelsConfig.models[0].supportsReasoning, true);
|
||||
assert.equal(modelsConfig.models[0].maxInputTokens, 272_000);
|
||||
assert.deepEqual(modelsConfig.models[0].reasoning.supportedEfforts, ["medium", "high"]);
|
||||
|
||||
assert.ok(result.workbuddyVirtualAuth);
|
||||
assert.equal(path.basename(result.workbuddyVirtualAuth.authFile), "workbuddy-desktop-ai.info");
|
||||
assert.ok(result.workbuddyVirtualAuth.authFile.includes(path.join("CodeBuddyExtension", "Data", "Public", "auth")));
|
||||
assert.equal(existsSync(result.workbuddyVirtualAuth.authFile), true);
|
||||
|
||||
const session = JSON.parse(readFileSync(result.workbuddyVirtualAuth.authFile, "utf8"));
|
||||
assert.equal(session.auth.accessToken, "ccr-local-profile");
|
||||
assert.equal(session.auth.domain, "www.workbuddy.ai");
|
||||
assert.equal(session.auth.refreshToken, "");
|
||||
assert.ok(Date.now() - session.auth.lastRefreshTime < 5_000);
|
||||
assert.equal(session.account.uid, "ccr-local-profile");
|
||||
assert.equal(session.account.nickname, "WorkBuddy Main");
|
||||
assert.equal(session.account.type, "personal");
|
||||
assert.deepEqual(session.accounts, [session.account]);
|
||||
assert.deepEqual(session.allAccounts, [session.account]);
|
||||
|
||||
const second = writeCodexCompatibleAppModelCatalog(configDir, profile, workbuddyConfig);
|
||||
assert.equal(second.workbuddyModelsConfig.changed, false);
|
||||
} finally {
|
||||
rmSync(configDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
function withPlatform(platform, callback) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
Object.defineProperty(process, "platform", {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,17 @@ const kiloProfile = {
|
|||
surface: "cli"
|
||||
};
|
||||
|
||||
const workbuddyProfile = {
|
||||
agent: "workbuddy",
|
||||
enabled: true,
|
||||
id: "workbuddy-main",
|
||||
model: "provider,model",
|
||||
name: "Workbuddy Main",
|
||||
providerId: "claude-code-router",
|
||||
scope: "ccr",
|
||||
surface: "app"
|
||||
};
|
||||
|
||||
const claudeDesignProfile = {
|
||||
agent: "claude-design",
|
||||
enabled: true,
|
||||
|
|
@ -130,6 +141,7 @@ test("profile open surfaces enforce agent capabilities", () => {
|
|||
assert.deepEqual(profileOpenSurfaces(kimiProfile), ["cli"]);
|
||||
assert.deepEqual(profileOpenSurfaces(piProfile), ["cli"]);
|
||||
assert.deepEqual(profileOpenSurfaces(kiloProfile), ["cli"]);
|
||||
assert.deepEqual(profileOpenSurfaces(workbuddyProfile), ["app"]);
|
||||
assert.deepEqual(profileOpenSurfaces(openCodeProfile), ["cli", "app"]);
|
||||
assert.deepEqual(profileOpenSurfaces(claudeDesignProfile), ["app"]);
|
||||
assert.equal(resolveProfileOpenSurface(codexProfile, "app"), "app");
|
||||
|
|
@ -138,6 +150,7 @@ test("profile open surfaces enforce agent capabilities", () => {
|
|||
assert.throws(() => resolveProfileOpenSurface(kimiProfile, "app"), /does not support APP/);
|
||||
assert.throws(() => resolveProfileOpenSurface(piProfile, "app"), /does not support APP/);
|
||||
assert.throws(() => resolveProfileOpenSurface(kiloProfile, "app"), /does not support APP/);
|
||||
assert.throws(() => resolveProfileOpenSurface(workbuddyProfile, "cli"), /does not support CLI/);
|
||||
assert.throws(() => resolveProfileOpenSurface(claudeDesignProfile, "cli"), /does not support CLI/);
|
||||
});
|
||||
|
||||
|
|
@ -145,6 +158,7 @@ test("default profile command surface is CLI unless the agent is app-only", () =
|
|||
assert.equal(defaultProfileOpenSurface(claudeProfile), "cli");
|
||||
assert.equal(defaultProfileOpenSurface(codexProfile), "cli");
|
||||
assert.equal(defaultProfileOpenSurface({ ...codexProfile, surface: "app" }), "cli");
|
||||
assert.equal(defaultProfileOpenSurface(workbuddyProfile), "app");
|
||||
assert.equal(defaultProfileOpenSurface({ ...codexProfile, agent: "zcode" }), "app");
|
||||
assert.equal(defaultProfileOpenSurface(claudeDesignProfile), "app");
|
||||
});
|
||||
|
|
@ -153,6 +167,7 @@ test("Grok and Kimi CLI start a temporary CCR gateway when none is already runni
|
|||
assert.equal(shouldAutoStartProfileGateway(grokProfile, "cli"), true);
|
||||
assert.equal(shouldAutoStartProfileGateway(kimiProfile, "cli"), true);
|
||||
assert.equal(shouldAutoStartProfileGateway(piProfile, "cli"), true);
|
||||
assert.equal(shouldAutoStartProfileGateway(workbuddyProfile, "app"), false);
|
||||
assert.equal(shouldAutoStartProfileGateway(kiloProfile, "cli"), false);
|
||||
assert.equal(shouldAutoStartProfileGateway(codexProfile, "cli"), false);
|
||||
assert.equal(shouldAutoStartProfileGateway(claudeProfile, "app"), false);
|
||||
|
|
@ -168,6 +183,7 @@ test("buildProfileLaunchPlan creates CCR-managed launcher paths", () => {
|
|||
const piPlan = buildProfileLaunchPlan(configDir, piProfile, "cli", ["--debug"]);
|
||||
const openCodePlan = buildProfileLaunchPlan(configDir, openCodeProfile, "cli", ["--debug"]);
|
||||
const kiloPlan = buildProfileLaunchPlan(configDir, kiloProfile, "cli", ["--debug"]);
|
||||
const workbuddyPlan = buildProfileLaunchPlan(configDir, workbuddyProfile, "app");
|
||||
|
||||
assert.equal(codexPlan.surface, "app");
|
||||
assert.deepEqual(codexPlan.args, ["app"]);
|
||||
|
|
@ -218,6 +234,12 @@ test("buildProfileLaunchPlan creates CCR-managed launcher paths", () => {
|
|||
assert.match(kiloPlan.env.KILO_CONFIG, /kilo[\\/]kilo\.jsonc$/);
|
||||
assert.throws(() => buildProfileLaunchPlan(configDir, kiloProfile, "app"), /does not support APP/);
|
||||
|
||||
assert.equal(workbuddyPlan.surface, "app");
|
||||
assert.deepEqual(workbuddyPlan.args, ["app"]);
|
||||
assert.equal(path.basename(workbuddyPlan.command), process.platform === "win32" ? "ccr-codex-cli-stdio-workbuddy-main.cmd" : "ccr-codex-cli-stdio-workbuddy-main");
|
||||
assert.equal(workbuddyPlan.env.CCR_PROFILE_SURFACE, "app");
|
||||
assert.throws(() => buildProfileLaunchPlan(configDir, workbuddyProfile, "cli"), /does not support CLI/);
|
||||
|
||||
assert.throws(() => buildProfileLaunchPlan(configDir, claudeProfile, "app"), /Claude App opening/);
|
||||
assert.throws(() => buildProfileLaunchPlan(configDir, claudeDesignProfile, "app"), /Claude Design profiles can only be opened from CCR Desktop/);
|
||||
});
|
||||
|
|
@ -239,6 +261,10 @@ test("profile config paths honor CCR, custom, and global scopes", () => {
|
|||
resolveCodexConfigFile(configDir, customProfile),
|
||||
path.join(configDir, "profiles", "custom-profile", "custom", "codex", "config.toml")
|
||||
);
|
||||
assert.equal(
|
||||
resolveCodexConfigFile(configDir, workbuddyProfile),
|
||||
path.join(configDir, "profiles", "workbuddy-main", "workbuddy", "config.toml")
|
||||
);
|
||||
assert.equal(resolveCodexConfigFile(configDir, globalCodex), path.join(process.env.HOME, "codex-home", "config.toml"));
|
||||
assert.equal(
|
||||
resolveOpenCodeConfigFile(configDir, openCodeProfile),
|
||||
|
|
@ -253,9 +279,11 @@ test("profile config paths honor CCR, custom, and global scopes", () => {
|
|||
test("profileOpenCommand quotes profile references for shell usage", () => {
|
||||
const cliCommand = profileOpenCommand(claudeProfile, "cli", "ccr", "Claude Main");
|
||||
const appCommand = profileOpenCommand(codexProfile, "app", "ccr", "Codex Main");
|
||||
const workbuddyCommand = profileOpenCommand(workbuddyProfile, undefined, "ccr", "Workbuddy Main");
|
||||
|
||||
assert.match(cliCommand, /Claude/);
|
||||
assert.match(cliCommand, /Main/);
|
||||
assert.equal(cliCommand.endsWith(" cli"), false);
|
||||
assert.match(appCommand, / app$/);
|
||||
assert.match(workbuddyCommand, / app$/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { scanBotHandoffBluetoothTargets, scanBotHandoffWifiTargets } from "@ccr/
|
|||
import { cancelBotGatewayQrLogin, startBotGatewayQrLogin, waitBotGatewayQrLogin } from "@ccr/core/agents/bot-gateway/qr-login-service";
|
||||
import { closeBotGatewayQrWindow, openBotGatewayQrWindow } from "./bot-gateway-qr-window-service";
|
||||
import { syncClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-service";
|
||||
import { findInstalledCodexAppExecutable } from "@ccr/core/agents/codex/app-launch";
|
||||
import { findInstalledCodexAppExecutable, findInstalledWorkbuddyAppExecutable } from "@ccr/core/agents/codex/app-launch";
|
||||
import { findInstalledOpenCodeAppExecutable } from "@ccr/core/agents/opencode/app-launch";
|
||||
import { loadAppConfig, saveApiKeysConfig, saveAppConfig, saveAppThemePreference, withClaudeDesignRuntimePluginConfig } from "@ccr/core/config/config";
|
||||
import {
|
||||
|
|
@ -68,6 +68,7 @@ function applyAppThemePreference(theme: AppConfig["theme"]): void {
|
|||
ipcMain.handle(IPC_CHANNELS.appGetInfo, () => {
|
||||
const chatgptAppPath = findInstalledCodexAppExecutable().executable;
|
||||
const opencodeAppPath = findInstalledOpenCodeAppExecutable().executable;
|
||||
const workbuddyAppPath = findInstalledWorkbuddyAppExecutable().executable;
|
||||
return {
|
||||
...(chatgptAppPath ? { chatgptAppPath } : {}),
|
||||
configDbFile: APP_CONFIG_DB_FILE,
|
||||
|
|
@ -80,7 +81,8 @@ ipcMain.handle(IPC_CHANNELS.appGetInfo, () => {
|
|||
platform: process.platform,
|
||||
requestLogsDbFile: REQUEST_LOGS_DB_FILE,
|
||||
usageDbFile: USAGE_DB_FILE,
|
||||
version: app.getVersion()
|
||||
version: app.getVersion(),
|
||||
...(workbuddyAppPath ? { workbuddyAppPath } : {})
|
||||
} satisfies AppInfo;
|
||||
});
|
||||
|
||||
|
|
|
|||
BIN
packages/ui/src/assets/agent-logos/workbuddy.png
Normal file
BIN
packages/ui/src/assets/agent-logos/workbuddy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 482 KiB |
|
|
@ -354,12 +354,12 @@ function App() {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!appInfo.chatgptAppPath && !appInfo.opencodeAppPath) {
|
||||
if (!appInfo.chatgptAppPath && !appInfo.opencodeAppPath && !appInfo.workbuddyAppPath) {
|
||||
return;
|
||||
}
|
||||
setProfileDraft((current) => profileDraftWithDetectedAppPath(current, appInfo.chatgptAppPath, appInfo.opencodeAppPath));
|
||||
setProfileEditDraft((current) => profileDraftWithDetectedAppPath(current, appInfo.chatgptAppPath, appInfo.opencodeAppPath));
|
||||
}, [appInfo.chatgptAppPath, appInfo.opencodeAppPath]);
|
||||
setProfileDraft((current) => profileDraftWithDetectedAppPath(current, appInfo.chatgptAppPath, appInfo.opencodeAppPath, appInfo.workbuddyAppPath));
|
||||
setProfileEditDraft((current) => profileDraftWithDetectedAppPath(current, appInfo.chatgptAppPath, appInfo.opencodeAppPath, appInfo.workbuddyAppPath));
|
||||
}, [appInfo.chatgptAppPath, appInfo.opencodeAppPath, appInfo.workbuddyAppPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProfileAgentAvailable(profileAgentTab)) {
|
||||
|
|
@ -807,10 +807,11 @@ function App() {
|
|||
setProfileDraft(profileDraftWithDetectedAppPath(
|
||||
createProfileDraftFromProfile(profile, draftConfig.botConfigs),
|
||||
appInfo.chatgptAppPath,
|
||||
appInfo.opencodeAppPath
|
||||
appInfo.opencodeAppPath,
|
||||
appInfo.workbuddyAppPath
|
||||
));
|
||||
setProfileActionError("");
|
||||
}, [activeView, onboardingStep, onboardingProfileConfirmed, configLoaded, draftConfig.profile.profiles, draftConfig.botConfigs, profileDraft.agent, appInfo.chatgptAppPath, appInfo.opencodeAppPath]);
|
||||
}, [activeView, onboardingStep, onboardingProfileConfirmed, configLoaded, draftConfig.profile.profiles, draftConfig.botConfigs, profileDraft.agent, appInfo.chatgptAppPath, appInfo.opencodeAppPath, appInfo.workbuddyAppPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeView !== "onboarding" || !configLoaded || !onboardingStatusLoaded || !providerPresetsLoaded || providerAddOpen) {
|
||||
|
|
@ -2523,7 +2524,7 @@ function App() {
|
|||
function openAddProfileDialog(agent: ProfileConfig["agent"] = profileAgentTab) {
|
||||
const resolvedAgent = isProfileAgentAvailable(agent) ? agent : defaultAvailableProfileAgent;
|
||||
setProfileAgentTab(resolvedAgent);
|
||||
setProfileDraft(profileDraftWithDetectedAppPath(createProfileDraft(resolvedAgent), appInfo.chatgptAppPath, appInfo.opencodeAppPath));
|
||||
setProfileDraft(profileDraftWithDetectedAppPath(createProfileDraft(resolvedAgent), appInfo.chatgptAppPath, appInfo.opencodeAppPath, appInfo.workbuddyAppPath));
|
||||
setProfileActionError("");
|
||||
setProfileAddOpen(true);
|
||||
}
|
||||
|
|
@ -2537,7 +2538,8 @@ function App() {
|
|||
setProfileEditDraft(profileDraftWithDetectedAppPath(
|
||||
createProfileDraftFromProfile(profile, draftConfig.botConfigs),
|
||||
appInfo.chatgptAppPath,
|
||||
appInfo.opencodeAppPath
|
||||
appInfo.opencodeAppPath,
|
||||
appInfo.workbuddyAppPath
|
||||
));
|
||||
setProfileActionError("");
|
||||
}
|
||||
|
|
@ -2754,7 +2756,7 @@ function App() {
|
|||
return profileDraftWithDetectedAppPath({
|
||||
...createProfileDraft(patch.agent, name),
|
||||
envRows: profileEnvRowsForAgent(patch.agent, current.envRows)
|
||||
}, appInfo.chatgptAppPath, appInfo.opencodeAppPath);
|
||||
}, appInfo.chatgptAppPath, appInfo.opencodeAppPath, appInfo.workbuddyAppPath);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
|
@ -2769,7 +2771,7 @@ function App() {
|
|||
return profileDraftWithDetectedAppPath({
|
||||
...createProfileDraft(patch.agent, name),
|
||||
envRows: profileEnvRowsForAgent(patch.agent, current.envRows)
|
||||
}, appInfo.chatgptAppPath, appInfo.opencodeAppPath);
|
||||
}, appInfo.chatgptAppPath, appInfo.opencodeAppPath, appInfo.workbuddyAppPath);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -792,7 +792,7 @@ export function AddProfileForm({
|
|||
scope: "ccr",
|
||||
surface: "app"
|
||||
}
|
||||
: agent === "zcode"
|
||||
: agent === "workbuddy" || agent === "zcode"
|
||||
? { agent, surface: "app" }
|
||||
: { agent })}
|
||||
value={draft.agent}
|
||||
|
|
@ -829,7 +829,7 @@ export function AddProfileForm({
|
|||
});
|
||||
}}
|
||||
options={translateOptions(
|
||||
draft.agent === "zcode" || draft.agent === "claude-design"
|
||||
draft.agent === "workbuddy" || draft.agent === "zcode" || draft.agent === "claude-design"
|
||||
? profileSurfaceOptions.filter((option) => option.value === "app")
|
||||
: draft.agent === "grok" || draft.agent === "kimi" || draft.agent === "pi" || draft.agent === "kilo"
|
||||
? profileSurfaceOptions.filter((option) => option.value === "cli")
|
||||
|
|
@ -940,7 +940,7 @@ export function AddProfileForm({
|
|||
</>
|
||||
) : draft.agent === "claude-design" ? null : (
|
||||
<>
|
||||
<Field className="sm:col-span-2" label={t(draft.agent === "zcode" ? "ZCode model" : draft.agent === "opencode" ? "OpenCode model" : draft.agent === "kilo" ? "Kilo model" : "Codex model")} requirement="optional" requirementLabel={optionalFieldLabel}>
|
||||
<Field className="sm:col-span-2" label={t(draft.agent === "zcode" ? "ZCode model" : draft.agent === "opencode" ? "OpenCode model" : draft.agent === "kilo" ? "Kilo model" : draft.agent === "workbuddy" ? "Workbuddy model" : "Codex model")} requirement="optional" requirementLabel={optionalFieldLabel}>
|
||||
<ModelSelector
|
||||
placeholder={modelPlaceholder}
|
||||
providers={providers}
|
||||
|
|
@ -1000,7 +1000,7 @@ export function AddProfileForm({
|
|||
<Input value={draft.providerName} onChange={(event) => onChange({ providerName: event.target.value })} />
|
||||
{validation.providerName ? <ProfileFieldHint>{t(validation.providerName)}</ProfileFieldHint> : null}
|
||||
</Field>
|
||||
{draft.agent !== "zcode" && draft.agent !== "opencode" && draft.agent !== "kilo" ? (
|
||||
{draft.agent !== "zcode" && draft.agent !== "opencode" && draft.agent !== "kilo" && draft.agent !== "workbuddy" ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/20 px-3 py-2">
|
||||
<span className="text-[12px] font-medium">{t("Show all sessions")}</span>
|
||||
<Toggle checked={draft.showAllSessions} onChange={(showAllSessions) => onChange({ showAllSessions })} />
|
||||
|
|
@ -1315,7 +1315,7 @@ function profileNumberDraftValid(value: string, min: number, max: number): boole
|
|||
return Number.isFinite(numeric) && numeric >= min && numeric <= max;
|
||||
}
|
||||
|
||||
function profileAppPathLabel(agent: ProfileConfig["agent"]): "CLAUDE_APP_PATH" | "CHATGPT_APP_PATH" | "OPENCODE_APP_PATH" | undefined {
|
||||
function profileAppPathLabel(agent: ProfileConfig["agent"]): "CLAUDE_APP_PATH" | "CHATGPT_APP_PATH" | "OPENCODE_APP_PATH" | "WORKBUDDY_APP_PATH" | undefined {
|
||||
if (agent === "claude-code") {
|
||||
return "CLAUDE_APP_PATH";
|
||||
}
|
||||
|
|
@ -1325,6 +1325,9 @@ function profileAppPathLabel(agent: ProfileConfig["agent"]): "CLAUDE_APP_PATH" |
|
|||
if (agent === "opencode") {
|
||||
return "OPENCODE_APP_PATH";
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return "WORKBUDDY_APP_PATH";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -325,6 +325,8 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
|||
"Kimi model is required.": "Kimi model is required.",
|
||||
"Pi": "Pi",
|
||||
"Pi model": "Pi model",
|
||||
"Workbuddy": "Workbuddy",
|
||||
"Workbuddy model": "Workbuddy model",
|
||||
"OpenCode CLI credential was found, but no usable API key was detected.": "OpenCode CLI credential was found, but no usable API key was detected.",
|
||||
"OpenCode CLI login detected. Click Import to add it as a gateway provider.": "OpenCode CLI login detected. Click Import to add it as a gateway provider.",
|
||||
"OpenCode CLI public models detected. No login is required.": "OpenCode CLI public models detected. No login is required.",
|
||||
|
|
@ -602,6 +604,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
|||
"CLAUDE_APP_PATH": "CLAUDE_APP_PATH",
|
||||
"CHATGPT_APP_PATH": "CHATGPT_APP_PATH",
|
||||
"OPENCODE_APP_PATH": "OPENCODE_APP_PATH",
|
||||
"WORKBUDDY_APP_PATH": "WORKBUDDY_APP_PATH",
|
||||
"Drop the app here or paste the executable path": "Drop the app here or paste the executable path",
|
||||
"Tool name": "Tool name",
|
||||
"Third-party tool environment": "Third-party tool environment",
|
||||
|
|
@ -935,6 +938,8 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
|||
"Allowed models": "允许模型",
|
||||
"Pi": "Pi",
|
||||
"Pi model": "Pi 模型",
|
||||
"Workbuddy": "Workbuddy",
|
||||
"Workbuddy model": "Workbuddy 模型",
|
||||
"OpenCode": "OpenCode",
|
||||
"OpenCode model": "OpenCode 模型",
|
||||
"CLI only": "仅 CLI",
|
||||
|
|
@ -1741,6 +1746,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
|||
"CLAUDE_APP_PATH": "CLAUDE_APP_PATH",
|
||||
"CHATGPT_APP_PATH": "CHATGPT_APP_PATH",
|
||||
"OPENCODE_APP_PATH": "OPENCODE_APP_PATH",
|
||||
"WORKBUDDY_APP_PATH": "WORKBUDDY_APP_PATH",
|
||||
"Drop the app here or paste the executable path": "拖动应用到这里,或粘贴可执行文件路径",
|
||||
"Third-party tool environment": "三方工具环境变量",
|
||||
"Environment variable keys are required when values are set.": "设置变量值时必须填写变量名。",
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ export const agentFilterOptions: Array<{ label: string; value: AgentFilterValue
|
|||
{ label: "Kilo CLI", value: "kilo" },
|
||||
{ label: "OpenCode", value: "opencode" },
|
||||
{ label: "Pi", value: "pi" },
|
||||
{ label: "Workbuddy", value: "workbuddy" },
|
||||
{ label: "ZCode", value: "zcode" },
|
||||
{ label: "Claude Design", value: "claude-design" },
|
||||
{ label: "Unknown", value: "unknown" }
|
||||
|
|
@ -136,6 +137,7 @@ export const profileAgentOptions: ProfileAgentOption[] = [
|
|||
{ label: "Kilo CLI", value: "kilo" },
|
||||
{ label: "OpenCode", value: "opencode" },
|
||||
{ label: "Pi", value: "pi" },
|
||||
{ label: "Workbuddy", value: "workbuddy" },
|
||||
{ label: "ZCode", value: "zcode" },
|
||||
{ label: "Claude Design", value: "claude-design" }
|
||||
];
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import grokLogoUrl from "@/assets/agent-logos/grok.ico";
|
|||
import kiloLogoUrl from "@/assets/agent-logos/kilo.svg";
|
||||
import openCodeLogoUrl from "@/assets/agent-logos/opencode.ico";
|
||||
import piLogoUrl from "@/assets/agent-logos/pi.svg";
|
||||
import workbuddyLogoUrl from "@/assets/agent-logos/workbuddy.png";
|
||||
import zcodeLogoUrl from "@/assets/agent-logos/zcode.png";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import {
|
||||
|
|
@ -459,7 +460,7 @@ function profileRoutingConfigFromDraft(draft: AddProfileDraft): ProfileRoutingCo
|
|||
}
|
||||
|
||||
export function createProfileDraft(agent: ProfileConfig["agent"] = "claude-code", name?: string): AddProfileDraft {
|
||||
const surface = agent === "zcode" || agent === "claude-design" ? "app" : "cli";
|
||||
const surface = agent === "workbuddy" || agent === "zcode" || agent === "claude-design" ? "app" : "cli";
|
||||
return {
|
||||
agent,
|
||||
appPath: "",
|
||||
|
|
@ -488,12 +489,15 @@ export function createProfileDraft(agent: ProfileConfig["agent"] = "claude-code"
|
|||
export function profileDraftWithDetectedAppPath(
|
||||
draft: AddProfileDraft,
|
||||
chatgptAppPath?: string,
|
||||
opencodeAppPath?: string
|
||||
opencodeAppPath?: string,
|
||||
workbuddyAppPath?: string
|
||||
): AddProfileDraft {
|
||||
const detectedPath = (draft.agent === "codex"
|
||||
? chatgptAppPath
|
||||
: draft.agent === "opencode"
|
||||
? opencodeAppPath
|
||||
: draft.agent === "workbuddy"
|
||||
? workbuddyAppPath
|
||||
: "")?.trim() || "";
|
||||
if (draft.appPath.trim() || !detectedPath) {
|
||||
return draft;
|
||||
|
|
@ -550,7 +554,7 @@ export function createProfileDraftFromProfile(profile: ProfileConfig, botConfigs
|
|||
surface: "app"
|
||||
};
|
||||
}
|
||||
const surface = profile.agent === "zcode" ? "app" : normalizeProfileSurfaceForForm(profile.surface);
|
||||
const surface = profile.agent === "workbuddy" || profile.agent === "zcode" ? "app" : normalizeProfileSurfaceForForm(profile.surface);
|
||||
return {
|
||||
...createProfileDraft(profile.agent, profile.name),
|
||||
...createProfileRoutingDraft(profile.routing),
|
||||
|
|
@ -565,7 +569,7 @@ export function createProfileDraftFromProfile(profile: ProfileConfig, botConfigs
|
|||
providerId: profile.providerId ?? "claude-code-router",
|
||||
providerName: profile.providerName ?? "Claude Code Router",
|
||||
scope: normalizeProfileFormScope(profile.scope),
|
||||
showAllSessions: profile.agent === "zcode" || profile.agent === "opencode" || profile.agent === "kilo" ? false : Boolean(profile.showAllSessions),
|
||||
showAllSessions: profile.agent === "zcode" || profile.agent === "opencode" || profile.agent === "kilo" || profile.agent === "workbuddy" ? false : Boolean(profile.showAllSessions),
|
||||
surface
|
||||
};
|
||||
}
|
||||
|
|
@ -663,7 +667,7 @@ export function profileConfigFromDraft(
|
|||
...(routing ? { routing } : {}),
|
||||
scope: draft.scope,
|
||||
settingsFile: draft.settingsFile,
|
||||
showAllSessions: draft.agent === "zcode" || draft.agent === "opencode" || draft.agent === "kilo" || draft.agent === "claude-design" ? false : draft.showAllSessions,
|
||||
showAllSessions: draft.agent === "zcode" || draft.agent === "opencode" || draft.agent === "kilo" || draft.agent === "workbuddy" || draft.agent === "claude-design" ? false : draft.showAllSessions,
|
||||
sonnetModel: draft.sonnetModel,
|
||||
smallFastModel: draft.haikuModel || draft.smallFastModel,
|
||||
surface: draft.surface
|
||||
|
|
@ -1267,9 +1271,9 @@ export function profileSummaryItems(
|
|||
}
|
||||
|
||||
return [
|
||||
{ label: t(profile.agent === "kilo" ? "Kilo model" : "Model"), value: modelValue },
|
||||
{ label: t(profile.agent === "kilo" ? "Kilo model" : profile.agent === "workbuddy" ? "Workbuddy model" : "Model"), value: modelValue },
|
||||
{ label: t("Provider ID"), value: profile.providerId ?? "claude-code-router" },
|
||||
...(profile.agent === "zcode" || profile.agent === "opencode" || profile.agent === "kilo" || !profile.showAllSessions ? [] : [{ label: t("Show all sessions"), value: t("Enabled") }]),
|
||||
...(profile.agent === "zcode" || profile.agent === "opencode" || profile.agent === "kilo" || profile.agent === "workbuddy" || !profile.showAllSessions ? [] : [{ label: t("Show all sessions"), value: t("Enabled") }]),
|
||||
...managedCompactItems,
|
||||
...routingSummaryItems,
|
||||
...appPathSummaryItems,
|
||||
|
|
@ -1363,7 +1367,7 @@ export function normalizeProfileItem(profile: ProfileConfig, index: number): Pro
|
|||
providerName: profile.providerName?.trim() || "Claude Code Router",
|
||||
...(routing ? { routing } : {}),
|
||||
scope,
|
||||
showAllSessions: agent === "zcode" || agent === "opencode" || agent === "kilo" ? false : Boolean(profile.showAllSessions),
|
||||
showAllSessions: agent === "zcode" || agent === "opencode" || agent === "kilo" || agent === "workbuddy" ? false : Boolean(profile.showAllSessions),
|
||||
surface
|
||||
};
|
||||
}
|
||||
|
|
@ -1434,6 +1438,8 @@ export function normalizeUnknownProfileItem(value: Record<string, unknown>, inde
|
|||
? "kilo"
|
||||
: rawAgent === "pi" || rawAgent === "pi-agent" || rawAgent === "pi agent" || rawAgent === "pi-coding-agent" || rawAgent === "pi coding agent"
|
||||
? "pi"
|
||||
: rawAgent === "workbuddy" || rawAgent === "work-buddy" || rawAgent === "work buddy" || rawAgent === "workbuddy-agent" || rawAgent === "workbuddy agent"
|
||||
? "workbuddy"
|
||||
: rawAgent === "zcode" || rawAgent === "z-code" || rawAgent === "z code"
|
||||
? "zcode"
|
||||
: rawAgent === "claude-design" || rawAgent === "claude design" || rawAgent === "design"
|
||||
|
|
@ -1530,6 +1536,9 @@ function readUnknownProfileAppPath(value: Record<string, unknown>, agent: Profil
|
|||
if (agent === "opencode") {
|
||||
return readUnknownString(value, "openCodeAppPath", "opencodeAppPath", "opencode_app_path");
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return readUnknownString(value, "workbuddyAppPath", "workbuddy_app_path", "workBuddyAppPath", "work_buddy_app_path");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -1577,6 +1586,9 @@ export function profileAgentLabel(agent: ProfileConfig["agent"]): string {
|
|||
if (agent === "pi") {
|
||||
return "Pi";
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return "Workbuddy";
|
||||
}
|
||||
if (agent === "opencode") {
|
||||
return "OpenCode";
|
||||
}
|
||||
|
|
@ -1607,7 +1619,7 @@ export function profileSurfaceLabel(surface: ProfileSurface): string {
|
|||
}
|
||||
|
||||
export function profileOpenSurfaces(profile: ProfileConfig): ProfileOpenSurface[] {
|
||||
if (profile.agent === "zcode" || profile.agent === "claude-design") {
|
||||
if (profile.agent === "workbuddy" || profile.agent === "zcode" || profile.agent === "claude-design") {
|
||||
return ["app"];
|
||||
}
|
||||
if (profile.agent === "grok" || profile.agent === "kimi" || profile.agent === "pi" || profile.agent === "kilo") {
|
||||
|
|
@ -1623,7 +1635,7 @@ export function profileOpenSurfaces(profile: ProfileConfig): ProfileOpenSurface[
|
|||
return ["cli", "app"];
|
||||
}
|
||||
|
||||
export function profileOpenCommandFallback(profile: ProfileConfig, surface: ProfileOpenSurface = profile.agent === "zcode" || profile.agent === "claude-design" ? "app" : "cli"): string {
|
||||
export function profileOpenCommandFallback(profile: ProfileConfig, surface: ProfileOpenSurface = profile.agent === "workbuddy" || profile.agent === "zcode" || profile.agent === "claude-design" ? "app" : "cli"): string {
|
||||
const profileRef = profile.name.trim() || profile.id;
|
||||
return ["ccr", shellCommandQuote(profileRef), ...(surface === "app" ? ["app"] : [])].join(" ");
|
||||
}
|
||||
|
|
@ -1659,19 +1671,22 @@ export function profileAgentLogoUrl(agent: ProfileConfig["agent"]): string {
|
|||
if (agent === "kilo") {
|
||||
return kiloLogoUrl;
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return workbuddyLogoUrl;
|
||||
}
|
||||
return codexLogoUrl;
|
||||
}
|
||||
|
||||
function normalizeCodexCompatibleAgent(agent: ProfileConfig["agent"]): "codex" | "kilo" | "opencode" | "zcode" {
|
||||
return agent === "zcode" ? "zcode" : agent === "opencode" ? "opencode" : agent === "kilo" ? "kilo" : "codex";
|
||||
function normalizeCodexCompatibleAgent(agent: ProfileConfig["agent"]): "codex" | "kilo" | "opencode" | "workbuddy" | "zcode" {
|
||||
return agent === "zcode" ? "zcode" : agent === "opencode" ? "opencode" : agent === "kilo" ? "kilo" : agent === "workbuddy" ? "workbuddy" : "codex";
|
||||
}
|
||||
|
||||
function normalizeProfileAgent(agent: ProfileConfig["agent"]): ProfileConfig["agent"] {
|
||||
return agent === "claude-design" ? "claude-design" : agent === "zcode" ? "zcode" : agent === "opencode" ? "opencode" : agent === "kilo" ? "kilo" : agent === "pi" ? "pi" : agent === "grok" ? "grok" : agent === "kimi" ? "kimi" : agent === "codex" ? "codex" : "claude-code";
|
||||
return agent === "claude-design" ? "claude-design" : agent === "zcode" ? "zcode" : agent === "workbuddy" ? "workbuddy" : agent === "opencode" ? "opencode" : agent === "kilo" ? "kilo" : agent === "pi" ? "pi" : agent === "grok" ? "grok" : agent === "kimi" ? "kimi" : agent === "codex" ? "codex" : "claude-code";
|
||||
}
|
||||
|
||||
function normalizeProfileSurfaceForAgent(agent: ProfileConfig["agent"], surface: unknown): ProfileSurface {
|
||||
return agent === "zcode" || agent === "claude-design" ? "app" : agent === "grok" || agent === "kimi" || agent === "pi" || agent === "kilo" ? "cli" : normalizeProfileSurface(surface);
|
||||
return agent === "workbuddy" || agent === "zcode" || agent === "claude-design" ? "app" : agent === "grok" || agent === "kimi" || agent === "pi" || agent === "kilo" ? "cli" : normalizeProfileSurface(surface);
|
||||
}
|
||||
|
||||
function defaultCodexConfigFile(agent: ProfileConfig["agent"]): string {
|
||||
|
|
@ -1680,7 +1695,9 @@ function defaultCodexConfigFile(agent: ProfileConfig["agent"]): string {
|
|||
: agent === "opencode"
|
||||
? "~/.config/opencode/opencode.jsonc"
|
||||
: agent === "kilo"
|
||||
? "~/.config/kilo/kilo.jsonc"
|
||||
? "~/.config/kilo/kilo.jsonc"
|
||||
: agent === "workbuddy"
|
||||
? "~/.workbuddy/config.toml"
|
||||
: agent === "pi"
|
||||
? "~/.pi/agent"
|
||||
: agent === "claude-design"
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ export function logSelectOptions(label: string, values: string[], selected: stri
|
|||
}
|
||||
|
||||
export function normalizeAgentFilterValue(value: string): AgentFilterValue {
|
||||
return value === "claude-code" || value === "codex" || value === "grok" || value === "kimi" || value === "kilo" || value === "opencode" || value === "pi" || value === "zcode" || value === "claude-design" || value === "unknown" ? value : "all";
|
||||
return value === "claude-code" || value === "codex" || value === "grok" || value === "kimi" || value === "kilo" || value === "opencode" || value === "pi" || value === "workbuddy" || value === "zcode" || value === "claude-design" || value === "unknown" ? value : "all";
|
||||
}
|
||||
|
||||
export function agentKindLabel(agent: AgentKind): string {
|
||||
|
|
@ -208,6 +208,9 @@ export function agentKindLabel(agent: AgentKind): string {
|
|||
if (agent === "pi") {
|
||||
return "Pi";
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return "Workbuddy";
|
||||
}
|
||||
if (agent === "zcode") {
|
||||
return "ZCode";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|||
import type { ProfileConfig } from "@ccr/core/contracts/app.ts";
|
||||
import { AddProfileForm, DeleteProfileDialog, ProfileView } from "@ccr/ui/pages/home/components/profiles.tsx";
|
||||
import { AppI18nContext, appCopy } from "@ccr/ui/pages/home/shared/i18n.tsx";
|
||||
import { createProfileDraft, createProfileDraftFromProfile, isProfileDraftSubmittable, normalizeUnknownProfileItem, profileConfigFromDraft, profileDraftWithDetectedAppPath, profileSummaryItems } from "@ccr/ui/pages/home/shared/profiles.ts";
|
||||
import { createProfileDraft, createProfileDraftFromProfile, isProfileDraftSubmittable, normalizeUnknownProfileItem, profileAgentLogoUrl, profileConfigFromDraft, profileDraftWithDetectedAppPath, profileSummaryItems } from "@ccr/ui/pages/home/shared/profiles.ts";
|
||||
import { appConfigFixture } from "../fixtures/index.ts";
|
||||
|
||||
const profile: ProfileConfig = {
|
||||
|
|
@ -440,6 +440,13 @@ test("detected OPENCODE_APP_PATH is used as the OpenCode profile default", () =>
|
|||
assert.equal(profileDraftWithDetectedAppPath({ ...draft, appPath: "/custom/opencode" }, undefined, detectedPath).appPath, "/custom/opencode");
|
||||
});
|
||||
|
||||
test("detected WORKBUDDY_APP_PATH is used as the Workbuddy profile default", () => {
|
||||
const detectedPath = "/Applications/WorkBuddy AI.app/Contents/MacOS/Electron";
|
||||
const draft = profileDraftWithDetectedAppPath(createProfileDraft("workbuddy"), undefined, undefined, detectedPath);
|
||||
assert.equal(draft.appPath, detectedPath);
|
||||
assert.equal(profileDraftWithDetectedAppPath({ ...draft, appPath: "/custom/workbuddy" }, undefined, undefined, detectedPath).appPath, "/custom/workbuddy");
|
||||
});
|
||||
|
||||
test("Grok CLI profile defaults to a CCR-scoped CLI entry", () => {
|
||||
const draft = createProfileDraft("grok");
|
||||
|
||||
|
|
@ -575,3 +582,46 @@ test("Kilo CLI profiles support local CLI configuration", () => {
|
|||
assert.equal(profile?.showAllSessions, false);
|
||||
assert.equal(profile?.surface, "cli");
|
||||
});
|
||||
|
||||
test("Workbuddy profiles support local App configuration", () => {
|
||||
const config = appConfigFixture();
|
||||
const draft = createProfileDraft("workbuddy");
|
||||
assert.equal(draft.name, "Workbuddy");
|
||||
assert.equal(draft.configFile, "~/.workbuddy/config.toml");
|
||||
assert.equal(draft.surface, "app");
|
||||
assert.equal(isProfileDraftSubmittable(draft), true);
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
<AddProfileForm
|
||||
botConfigs={config.botConfigs ?? []}
|
||||
draft={draft}
|
||||
error=""
|
||||
mode="edit"
|
||||
onChange={() => undefined}
|
||||
onCreateBot={() => undefined}
|
||||
providers={config.Providers}
|
||||
virtualModelProfiles={config.virtualModelProfiles}
|
||||
/>
|
||||
);
|
||||
assert.match(html, /App only/);
|
||||
assert.doesNotMatch(html, /CLI only/);
|
||||
assert.match(html, /WORKBUDDY_APP_PATH/);
|
||||
|
||||
const profile = normalizeUnknownProfileItem({
|
||||
agent: "work-buddy",
|
||||
enabled: true,
|
||||
id: "workbuddy-work",
|
||||
model: "Provider/model",
|
||||
name: "Workbuddy Work",
|
||||
providerId: "claude-code-router",
|
||||
scope: "global",
|
||||
showAllSessions: true,
|
||||
surface: "app"
|
||||
}, 0);
|
||||
assert.equal(profile?.agent, "workbuddy");
|
||||
assert.equal(profile?.configFile, "~/.workbuddy/config.toml");
|
||||
assert.equal(profile?.showAllSessions, false);
|
||||
assert.equal(profile?.surface, "app");
|
||||
assert.match(profileAgentLogoUrl("workbuddy"), /workbuddy/i);
|
||||
assert.notEqual(profileAgentLogoUrl("workbuddy"), profileAgentLogoUrl("codex"));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { chromium, expect, test, type Browser, type Page } from "@playwright/test";
|
||||
import { spawn, spawnSync, type ChildProcessByStdio } from "node:child_process";
|
||||
import electronModule from "electron";
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
|
@ -143,6 +143,114 @@ test("starts the managed gateway from IPC without runtime config JSON", async ()
|
|||
expect(readGatewayRuntimeMarker(path.join(configDir, "config.sqlite"))).toBeUndefined();
|
||||
});
|
||||
|
||||
test("applies Workbuddy profiles through the desktop bridge", async () => {
|
||||
const current = requireRuntime();
|
||||
const profileId = "workbuddy-e2e";
|
||||
const result = await current.mainPage.evaluate(async (input) => {
|
||||
const config = await window.ccr?.getConfig();
|
||||
if (!config) {
|
||||
throw new Error("Config bridge is unavailable.");
|
||||
}
|
||||
config.APIKEYS = [];
|
||||
config.Providers = [{
|
||||
api_base_url: "http://127.0.0.1:9/v1",
|
||||
api_key: "e2e-provider-key",
|
||||
id: "workbuddy-e2e-provider",
|
||||
models: ["gpt-5-codex"],
|
||||
name: "Workbuddy E2E Provider",
|
||||
type: "openai_responses"
|
||||
}];
|
||||
config.preferredProvider = "Workbuddy E2E Provider";
|
||||
config.gateway.enabled = false;
|
||||
config.profile = {
|
||||
...config.profile,
|
||||
enabled: true,
|
||||
profiles: [{
|
||||
agent: "workbuddy",
|
||||
cliMiddleware: true,
|
||||
codexCliPath: "",
|
||||
codexHome: "",
|
||||
configFile: "~/.workbuddy/config.toml",
|
||||
configFormat: "separate_profile_files",
|
||||
enabled: true,
|
||||
env: {},
|
||||
id: input.profileId,
|
||||
managedCompact: false,
|
||||
model: "Workbuddy E2E Provider/gpt-5-codex",
|
||||
name: "Workbuddy E2E",
|
||||
providerId: "claude-code-router",
|
||||
providerName: "Claude Code Router",
|
||||
showAllSessions: true,
|
||||
scope: "ccr",
|
||||
surface: "app"
|
||||
}]
|
||||
};
|
||||
|
||||
const saved = await window.ccr?.saveConfig(config, { applyProfile: false });
|
||||
const applyResult = await window.ccr?.applyProfile();
|
||||
return { applyResult, saved };
|
||||
}, { profileId });
|
||||
|
||||
expect(result.saved?.profile.profiles).toHaveLength(1);
|
||||
expect(result.saved?.profile.profiles[0]?.agent).toBe("workbuddy");
|
||||
expect(result.saved?.profile.profiles[0]?.showAllSessions).toBe(false);
|
||||
expect(result.saved?.profile.profiles[0]?.surface).toBe("app");
|
||||
|
||||
const workbuddyStatus = result.applyResult?.clients.find((client) => client.client === "workbuddy");
|
||||
expect(workbuddyStatus?.ok, workbuddyStatus?.message).toBe(true);
|
||||
|
||||
const configDir = path.join(current.testHome, ".claude-code-router");
|
||||
const profileHome = path.join(configDir, "profiles", profileId, "workbuddy");
|
||||
const configFile = path.join(profileHome, "config.toml");
|
||||
const launcherFile = path.join(
|
||||
configDir,
|
||||
"bin",
|
||||
process.platform === "win32"
|
||||
? `ccr-codex-cli-stdio-${profileId}.cmd`
|
||||
: `ccr-codex-cli-stdio-${profileId}`
|
||||
);
|
||||
expect(workbuddyStatus?.path).toBe(configFile);
|
||||
|
||||
const toml = readFileSync(configFile, "utf8");
|
||||
expect(toml).toContain('model_provider = "claude-code-router"');
|
||||
expect(toml).toContain('model = "Workbuddy E2E Provider/gpt-5-codex"');
|
||||
expect(toml).toContain(`model_catalog_json = "${path.join(profileHome, "ccr-model-catalog.json")}"`);
|
||||
expect(toml).toContain(`base_url = "http://${host}:${current.gatewayPort}/v1"`);
|
||||
expect(toml).toContain('wire_api = "responses"');
|
||||
expect(toml).not.toContain("show_all_sessions = true");
|
||||
|
||||
const workbuddyModelsFile = path.join(profileHome, "models.json");
|
||||
expect(existsSync(workbuddyModelsFile)).toBe(true);
|
||||
const workbuddyModels = JSON.parse(readFileSync(workbuddyModelsFile, "utf8"));
|
||||
expect(workbuddyModels.availableModels).toEqual(["Workbuddy E2E Provider/gpt-5-codex"]);
|
||||
expect(workbuddyModels.models).toHaveLength(1);
|
||||
expect(workbuddyModels.models[0]).toMatchObject({
|
||||
apiKey: "${CCR_PROFILE_API_KEY}",
|
||||
id: "Workbuddy E2E Provider/gpt-5-codex",
|
||||
supportsToolCall: true,
|
||||
tags: ["chat", "custom"],
|
||||
url: `http://${host}:${current.gatewayPort}/v1`,
|
||||
vendor: "Workbuddy E2E Provider"
|
||||
});
|
||||
|
||||
const launcher = readFileSync(launcherFile, "utf8");
|
||||
expect(launcher).toContain(profileHome);
|
||||
expect(launcher).toContain("WORKBUDDY_HOME");
|
||||
expect(launcher).toContain("WORKBUDDY_CONFIG_DIR");
|
||||
expect(launcher).toContain("CODEBUDDY_CONFIG_DIR");
|
||||
expect(launcher).toContain("CCR_REAL_CODEX_CLI_PATH");
|
||||
expect(launcher).toContain("codebuddy");
|
||||
|
||||
const virtualAuthFile = workbuddyVirtualAuthFile(profileHome);
|
||||
expect(existsSync(virtualAuthFile)).toBe(true);
|
||||
const virtualSession = JSON.parse(readFileSync(virtualAuthFile, "utf8"));
|
||||
expect(virtualSession.auth.accessToken).toBe("ccr-local-profile");
|
||||
expect(virtualSession.auth.domain).toBe("www.workbuddy.ai");
|
||||
expect(virtualSession.account.uid).toBe("ccr-local-profile");
|
||||
expect(virtualSession.accounts).toHaveLength(1);
|
||||
expect(virtualSession.allAccounts).toHaveLength(1);
|
||||
});
|
||||
|
||||
async function startElectronOverCdp(): Promise<ElectronCdpRuntime> {
|
||||
const cdpPort = await findAvailablePort();
|
||||
const gatewayPort = await findAvailablePort();
|
||||
|
|
@ -307,6 +415,16 @@ function readSqliteRows<Row>(file: string, query: string): Row[] {
|
|||
return JSON.parse(output || "[]") as Row[];
|
||||
}
|
||||
|
||||
function workbuddyVirtualAuthFile(profileHome: string): string {
|
||||
const virtualHome = path.join(profileHome, ".claude-code-router", "workbuddy-app-home");
|
||||
const sharedAuthSegments = process.platform === "darwin"
|
||||
? ["Library", "Application Support", "CodeBuddyExtension", "Data", "Public", "auth"]
|
||||
: process.platform === "win32"
|
||||
? ["AppData", "Local", "CodeBuddyExtension", "Data", "Public", "auth"]
|
||||
: [".local", "share", "CodeBuddyExtension", "Data", "Public", "auth"];
|
||||
return path.join(virtualHome, ...sharedAuthSegments, "workbuddy-desktop-ai.info");
|
||||
}
|
||||
|
||||
function runElectronNode(script: string, file: string, query = ""): string {
|
||||
const result = spawnSync(electronExecutable, ["-e", script], {
|
||||
cwd: projectRoot,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue