feat: enhance agent management with project-level support
- Added support for project-level agents, allowing agents to be created, updated, and deleted within a specified working directory. - Introduced `getAgentScope` function to determine the scope of agents based on their existence in project or user directories. - Updated `getAgentSources`, `createAgent`, `updateAgent`, and `deleteAgent` functions to handle project-level paths and configurations. - Enhanced logging to provide clearer information about agent operations, including scope and working directory. - Refactored configuration reading and writing to accommodate project-specific configurations alongside user-level settings.
This commit is contained in:
+1
-1
@@ -10,7 +10,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Added queued message mode with chips, batching, and idle auto‑send (including attachments).
|
- Added queued message mode with chips, batching, and idle auto‑send (including attachments).
|
||||||
- Added queue mode toggle to OpenChamber settings (chat section) with persistence across runtimes.
|
- Added queue mode toggle to OpenChamber settings (chat section) with persistence across runtimes.
|
||||||
- Fixed scroll position persistence for active conversation turns across session switches.
|
- Fixed scroll position persistence for active conversation turns across session switches.
|
||||||
- Refactored command management with ability to configure project/user scoped commands.
|
- Refactored Agents/Commands management with ability to configure project/user scopes.
|
||||||
|
|
||||||
|
|
||||||
## [1.3.7] - 2025-12-28
|
## [1.3.7] - 2025-12-28
|
||||||
|
|||||||
@@ -1190,18 +1190,27 @@ async fn handle_agent_route(
|
|||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<Response<Body>, StatusCode> {
|
) -> Result<Response<Body>, StatusCode> {
|
||||||
|
// Get working directory for project-level agent detection
|
||||||
|
let working_directory = state.opencode.get_working_directory();
|
||||||
|
|
||||||
match method {
|
match method {
|
||||||
Method::GET => {
|
Method::GET => {
|
||||||
match opencode_config::get_agent_sources(&name).await {
|
match opencode_config::get_agent_sources(&name, Some(&working_directory)).await {
|
||||||
Ok(sources) => Ok(json_response(
|
Ok(sources) => {
|
||||||
StatusCode::OK,
|
let scope = sources.md.scope.clone().map(|s| match s {
|
||||||
ConfigMetadataResponse {
|
opencode_config::Scope::User => opencode_config::CommandScope::User,
|
||||||
name,
|
opencode_config::Scope::Project => opencode_config::CommandScope::Project,
|
||||||
is_built_in: !sources.md.exists && !sources.json.exists,
|
});
|
||||||
scope: None,
|
Ok(json_response(
|
||||||
sources,
|
StatusCode::OK,
|
||||||
},
|
ConfigMetadataResponse {
|
||||||
)),
|
name,
|
||||||
|
is_built_in: !sources.md.exists && !sources.json.exists,
|
||||||
|
scope,
|
||||||
|
sources,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("[desktop:config] Failed to read agent sources: {}", err);
|
error!("[desktop:config] Failed to read agent sources: {}", err);
|
||||||
Ok(config_error_response(
|
Ok(config_error_response(
|
||||||
@@ -1216,8 +1225,17 @@ async fn handle_agent_route(
|
|||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(resp) => return Ok(resp),
|
Err(resp) => return Ok(resp),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extract scope from payload if present
|
||||||
|
let scope = payload.get("scope")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|s| match s {
|
||||||
|
"project" => Some(opencode_config::AgentScope::Project),
|
||||||
|
"user" => Some(opencode_config::AgentScope::User),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
match opencode_config::create_agent(&name, &payload).await {
|
match opencode_config::create_agent(&name, &payload, Some(&working_directory), scope).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Err(resp) =
|
if let Err(resp) =
|
||||||
refresh_opencode_after_config_change(state, "agent creation").await
|
refresh_opencode_after_config_change(state, "agent creation").await
|
||||||
@@ -1253,7 +1271,7 @@ async fn handle_agent_route(
|
|||||||
Err(resp) => return Ok(resp),
|
Err(resp) => return Ok(resp),
|
||||||
};
|
};
|
||||||
|
|
||||||
match opencode_config::update_agent(&name, &payload).await {
|
match opencode_config::update_agent(&name, &payload, Some(&working_directory)).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Err(resp) =
|
if let Err(resp) =
|
||||||
refresh_opencode_after_config_change(state, "agent update").await
|
refresh_opencode_after_config_change(state, "agent update").await
|
||||||
@@ -1283,7 +1301,7 @@ async fn handle_agent_route(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Method::DELETE => match opencode_config::delete_agent(&name).await {
|
Method::DELETE => match opencode_config::delete_agent(&name, Some(&working_directory)).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Err(resp) =
|
if let Err(resp) =
|
||||||
refresh_opencode_after_config_change(state, "agent deletion").await
|
refresh_opencode_after_config_change(state, "agent deletion").await
|
||||||
@@ -1329,7 +1347,10 @@ async fn handle_command_route(
|
|||||||
Method::GET => {
|
Method::GET => {
|
||||||
match opencode_config::get_command_sources(&name, Some(&working_directory)).await {
|
match opencode_config::get_command_sources(&name, Some(&working_directory)).await {
|
||||||
Ok(sources) => {
|
Ok(sources) => {
|
||||||
let scope = sources.md.scope.clone();
|
let scope = sources.md.scope.clone().map(|s| match s {
|
||||||
|
opencode_config::Scope::User => opencode_config::CommandScope::User,
|
||||||
|
opencode_config::Scope::Project => opencode_config::CommandScope::Project,
|
||||||
|
});
|
||||||
Ok(json_response(
|
Ok(json_response(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
ConfigMetadataResponse {
|
ConfigMetadataResponse {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,22 +4,21 @@ import { ButtonLarge } from '@/components/ui/button-large';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { RiAddLine, RiAiAgentFill, RiAiAgentLine, RiDeleteBinLine, RiFileCopyLine, RiMore2Line, RiRobot2Line, RiRobotLine } from '@remixicon/react';
|
import { RiAddLine, RiAiAgentFill, RiAiAgentLine, RiDeleteBinLine, RiFileCopyLine, RiMore2Line, RiRobot2Line, RiRobotLine, RiRestartLine, RiEditLine } from '@remixicon/react';
|
||||||
import { useAgentsStore, isAgentBuiltIn, isAgentHidden } from '@/stores/useAgentsStore';
|
import { useAgentsStore, isAgentBuiltIn, isAgentHidden, type AgentScope } from '@/stores/useAgentsStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
@@ -28,309 +27,451 @@ import type { Agent } from '@opencode-ai/sdk';
|
|||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
|
|
||||||
interface AgentsSidebarProps {
|
interface AgentsSidebarProps {
|
||||||
onItemSelect?: () => void;
|
onItemSelect?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) => {
|
export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) => {
|
||||||
const [newAgentName, setNewAgentName] = React.useState('');
|
const [renameDialogAgent, setRenameDialogAgent] = React.useState<Agent | null>(null);
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = React.useState(false);
|
const [renameNewName, setRenameNewName] = React.useState('');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedAgentName,
|
selectedAgentName,
|
||||||
agents,
|
agents,
|
||||||
setSelectedAgent,
|
setSelectedAgent,
|
||||||
deleteAgent,
|
setAgentDraft,
|
||||||
loadAgents,
|
createAgent,
|
||||||
} = useAgentsStore();
|
deleteAgent,
|
||||||
|
loadAgents,
|
||||||
|
} = useAgentsStore();
|
||||||
|
|
||||||
const { setSidebarOpen } = useUIStore();
|
const { setSidebarOpen } = useUIStore();
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile } = useDeviceInfo();
|
||||||
|
|
||||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
return typeof window.opencodeDesktop !== 'undefined';
|
return typeof window.opencodeDesktop !== 'undefined';
|
||||||
|
});
|
||||||
|
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
loadAgents();
|
||||||
|
}, [loadAgents]);
|
||||||
|
|
||||||
|
const bgClass = isDesktopRuntime
|
||||||
|
? 'bg-transparent'
|
||||||
|
: isVSCode
|
||||||
|
? 'bg-background'
|
||||||
|
: 'bg-sidebar';
|
||||||
|
|
||||||
|
const handleCreateNew = () => {
|
||||||
|
// Generate unique name
|
||||||
|
const baseName = 'new-agent';
|
||||||
|
let newName = baseName;
|
||||||
|
let counter = 1;
|
||||||
|
while (agents.some((a) => a.name === newName)) {
|
||||||
|
newName = `${baseName}-${counter}`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set draft and open the page for editing
|
||||||
|
setAgentDraft({ name: newName, scope: 'user' });
|
||||||
|
setSelectedAgent(newName);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
setSidebarOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAgent = async (agent: Agent) => {
|
||||||
|
if (isAgentBuiltIn(agent)) {
|
||||||
|
toast.error('Built-in agents cannot be deleted');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.confirm(`Are you sure you want to delete agent "${agent.name}"?`)) {
|
||||||
|
const success = await deleteAgent(agent.name);
|
||||||
|
if (success) {
|
||||||
|
toast.success(`Agent "${agent.name}" deleted successfully`);
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to delete agent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetAgent = async (agent: Agent) => {
|
||||||
|
if (!isAgentBuiltIn(agent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.confirm(`Are you sure you want to reset agent "${agent.name}" to its default configuration?`)) {
|
||||||
|
const success = await deleteAgent(agent.name);
|
||||||
|
if (success) {
|
||||||
|
toast.success(`Agent "${agent.name}" reset to default`);
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to reset agent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDuplicateAgent = (agent: Agent) => {
|
||||||
|
const baseName = agent.name;
|
||||||
|
let copyNumber = 1;
|
||||||
|
let newName = `${baseName}-copy`;
|
||||||
|
|
||||||
|
while (agents.some((a) => a.name === newName)) {
|
||||||
|
copyNumber++;
|
||||||
|
newName = `${baseName}-copy-${copyNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set draft with prefilled values from source agent
|
||||||
|
const extAgent = agent as Agent & { scope?: AgentScope };
|
||||||
|
// Convert model object to string if needed (SDK type vs API type difference)
|
||||||
|
const modelStr = typeof agent.model === 'string'
|
||||||
|
? agent.model
|
||||||
|
: agent.model?.providerID && agent.model?.modelID
|
||||||
|
? `${agent.model.providerID}/${agent.model.modelID}`
|
||||||
|
: undefined;
|
||||||
|
const draftAgent = agent as Agent & { disable?: boolean };
|
||||||
|
setAgentDraft({
|
||||||
|
name: newName,
|
||||||
|
scope: extAgent.scope || 'user',
|
||||||
|
description: agent.description,
|
||||||
|
model: modelStr,
|
||||||
|
temperature: agent.temperature,
|
||||||
|
top_p: agent.topP,
|
||||||
|
prompt: agent.prompt,
|
||||||
|
mode: agent.mode,
|
||||||
|
tools: agent.tools,
|
||||||
|
permission: agent.permission,
|
||||||
|
disable: draftAgent.disable,
|
||||||
|
});
|
||||||
|
setSelectedAgent(newName);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
setSidebarOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenRenameDialog = (agent: Agent) => {
|
||||||
|
setRenameNewName(agent.name);
|
||||||
|
setRenameDialogAgent(agent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameAgent = async () => {
|
||||||
|
if (!renameDialogAgent) return;
|
||||||
|
|
||||||
|
const sanitizedName = renameNewName.trim().replace(/\s+/g, '-');
|
||||||
|
|
||||||
|
if (!sanitizedName) {
|
||||||
|
toast.error('Agent name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitizedName === renameDialogAgent.name) {
|
||||||
|
setRenameDialogAgent(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agents.some((a) => a.name === sanitizedName)) {
|
||||||
|
toast.error('An agent with this name already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new agent with new name and all existing config
|
||||||
|
// Convert model object to string if needed (SDK type vs API type difference)
|
||||||
|
const renameModelStr = typeof renameDialogAgent.model === 'string'
|
||||||
|
? renameDialogAgent.model
|
||||||
|
: renameDialogAgent.model?.providerID && renameDialogAgent.model?.modelID
|
||||||
|
? `${renameDialogAgent.model.providerID}/${renameDialogAgent.model.modelID}`
|
||||||
|
: undefined;
|
||||||
|
const renameExt = renameDialogAgent as Agent & { scope?: AgentScope; disable?: boolean };
|
||||||
|
const success = await createAgent({
|
||||||
|
name: sanitizedName,
|
||||||
|
description: renameDialogAgent.description,
|
||||||
|
model: renameModelStr,
|
||||||
|
temperature: renameDialogAgent.temperature,
|
||||||
|
top_p: renameDialogAgent.topP,
|
||||||
|
prompt: renameDialogAgent.prompt,
|
||||||
|
mode: renameDialogAgent.mode,
|
||||||
|
tools: renameDialogAgent.tools,
|
||||||
|
permission: renameDialogAgent.permission,
|
||||||
|
disable: renameExt.disable,
|
||||||
|
scope: renameExt.scope,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
if (success) {
|
||||||
|
// Delete old agent
|
||||||
|
const deleteSuccess = await deleteAgent(renameDialogAgent.name);
|
||||||
|
if (deleteSuccess) {
|
||||||
|
toast.success(`Agent renamed to "${sanitizedName}"`);
|
||||||
|
setSelectedAgent(sanitizedName);
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to remove old agent after rename');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to rename agent');
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
setRenameDialogAgent(null);
|
||||||
if (typeof window === 'undefined') return;
|
};
|
||||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
const getAgentModeIcon = (mode?: string) => {
|
||||||
loadAgents();
|
switch (mode) {
|
||||||
}, [loadAgents]);
|
case 'primary':
|
||||||
|
return <RiAiAgentLine className="h-3 w-3 text-primary" />;
|
||||||
|
case 'all':
|
||||||
|
return <RiAiAgentFill className="h-3 w-3 text-primary" />;
|
||||||
|
case 'subagent':
|
||||||
|
return <RiRobotLine className="h-3 w-3 text-primary" />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const bgClass = isDesktopRuntime
|
// Filter out hidden agents (internal agents like title, compaction, summary)
|
||||||
? 'bg-transparent'
|
const visibleAgents = agents.filter((agent) => !isAgentHidden(agent));
|
||||||
: isVSCode
|
const builtInAgents = visibleAgents.filter(isAgentBuiltIn);
|
||||||
? 'bg-background'
|
const customAgents = visibleAgents.filter((agent) => !isAgentBuiltIn(agent));
|
||||||
: 'bg-sidebar';
|
|
||||||
|
|
||||||
const handleCreateAgent = () => {
|
return (
|
||||||
if (!newAgentName.trim()) {
|
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||||
toast.error('Agent name is required');
|
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||||
return;
|
<div className="flex items-center justify-between gap-2">
|
||||||
}
|
<span className="typography-meta text-muted-foreground">Total {visibleAgents.length}</span>
|
||||||
|
<Button
|
||||||
if (agents.some((agent) => agent.name === newAgentName)) {
|
type="button"
|
||||||
toast.error('An agent with this name already exists');
|
variant="ghost"
|
||||||
return;
|
size="icon"
|
||||||
}
|
className="h-7 w-7 -my-1 text-muted-foreground"
|
||||||
|
onClick={handleCreateNew}
|
||||||
setSelectedAgent(newAgentName);
|
>
|
||||||
setNewAgentName('');
|
<RiAddLine className="size-4" />
|
||||||
setIsCreateDialogOpen(false);
|
</Button>
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
setSidebarOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteAgent = async (agent: Agent) => {
|
|
||||||
if (isAgentBuiltIn(agent)) {
|
|
||||||
toast.error('Built-in agents cannot be deleted');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.confirm(`Are you sure you want to delete agent "${agent.name}"?`)) {
|
|
||||||
const success = await deleteAgent(agent.name);
|
|
||||||
if (success) {
|
|
||||||
toast.success(`Agent "${agent.name}" deleted successfully`);
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to delete agent');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDuplicateAgent = (agent: Agent) => {
|
|
||||||
const baseName = agent.name;
|
|
||||||
let copyNumber = 1;
|
|
||||||
let newName = `${baseName} Copy`;
|
|
||||||
|
|
||||||
while (agents.some((a) => a.name === newName)) {
|
|
||||||
copyNumber++;
|
|
||||||
newName = `${baseName} Copy ${copyNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedAgent(newName);
|
|
||||||
setIsCreateDialogOpen(false);
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
setSidebarOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAgentModeIcon = (mode?: string) => {
|
|
||||||
switch (mode) {
|
|
||||||
case 'primary':
|
|
||||||
return <RiAiAgentLine className="h-3 w-3 text-primary" />;
|
|
||||||
case 'all':
|
|
||||||
return <RiAiAgentFill className="h-3 w-3 text-primary" />;
|
|
||||||
case 'subagent':
|
|
||||||
return <RiRobotLine className="h-3 w-3 text-primary" />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter out hidden agents (internal agents like title, compaction, summary)
|
|
||||||
const visibleAgents = agents.filter((agent) => !isAgentHidden(agent));
|
|
||||||
const builtInAgents = visibleAgents.filter(isAgentBuiltIn);
|
|
||||||
const customAgents = visibleAgents.filter((agent) => !isAgentBuiltIn(agent));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
|
||||||
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className="typography-meta text-muted-foreground">Total {visibleAgents.length}</span>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 -my-1 text-muted-foreground">
|
|
||||||
<RiAddLine className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-1 px-3 py-2 overflow-x-hidden">
|
|
||||||
{visibleAgents.length === 0 ? (
|
|
||||||
<div className="py-12 px-4 text-center text-muted-foreground">
|
|
||||||
<RiRobot2Line className="mx-auto mb-3 h-10 w-10 opacity-50" />
|
|
||||||
<p className="typography-ui-label font-medium">No agents configured</p>
|
|
||||||
<p className="typography-meta mt-1 opacity-75">Use the + button above to create one</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{builtInAgents.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="px-2 pb-1.5 pt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
||||||
Built-in Agents
|
|
||||||
</div>
|
|
||||||
{builtInAgents.map((agent) => (
|
|
||||||
<AgentListItem
|
|
||||||
key={agent.name}
|
|
||||||
agent={agent}
|
|
||||||
isSelected={selectedAgentName === agent.name}
|
|
||||||
onSelect={() => {
|
|
||||||
setSelectedAgent(agent.name);
|
|
||||||
onItemSelect?.();
|
|
||||||
if (isMobile) {
|
|
||||||
setSidebarOpen(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDuplicate={() => handleDuplicateAgent(agent)}
|
|
||||||
getAgentModeIcon={getAgentModeIcon}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{customAgents.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="px-2 pb-1.5 pt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
||||||
Custom Agents
|
|
||||||
</div>
|
|
||||||
{customAgents.map((agent) => (
|
|
||||||
<AgentListItem
|
|
||||||
key={agent.name}
|
|
||||||
agent={agent}
|
|
||||||
isSelected={selectedAgentName === agent.name}
|
|
||||||
onSelect={() => {
|
|
||||||
setSelectedAgent(agent.name);
|
|
||||||
onItemSelect?.();
|
|
||||||
if (isMobile) {
|
|
||||||
setSidebarOpen(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDelete={() => handleDeleteAgent(agent)}
|
|
||||||
onDuplicate={() => handleDuplicateAgent(agent)}
|
|
||||||
getAgentModeIcon={getAgentModeIcon}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ScrollableOverlay>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create New Agent</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Enter a unique name for your new agent
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Input
|
|
||||||
value={newAgentName}
|
|
||||||
onChange={(e) => setNewAgentName(e.target.value)}
|
|
||||||
placeholder="Agent name..."
|
|
||||||
className="text-foreground placeholder:text-muted-foreground"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleCreateAgent();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => setIsCreateDialogOpen(false)}
|
|
||||||
className="text-foreground hover:bg-muted hover:text-foreground"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<ButtonLarge onClick={handleCreateAgent}>
|
|
||||||
Create
|
|
||||||
</ButtonLarge>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
|
||||||
|
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-1 px-3 py-2 overflow-x-hidden">
|
||||||
|
{visibleAgents.length === 0 ? (
|
||||||
|
<div className="py-12 px-4 text-center text-muted-foreground">
|
||||||
|
<RiRobot2Line className="mx-auto mb-3 h-10 w-10 opacity-50" />
|
||||||
|
<p className="typography-ui-label font-medium">No agents configured</p>
|
||||||
|
<p className="typography-meta mt-1 opacity-75">Use the + button above to create one</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{builtInAgents.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="px-2 pb-1.5 pt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Built-in Agents
|
||||||
|
</div>
|
||||||
|
{builtInAgents.map((agent) => (
|
||||||
|
<AgentListItem
|
||||||
|
key={agent.name}
|
||||||
|
agent={agent}
|
||||||
|
isSelected={selectedAgentName === agent.name}
|
||||||
|
onSelect={() => {
|
||||||
|
setSelectedAgent(agent.name);
|
||||||
|
onItemSelect?.();
|
||||||
|
if (isMobile) {
|
||||||
|
setSidebarOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReset={() => handleResetAgent(agent)}
|
||||||
|
onDuplicate={() => handleDuplicateAgent(agent)}
|
||||||
|
getAgentModeIcon={getAgentModeIcon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{customAgents.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="px-2 pb-1.5 pt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Custom Agents
|
||||||
|
</div>
|
||||||
|
{customAgents.map((agent) => (
|
||||||
|
<AgentListItem
|
||||||
|
key={agent.name}
|
||||||
|
agent={agent}
|
||||||
|
isSelected={selectedAgentName === agent.name}
|
||||||
|
onSelect={() => {
|
||||||
|
setSelectedAgent(agent.name);
|
||||||
|
onItemSelect?.();
|
||||||
|
if (isMobile) {
|
||||||
|
setSidebarOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRename={() => handleOpenRenameDialog(agent)}
|
||||||
|
onDelete={() => handleDeleteAgent(agent)}
|
||||||
|
onDuplicate={() => handleDuplicateAgent(agent)}
|
||||||
|
getAgentModeIcon={getAgentModeIcon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ScrollableOverlay>
|
||||||
|
|
||||||
|
{/* Rename Dialog */}
|
||||||
|
<Dialog open={renameDialogAgent !== null} onOpenChange={(open) => !open && setRenameDialogAgent(null)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Rename Agent</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter a new name for the agent "@{renameDialogAgent?.name}"
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Input
|
||||||
|
value={renameNewName}
|
||||||
|
onChange={(e) => setRenameNewName(e.target.value)}
|
||||||
|
placeholder="New agent name..."
|
||||||
|
className="text-foreground placeholder:text-muted-foreground"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleRenameAgent();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setRenameDialogAgent(null)}
|
||||||
|
className="text-foreground hover:bg-muted hover:text-foreground"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<ButtonLarge onClick={handleRenameAgent}>
|
||||||
|
Rename
|
||||||
|
</ButtonLarge>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AgentListItemProps {
|
interface AgentListItemProps {
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onDuplicate: () => void;
|
onReset?: () => void;
|
||||||
getAgentModeIcon: (mode?: string) => React.ReactNode;
|
onRename?: () => void;
|
||||||
|
onDuplicate: () => void;
|
||||||
|
getAgentModeIcon: (mode?: string) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AgentListItem: React.FC<AgentListItemProps> = ({
|
const AgentListItem: React.FC<AgentListItemProps> = ({
|
||||||
agent,
|
agent,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
onDelete,
|
onDelete,
|
||||||
onDuplicate,
|
onReset,
|
||||||
getAgentModeIcon,
|
onRename,
|
||||||
|
onDuplicate,
|
||||||
|
getAgentModeIcon,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
const extAgent = agent as Agent & { scope?: AgentScope };
|
||||||
<div
|
|
||||||
className={cn(
|
return (
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
<div
|
||||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
className={cn(
|
||||||
)}
|
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
||||||
|
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
|
<button
|
||||||
|
onClick={onSelect}
|
||||||
|
className="flex min-w-0 flex-1 flex-col gap-0 rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 items-center">
|
<div className="flex items-center gap-1.5">
|
||||||
<button
|
<span className="typography-ui-label font-normal truncate text-foreground">
|
||||||
onClick={onSelect}
|
{agent.name}
|
||||||
className="flex min-w-0 flex-1 flex-col gap-0 rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
</span>
|
||||||
tabIndex={0}
|
{getAgentModeIcon(agent.mode)}
|
||||||
>
|
{(extAgent.scope || isAgentBuiltIn(agent)) && (
|
||||||
<div className="flex items-center gap-1.5">
|
<span className="typography-micro text-muted-foreground bg-muted px-1 rounded flex-shrink-0 leading-none pb-px border border-border/50">
|
||||||
<span className="typography-ui-label font-normal truncate text-foreground">
|
{isAgentBuiltIn(agent) ? 'system' : extAgent.scope}
|
||||||
{agent.name}
|
</span>
|
||||||
</span>
|
)}
|
||||||
{getAgentModeIcon(agent.mode)}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{agent.description && (
|
{agent.description && (
|
||||||
<div className="typography-micro text-muted-foreground/60 truncate leading-tight">
|
<div className="typography-micro text-muted-foreground/60 truncate leading-tight">
|
||||||
{agent.description}
|
{agent.description}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 w-6 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100"
|
|
||||||
>
|
|
||||||
<RiMore2Line className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-fit min-w-20">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDuplicate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiFileCopyLine className="h-4 w-4 mr-px" />
|
|
||||||
Duplicate
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
{!isAgentBuiltIn(agent) && onDelete && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDelete();
|
|
||||||
}}
|
|
||||||
className="text-destructive focus:text-destructive"
|
|
||||||
>
|
|
||||||
<RiDeleteBinLine className="h-4 w-4 mr-px" />
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
);
|
</button>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<RiMore2Line className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-fit min-w-20">
|
||||||
|
{onRename && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRename();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiEditLine className="h-4 w-4 mr-px" />
|
||||||
|
Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiFileCopyLine className="h-4 w-4 mr-px" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{onReset && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onReset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiRestartLine className="h-4 w-4 mr-px" />
|
||||||
|
Reset
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onDelete && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className="h-4 w-4 mr-px" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,26 @@ import {
|
|||||||
import { getSafeStorage } from "./utils/safeStorage";
|
import { getSafeStorage } from "./utils/safeStorage";
|
||||||
import { useConfigStore } from "@/stores/useConfigStore";
|
import { useConfigStore } from "@/stores/useConfigStore";
|
||||||
|
|
||||||
|
// Note: useDirectoryStore cannot be imported at top level to avoid circular dependency
|
||||||
|
// useDirectoryStore -> useAgentsStore (for refreshAfterOpenCodeRestart)
|
||||||
|
// useAgentsStore -> useDirectoryStore (for currentDirectory)
|
||||||
|
// Instead we access it from the window object where it's exposed
|
||||||
|
const getCurrentDirectory = (): string | null => {
|
||||||
|
// Try to get from window if store is already loaded
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const store = (window as any).__zustand_directory_store__;
|
||||||
|
if (store) {
|
||||||
|
return store.getState().currentDirectory;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentScope = 'user' | 'project';
|
||||||
|
|
||||||
export interface AgentConfig {
|
export interface AgentConfig {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -22,12 +42,16 @@ export interface AgentConfig {
|
|||||||
mode?: "primary" | "subagent" | "all";
|
mode?: "primary" | "subagent" | "all";
|
||||||
tools?: Record<string, boolean>;
|
tools?: Record<string, boolean>;
|
||||||
permission?: {
|
permission?: {
|
||||||
edit?: "allow" | "ask" | "deny" | "full";
|
edit?: "allow" | "ask" | "deny";
|
||||||
bash?: "allow" | "ask" | "deny" | Record<string, "allow" | "ask" | "deny">;
|
bash?: "allow" | "ask" | "deny" | Record<string, "allow" | "ask" | "deny">;
|
||||||
|
skill?: "allow" | "ask" | "deny" | Record<string, "allow" | "ask" | "deny">;
|
||||||
webfetch?: "allow" | "ask" | "deny";
|
webfetch?: "allow" | "ask" | "deny";
|
||||||
|
doom_loop?: "allow" | "ask" | "deny";
|
||||||
|
external_directory?: "allow" | "ask" | "deny";
|
||||||
};
|
};
|
||||||
|
|
||||||
disable?: boolean;
|
disable?: boolean;
|
||||||
|
scope?: AgentScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extended Agent type for API properties not in SDK types
|
// Extended Agent type for API properties not in SDK types
|
||||||
@@ -61,13 +85,36 @@ const SLOW_HEALTH_POLL_BASE_MS = 800;
|
|||||||
const SLOW_HEALTH_POLL_INCREMENT_MS = 200;
|
const SLOW_HEALTH_POLL_INCREMENT_MS = 200;
|
||||||
const SLOW_HEALTH_POLL_MAX_MS = 2000;
|
const SLOW_HEALTH_POLL_MAX_MS = 2000;
|
||||||
|
|
||||||
|
export interface AgentDraft {
|
||||||
|
name: string;
|
||||||
|
scope: AgentScope;
|
||||||
|
description?: string;
|
||||||
|
model?: string | null;
|
||||||
|
temperature?: number;
|
||||||
|
top_p?: number;
|
||||||
|
prompt?: string;
|
||||||
|
mode?: "primary" | "subagent" | "all";
|
||||||
|
tools?: Record<string, boolean>;
|
||||||
|
permission?: {
|
||||||
|
edit?: "allow" | "ask" | "deny";
|
||||||
|
bash?: "allow" | "ask" | "deny" | Record<string, "allow" | "ask" | "deny">;
|
||||||
|
skill?: "allow" | "ask" | "deny" | Record<string, "allow" | "ask" | "deny">;
|
||||||
|
webfetch?: "allow" | "ask" | "deny";
|
||||||
|
doom_loop?: "allow" | "ask" | "deny";
|
||||||
|
external_directory?: "allow" | "ask" | "deny";
|
||||||
|
};
|
||||||
|
disable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface AgentsStore {
|
interface AgentsStore {
|
||||||
|
|
||||||
selectedAgentName: string | null;
|
selectedAgentName: string | null;
|
||||||
agents: Agent[];
|
agents: Agent[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
agentDraft: AgentDraft | null;
|
||||||
|
|
||||||
setSelectedAgent: (name: string | null) => void;
|
setSelectedAgent: (name: string | null) => void;
|
||||||
|
setAgentDraft: (draft: AgentDraft | null) => void;
|
||||||
loadAgents: () => Promise<boolean>;
|
loadAgents: () => Promise<boolean>;
|
||||||
createAgent: (config: AgentConfig) => Promise<boolean>;
|
createAgent: (config: AgentConfig) => Promise<boolean>;
|
||||||
updateAgent: (name: string, config: Partial<AgentConfig>) => Promise<boolean>;
|
updateAgent: (name: string, config: Partial<AgentConfig>) => Promise<boolean>;
|
||||||
@@ -91,11 +138,16 @@ export const useAgentsStore = create<AgentsStore>()(
|
|||||||
selectedAgentName: null,
|
selectedAgentName: null,
|
||||||
agents: [],
|
agents: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
agentDraft: null,
|
||||||
|
|
||||||
setSelectedAgent: (name: string | null) => {
|
setSelectedAgent: (name: string | null) => {
|
||||||
set({ selectedAgentName: name });
|
set({ selectedAgentName: name });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setAgentDraft: (draft: AgentDraft | null) => {
|
||||||
|
set({ agentDraft: draft });
|
||||||
|
},
|
||||||
|
|
||||||
loadAgents: async () => {
|
loadAgents: async () => {
|
||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
const previousAgents = get().agents;
|
const previousAgents = get().agents;
|
||||||
@@ -104,7 +156,29 @@ export const useAgentsStore = create<AgentsStore>()(
|
|||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
try {
|
try {
|
||||||
const agents = await opencodeClient.listAgents();
|
const agents = await opencodeClient.listAgents();
|
||||||
set({ agents, isLoading: false });
|
|
||||||
|
// Fetch scope info for each agent
|
||||||
|
const currentDirectory = getCurrentDirectory();
|
||||||
|
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||||
|
|
||||||
|
const agentsWithScope = await Promise.all(
|
||||||
|
agents.map(async (agent) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/config/agents/${encodeURIComponent(agent.name)}${queryParams}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
// Handle web/desktop response formats; fall back to JSON scope if md scope missing
|
||||||
|
const scope = data.scope ?? data.sources?.md?.scope ?? data.sources?.json?.scope;
|
||||||
|
return { ...agent, scope: scope as AgentScope | undefined };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[AgentsStore] Failed to fetch scope for agent "${agent.name}":`, err);
|
||||||
|
}
|
||||||
|
return agent;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
set({ agents: agentsWithScope, isLoading: false });
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
@@ -136,10 +210,15 @@ export const useAgentsStore = create<AgentsStore>()(
|
|||||||
if (config.tools && Object.keys(config.tools).length > 0) agentConfig.tools = config.tools;
|
if (config.tools && Object.keys(config.tools).length > 0) agentConfig.tools = config.tools;
|
||||||
if (config.permission) agentConfig.permission = config.permission;
|
if (config.permission) agentConfig.permission = config.permission;
|
||||||
if (config.disable !== undefined) agentConfig.disable = config.disable;
|
if (config.disable !== undefined) agentConfig.disable = config.disable;
|
||||||
|
if (config.scope) agentConfig.scope = config.scope;
|
||||||
|
|
||||||
console.log('[AgentsStore] Agent config to save:', agentConfig);
|
console.log('[AgentsStore] Agent config to save:', agentConfig);
|
||||||
|
|
||||||
const response = await fetch(`/api/config/agents/${encodeURIComponent(config.name)}`, {
|
// Get current directory for project-level agent support
|
||||||
|
const currentDirectory = getCurrentDirectory();
|
||||||
|
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||||
|
|
||||||
|
const response = await fetch(`/api/config/agents/${encodeURIComponent(config.name)}${queryParams}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(agentConfig)
|
body: JSON.stringify(agentConfig)
|
||||||
@@ -199,7 +278,11 @@ export const useAgentsStore = create<AgentsStore>()(
|
|||||||
|
|
||||||
console.log('[AgentsStore] Agent config to update:', agentConfig);
|
console.log('[AgentsStore] Agent config to update:', agentConfig);
|
||||||
|
|
||||||
const response = await fetch(`/api/config/agents/${encodeURIComponent(name)}`, {
|
// Get current directory for project-level agent support
|
||||||
|
const currentDirectory = getCurrentDirectory();
|
||||||
|
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||||
|
|
||||||
|
const response = await fetch(`/api/config/agents/${encodeURIComponent(name)}${queryParams}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(agentConfig)
|
body: JSON.stringify(agentConfig)
|
||||||
@@ -242,7 +325,11 @@ export const useAgentsStore = create<AgentsStore>()(
|
|||||||
startConfigUpdate("Deleting agent configuration…");
|
startConfigUpdate("Deleting agent configuration…");
|
||||||
let requiresReload = false;
|
let requiresReload = false;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/config/agents/${encodeURIComponent(name)}`, {
|
// Get current directory for project-level agent support
|
||||||
|
const currentDirectory = getCurrentDirectory();
|
||||||
|
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||||
|
|
||||||
|
const response = await fetch(`/api/config/agents/${encodeURIComponent(name)}${queryParams}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { OpenCodeManager } from './opencode';
|
import type { OpenCodeManager } from './opencode';
|
||||||
import { createAgent, createCommand, deleteAgent, deleteCommand, getAgentSources, getCommandSources, updateAgent, updateCommand, type CommandScope, COMMAND_SCOPE } from './opencodeConfig';
|
import { createAgent, createCommand, deleteAgent, deleteCommand, getAgentSources, getCommandSources, updateAgent, updateCommand, type AgentScope, type CommandScope, AGENT_SCOPE, COMMAND_SCOPE } from './opencodeConfig';
|
||||||
import { removeProviderAuth } from './opencodeAuth';
|
import { removeProviderAuth } from './opencodeAuth';
|
||||||
|
|
||||||
export interface BridgeRequest {
|
export interface BridgeRequest {
|
||||||
@@ -617,19 +617,25 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
|||||||
return { id, type, success: false, error: 'Agent name is required' };
|
return { id, type, success: false, error: 'Agent name is required' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get working directory for project-level agent support
|
||||||
|
const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
|
||||||
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
|
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
|
||||||
if (normalizedMethod === 'GET') {
|
if (normalizedMethod === 'GET') {
|
||||||
const sources = getAgentSources(agentName);
|
const sources = getAgentSources(agentName, workingDirectory);
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
success: true,
|
success: true,
|
||||||
data: { name: agentName, sources, isBuiltIn: !sources.md.exists && !sources.json.exists },
|
data: { name: agentName, sources, scope: sources.md.scope, isBuiltIn: !sources.md.exists && !sources.json.exists },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedMethod === 'POST') {
|
if (normalizedMethod === 'POST') {
|
||||||
createAgent(agentName, (body || {}) as Record<string, unknown>);
|
// Extract scope from body if present
|
||||||
|
const scopeValue = body?.scope as string | undefined;
|
||||||
|
const scope: AgentScope | undefined = scopeValue === 'project' ? AGENT_SCOPE.PROJECT : scopeValue === 'user' ? AGENT_SCOPE.USER : undefined;
|
||||||
|
createAgent(agentName, (body || {}) as Record<string, unknown>, workingDirectory, scope);
|
||||||
await ctx?.manager?.restart();
|
await ctx?.manager?.restart();
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -645,7 +651,7 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedMethod === 'PATCH') {
|
if (normalizedMethod === 'PATCH') {
|
||||||
updateAgent(agentName, (body || {}) as Record<string, unknown>);
|
updateAgent(agentName, (body || {}) as Record<string, unknown>, workingDirectory);
|
||||||
await ctx?.manager?.restart();
|
await ctx?.manager?.restart();
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -661,7 +667,7 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedMethod === 'DELETE') {
|
if (normalizedMethod === 'DELETE') {
|
||||||
deleteAgent(agentName);
|
deleteAgent(agentName, workingDirectory);
|
||||||
await ctx?.manager?.restart();
|
await ctx?.manager?.restart();
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -8,19 +8,28 @@ const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|||||||
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agent');
|
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agent');
|
||||||
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'command');
|
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'command');
|
||||||
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
||||||
|
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
||||||
|
? path.resolve(process.env.OPENCODE_CONFIG)
|
||||||
|
: null;
|
||||||
const PROMPT_FILE_PATTERN = /^\{file:(.+)\}$/i;
|
const PROMPT_FILE_PATTERN = /^\{file:(.+)\}$/i;
|
||||||
|
|
||||||
// Command scope types
|
// Scope types (shared by agents and commands)
|
||||||
|
export const AGENT_SCOPE = {
|
||||||
|
USER: 'user',
|
||||||
|
PROJECT: 'project'
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const COMMAND_SCOPE = {
|
export const COMMAND_SCOPE = {
|
||||||
USER: 'user',
|
USER: 'user',
|
||||||
PROJECT: 'project'
|
PROJECT: 'project'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type AgentScope = typeof AGENT_SCOPE[keyof typeof AGENT_SCOPE];
|
||||||
export type CommandScope = typeof COMMAND_SCOPE[keyof typeof COMMAND_SCOPE];
|
export type CommandScope = typeof COMMAND_SCOPE[keyof typeof COMMAND_SCOPE];
|
||||||
|
|
||||||
export type ConfigSources = {
|
export type ConfigSources = {
|
||||||
md: { exists: boolean; path: string | null; fields: string[]; scope?: CommandScope | null };
|
md: { exists: boolean; path: string | null; fields: string[]; scope?: AgentScope | CommandScope | null };
|
||||||
json: { exists: boolean; path: string; fields: string[] };
|
json: { exists: boolean; path: string; fields: string[]; scope?: AgentScope | CommandScope | null };
|
||||||
projectMd?: { exists: boolean; path: string | null };
|
projectMd?: { exists: boolean; path: string | null };
|
||||||
userMd?: { exists: boolean; path: string | null };
|
userMd?: { exists: boolean; path: string | null };
|
||||||
};
|
};
|
||||||
@@ -31,6 +40,62 @@ const ensureDirs = () => {
|
|||||||
if (!fs.existsSync(COMMAND_DIR)) fs.mkdirSync(COMMAND_DIR, { recursive: true });
|
if (!fs.existsSync(COMMAND_DIR)) fs.mkdirSync(COMMAND_DIR, { recursive: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============== AGENT SCOPE HELPERS ==============
|
||||||
|
|
||||||
|
const ensureProjectAgentDir = (workingDirectory: string): string => {
|
||||||
|
const projectAgentDir = path.join(workingDirectory, '.opencode', 'agent');
|
||||||
|
if (!fs.existsSync(projectAgentDir)) {
|
||||||
|
fs.mkdirSync(projectAgentDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return projectAgentDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectAgentPath = (workingDirectory: string, agentName: string): string => {
|
||||||
|
return path.join(workingDirectory, '.opencode', 'agent', `${agentName}.md`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserAgentPath = (agentName: string): string => {
|
||||||
|
return path.join(AGENT_DIR, `${agentName}.md`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAgentScope = (agentName: string, workingDirectory?: string): { scope: AgentScope | null; path: string | null } => {
|
||||||
|
if (workingDirectory) {
|
||||||
|
const projectPath = getProjectAgentPath(workingDirectory, agentName);
|
||||||
|
if (fs.existsSync(projectPath)) {
|
||||||
|
return { scope: AGENT_SCOPE.PROJECT, path: projectPath };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPath = getUserAgentPath(agentName);
|
||||||
|
if (fs.existsSync(userPath)) {
|
||||||
|
return { scope: AGENT_SCOPE.USER, path: userPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { scope: null, path: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentWritePath = (agentName: string, workingDirectory?: string, requestedScope?: AgentScope): { scope: AgentScope; path: string } => {
|
||||||
|
const existing = getAgentScope(agentName, workingDirectory);
|
||||||
|
if (existing.path) {
|
||||||
|
return { scope: existing.scope!, path: existing.path };
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = requestedScope || AGENT_SCOPE.USER;
|
||||||
|
if (scope === AGENT_SCOPE.PROJECT && workingDirectory) {
|
||||||
|
return {
|
||||||
|
scope: AGENT_SCOPE.PROJECT,
|
||||||
|
path: getProjectAgentPath(workingDirectory, agentName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: AGENT_SCOPE.USER,
|
||||||
|
path: getUserAgentPath(agentName)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============== COMMAND SCOPE HELPERS ==============
|
||||||
|
|
||||||
const ensureProjectCommandDir = (workingDirectory: string): string => {
|
const ensureProjectCommandDir = (workingDirectory: string): string => {
|
||||||
const projectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
const projectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
||||||
if (!fs.existsSync(projectCommandDir)) {
|
if (!fs.existsSync(projectCommandDir)) {
|
||||||
@@ -107,24 +172,116 @@ const writePromptFile = (filePath: string, content: string) => {
|
|||||||
fs.writeFileSync(filePath, content, 'utf8');
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
};
|
};
|
||||||
|
|
||||||
const readConfig = (): Record<string, unknown> => {
|
const getProjectConfigPath = (workingDirectory?: string): string | null => {
|
||||||
if (!fs.existsSync(CONFIG_FILE)) return {};
|
if (!workingDirectory) return null;
|
||||||
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
return path.join(workingDirectory, 'opencode.json');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfigPaths = (workingDirectory?: string) => ({
|
||||||
|
userPath: CONFIG_FILE,
|
||||||
|
projectPath: getProjectConfigPath(workingDirectory),
|
||||||
|
customPath: CUSTOM_CONFIG_FILE
|
||||||
|
});
|
||||||
|
|
||||||
|
const readConfigFile = (filePath?: string | null): Record<string, unknown> => {
|
||||||
|
if (!filePath || !fs.existsSync(filePath)) return {};
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
const normalized = stripJsonComments(content).trim();
|
const normalized = stripJsonComments(content).trim();
|
||||||
if (!normalized) return {};
|
if (!normalized) return {};
|
||||||
return JSON.parse(normalized) as Record<string, unknown>;
|
return JSON.parse(normalized) as Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const writeConfig = (config: Record<string, unknown>) => {
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||||
const backupFile = `${CONFIG_FILE}.openchamber.backup`;
|
|
||||||
|
const mergeConfigs = (base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown> => {
|
||||||
|
const result: Record<string, unknown> = { ...base };
|
||||||
|
for (const [key, value] of Object.entries(override)) {
|
||||||
|
if (key in result) {
|
||||||
|
const baseValue = result[key];
|
||||||
|
if (isPlainObject(baseValue) && isPlainObject(value)) {
|
||||||
|
result[key] = mergeConfigs(baseValue, value);
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readConfigLayers = (workingDirectory?: string) => {
|
||||||
|
const { userPath, projectPath, customPath } = getConfigPaths(workingDirectory);
|
||||||
|
const userConfig = readConfigFile(userPath);
|
||||||
|
const projectConfig = readConfigFile(projectPath);
|
||||||
|
const customConfig = readConfigFile(customPath);
|
||||||
|
const mergedConfig = mergeConfigs(mergeConfigs(userConfig, projectConfig), customConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userConfig,
|
||||||
|
projectConfig,
|
||||||
|
customConfig,
|
||||||
|
mergedConfig,
|
||||||
|
paths: { userPath, projectPath, customPath }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const readConfig = (workingDirectory?: string): Record<string, unknown> =>
|
||||||
|
readConfigLayers(workingDirectory).mergedConfig;
|
||||||
|
|
||||||
|
const writeConfig = (config: Record<string, unknown>, filePath: string = CONFIG_FILE) => {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const backupFile = `${filePath}.openchamber.backup`;
|
||||||
try {
|
try {
|
||||||
fs.copyFileSync(CONFIG_FILE, backupFile);
|
fs.copyFileSync(filePath, backupFile);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore backup failures
|
// ignore backup failures
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getJsonEntrySource = (
|
||||||
|
layers: ReturnType<typeof readConfigLayers>,
|
||||||
|
sectionKey: 'agent' | 'command',
|
||||||
|
entryName: string
|
||||||
|
) => {
|
||||||
|
const { userConfig, projectConfig, customConfig, paths } = layers;
|
||||||
|
const customSection = (customConfig as Record<string, unknown>)?.[sectionKey] as Record<string, unknown> | undefined;
|
||||||
|
if (customSection?.[entryName] !== undefined) {
|
||||||
|
return { section: customSection[entryName], config: customConfig, path: paths.customPath, exists: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectSection = (projectConfig as Record<string, unknown>)?.[sectionKey] as Record<string, unknown> | undefined;
|
||||||
|
if (projectSection?.[entryName] !== undefined) {
|
||||||
|
return { section: projectSection[entryName], config: projectConfig, path: paths.projectPath, exists: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSection = (userConfig as Record<string, unknown>)?.[sectionKey] as Record<string, unknown> | undefined;
|
||||||
|
if (userSection?.[entryName] !== undefined) {
|
||||||
|
return { section: userSection[entryName], config: userConfig, path: paths.userPath, exists: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { section: null, config: null, path: null, exists: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getJsonWriteTarget = (
|
||||||
|
layers: ReturnType<typeof readConfigLayers>,
|
||||||
|
preferredScope: AgentScope | CommandScope
|
||||||
|
) => {
|
||||||
|
const { userConfig, projectConfig, customConfig, paths } = layers;
|
||||||
|
if (paths.customPath) {
|
||||||
|
return { config: customConfig, path: paths.customPath };
|
||||||
|
}
|
||||||
|
if (preferredScope === AGENT_SCOPE.PROJECT && paths.projectPath) {
|
||||||
|
return { config: projectConfig, path: paths.projectPath };
|
||||||
|
}
|
||||||
|
if (paths.projectPath) {
|
||||||
|
return { config: projectConfig, path: paths.projectPath };
|
||||||
|
}
|
||||||
|
return { config: userConfig, path: paths.userPath };
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseMdFile = (filePath: string): { frontmatter: Record<string, unknown>; body: string } => {
|
const parseMdFile = (filePath: string): { frontmatter: Record<string, unknown>; body: string } => {
|
||||||
@@ -144,19 +301,34 @@ const writeMdFile = (filePath: string, frontmatter: Record<string, unknown>, bod
|
|||||||
fs.writeFileSync(filePath, content, 'utf8');
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAgentSources = (agentName: string): ConfigSources => {
|
export const getAgentSources = (agentName: string, workingDirectory?: string): ConfigSources => {
|
||||||
const mdPath = path.join(AGENT_DIR, `${agentName}.md`);
|
// Check project level first (takes precedence)
|
||||||
const mdExists = fs.existsSync(mdPath);
|
const projectPath = workingDirectory ? getProjectAgentPath(workingDirectory, agentName) : null;
|
||||||
|
const projectExists = projectPath ? fs.existsSync(projectPath) : false;
|
||||||
|
|
||||||
|
// Then check user level
|
||||||
|
const userPath = getUserAgentPath(agentName);
|
||||||
|
const userExists = fs.existsSync(userPath);
|
||||||
|
|
||||||
|
// Determine which md file to use (project takes precedence)
|
||||||
|
const mdPath = projectExists ? projectPath : (userExists ? userPath : null);
|
||||||
|
const mdExists = !!mdPath;
|
||||||
|
const mdScope = projectExists ? AGENT_SCOPE.PROJECT : (userExists ? AGENT_SCOPE.USER : null);
|
||||||
|
|
||||||
const config = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
const agentSection = (config.agent as Record<string, unknown> | undefined)?.[agentName] as Record<string, unknown> | undefined;
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
||||||
|
const agentSection = jsonSource.section as Record<string, unknown> | undefined;
|
||||||
|
const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
|
||||||
|
const jsonScope = jsonSource.path === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
|
||||||
|
|
||||||
const sources: ConfigSources = {
|
const sources: ConfigSources = {
|
||||||
md: { exists: mdExists, path: mdExists ? mdPath : null, fields: [] },
|
md: { exists: mdExists, path: mdPath, scope: mdScope, fields: [] },
|
||||||
json: { exists: Boolean(agentSection), path: CONFIG_FILE, fields: [] },
|
json: { exists: jsonSource.exists, path: jsonPath || CONFIG_FILE, scope: jsonSource.exists ? jsonScope : null, fields: [] },
|
||||||
|
projectMd: { exists: projectExists, path: projectPath },
|
||||||
|
userMd: { exists: userExists, path: userPath }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mdExists) {
|
if (mdExists && mdPath) {
|
||||||
const { frontmatter, body } = parseMdFile(mdPath);
|
const { frontmatter, body } = parseMdFile(mdPath);
|
||||||
sources.md.fields = Object.keys(frontmatter);
|
sources.md.fields = Object.keys(frontmatter);
|
||||||
if (body) sources.md.fields.push('prompt');
|
if (body) sources.md.fields.push('prompt');
|
||||||
@@ -169,41 +341,86 @@ export const getAgentSources = (agentName: string): ConfigSources => {
|
|||||||
return sources;
|
return sources;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createAgent = (agentName: string, config: Record<string, unknown>) => {
|
export const createAgent = (agentName: string, config: Record<string, unknown>, workingDirectory?: string, scope?: AgentScope) => {
|
||||||
ensureDirs();
|
ensureDirs();
|
||||||
|
|
||||||
const mdPath = path.join(AGENT_DIR, `${agentName}.md`);
|
// Check if agent already exists at either level
|
||||||
if (fs.existsSync(mdPath)) throw new Error(`Agent ${agentName} already exists as .md file`);
|
const projectPath = workingDirectory ? getProjectAgentPath(workingDirectory, agentName) : null;
|
||||||
|
const userPath = getUserAgentPath(agentName);
|
||||||
|
|
||||||
|
if (projectPath && fs.existsSync(projectPath)) {
|
||||||
|
throw new Error(`Agent ${agentName} already exists as project-level .md file`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(userPath)) {
|
||||||
|
throw new Error(`Agent ${agentName} already exists as user-level .md file`);
|
||||||
|
}
|
||||||
|
|
||||||
const existingConfig = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
const agentMap = existingConfig.agent as Record<string, unknown> | undefined;
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
||||||
if (agentMap?.[agentName]) throw new Error(`Agent ${agentName} already exists in opencode.json`);
|
if (jsonSource.exists) throw new Error(`Agent ${agentName} already exists in opencode.json`);
|
||||||
|
|
||||||
const { prompt, ...frontmatter } = config as Record<string, unknown> & { prompt?: unknown };
|
// Determine target path based on requested scope
|
||||||
writeMdFile(mdPath, frontmatter, typeof prompt === 'string' ? prompt : '');
|
let targetPath: string;
|
||||||
|
|
||||||
|
if (scope === AGENT_SCOPE.PROJECT && workingDirectory) {
|
||||||
|
ensureProjectAgentDir(workingDirectory);
|
||||||
|
targetPath = projectPath!;
|
||||||
|
} else {
|
||||||
|
targetPath = userPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract scope and prompt from config - scope is only used for path determination, not written to file
|
||||||
|
const { prompt, scope: _ignored, ...frontmatter } = config as Record<string, unknown> & { prompt?: unknown; scope?: unknown };
|
||||||
|
void _ignored; // Scope is only used for path determination
|
||||||
|
writeMdFile(targetPath, frontmatter, typeof prompt === 'string' ? prompt : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateAgent = (agentName: string, updates: Record<string, unknown>) => {
|
export const updateAgent = (agentName: string, updates: Record<string, unknown>, workingDirectory?: string) => {
|
||||||
ensureDirs();
|
ensureDirs();
|
||||||
|
|
||||||
const mdPath = path.join(AGENT_DIR, `${agentName}.md`);
|
// Determine correct path: project level takes precedence
|
||||||
const mdExists = fs.existsSync(mdPath);
|
const { path: mdPath } = getAgentWritePath(agentName, workingDirectory);
|
||||||
|
const mdExists = mdPath ? fs.existsSync(mdPath) : false;
|
||||||
|
|
||||||
|
// Check if agent exists in opencode.json across all config layers
|
||||||
|
const layers = readConfigLayers(workingDirectory);
|
||||||
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
||||||
|
const jsonSection = jsonSource.section as Record<string, unknown> | undefined;
|
||||||
|
const hasJsonFields = Boolean(jsonSource.exists && jsonSection && Object.keys(jsonSection).length > 0);
|
||||||
|
const jsonTarget = jsonSource.exists
|
||||||
|
? { config: jsonSource.config, path: jsonSource.path }
|
||||||
|
: getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER);
|
||||||
|
const config = (jsonTarget.config || {}) as Record<string, unknown>;
|
||||||
|
|
||||||
|
// Determine if we should create a new md file:
|
||||||
|
// Only for built-in agents (no md file AND no json config)
|
||||||
|
const isBuiltinOverride = !mdExists && !hasJsonFields;
|
||||||
|
|
||||||
|
let targetPath = mdPath;
|
||||||
|
|
||||||
|
if (!mdExists && isBuiltinOverride) {
|
||||||
|
// Built-in agent override - create at user level
|
||||||
|
targetPath = getUserAgentPath(agentName);
|
||||||
|
}
|
||||||
|
|
||||||
const mdData = mdExists ? parseMdFile(mdPath) : null;
|
// Only create md data for existing md files or built-in overrides
|
||||||
const config = readConfig();
|
const mdData = mdExists && mdPath ? parseMdFile(mdPath) : (isBuiltinOverride ? { frontmatter: {} as Record<string, unknown>, body: '' } : null);
|
||||||
const agentMap = (config.agent as Record<string, unknown> | undefined) ?? {};
|
|
||||||
const jsonSection = agentMap[agentName] as Record<string, unknown> | undefined;
|
|
||||||
|
|
||||||
let mdModified = false;
|
let mdModified = false;
|
||||||
let jsonModified = false;
|
let jsonModified = false;
|
||||||
|
// Only create new md if it's a built-in override
|
||||||
|
let creatingNewMd = isBuiltinOverride;
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(updates || {})) {
|
for (const [field, value] of Object.entries(updates || {})) {
|
||||||
if (field === 'prompt') {
|
if (field === 'prompt') {
|
||||||
const normalizedValue = typeof value === 'string' ? value : value == null ? '' : String(value);
|
const normalizedValue = typeof value === 'string' ? value : value == null ? '' : String(value);
|
||||||
|
|
||||||
if (mdExists && mdData) {
|
if (mdExists || creatingNewMd) {
|
||||||
mdData.body = normalizedValue;
|
if (mdData) {
|
||||||
mdModified = true;
|
mdData.body = normalizedValue;
|
||||||
|
mdModified = true;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,9 +431,10 @@ export const updateAgent = (agentName: string, updates: Record<string, unknown>)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For JSON-only agents, store prompt inline in JSON
|
||||||
if (!config.agent) config.agent = {};
|
if (!config.agent) config.agent = {};
|
||||||
const target = (config.agent as Record<string, unknown>)[agentName] as Record<string, unknown> | undefined;
|
const current = ((config.agent as Record<string, unknown>)[agentName] as Record<string, unknown> | undefined) ?? {};
|
||||||
(config.agent as Record<string, unknown>)[agentName] = { ...(target || {}), prompt: normalizedValue };
|
(config.agent as Record<string, unknown>)[agentName] = { ...current, prompt: normalizedValue };
|
||||||
jsonModified = true;
|
jsonModified = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -224,53 +442,83 @@ export const updateAgent = (agentName: string, updates: Record<string, unknown>)
|
|||||||
const hasMdField = Boolean(mdData?.frontmatter?.[field] !== undefined);
|
const hasMdField = Boolean(mdData?.frontmatter?.[field] !== undefined);
|
||||||
const hasJsonField = Boolean(jsonSection?.[field] !== undefined);
|
const hasJsonField = Boolean(jsonSection?.[field] !== undefined);
|
||||||
|
|
||||||
if (hasMdField && mdData) {
|
// JSON takes precedence over md, so update JSON first if field exists there
|
||||||
mdData.frontmatter[field] = value;
|
if (hasJsonField) {
|
||||||
mdModified = true;
|
if (!config.agent) config.agent = {};
|
||||||
|
const current = ((config.agent as Record<string, unknown>)[agentName] as Record<string, unknown> | undefined) ?? {};
|
||||||
|
(config.agent as Record<string, unknown>)[agentName] = { ...current, [field]: value };
|
||||||
|
jsonModified = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.agent) config.agent = {};
|
if (hasMdField || creatingNewMd) {
|
||||||
const current = ((config.agent as Record<string, unknown>)[agentName] as Record<string, unknown> | undefined) ?? {};
|
if (mdData) {
|
||||||
(config.agent as Record<string, unknown>)[agentName] = { ...current, [field]: value };
|
mdData.frontmatter[field] = value;
|
||||||
jsonModified = true;
|
mdModified = true;
|
||||||
|
}
|
||||||
if (hasJsonField) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New field - add to appropriate location based on agent source
|
||||||
|
if ((mdExists || creatingNewMd) && mdData) {
|
||||||
|
mdData.frontmatter[field] = value;
|
||||||
|
mdModified = true;
|
||||||
|
} else {
|
||||||
|
if (!config.agent) config.agent = {};
|
||||||
|
const current = ((config.agent as Record<string, unknown>)[agentName] as Record<string, unknown> | undefined) ?? {};
|
||||||
|
(config.agent as Record<string, unknown>)[agentName] = { ...current, [field]: value };
|
||||||
|
jsonModified = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mdModified && mdData) {
|
if (mdModified && mdData && targetPath) {
|
||||||
writeMdFile(mdPath, mdData.frontmatter, mdData.body);
|
writeMdFile(targetPath, mdData.frontmatter, mdData.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonModified) {
|
if (jsonModified) {
|
||||||
writeConfig(config);
|
writeConfig(config, jsonTarget.path || CONFIG_FILE);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAgent = (agentName: string) => {
|
export const deleteAgent = (agentName: string, workingDirectory?: string) => {
|
||||||
const mdPath = path.join(AGENT_DIR, `${agentName}.md`);
|
|
||||||
let deleted = false;
|
let deleted = false;
|
||||||
|
|
||||||
if (fs.existsSync(mdPath)) {
|
// Check project level first (takes precedence)
|
||||||
fs.unlinkSync(mdPath);
|
if (workingDirectory) {
|
||||||
|
const projectPath = getProjectAgentPath(workingDirectory, agentName);
|
||||||
|
if (fs.existsSync(projectPath)) {
|
||||||
|
fs.unlinkSync(projectPath);
|
||||||
|
deleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check user level
|
||||||
|
const userPath = getUserAgentPath(agentName);
|
||||||
|
if (fs.existsSync(userPath)) {
|
||||||
|
fs.unlinkSync(userPath);
|
||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = readConfig();
|
// Also check json config (highest precedence entry only)
|
||||||
const agentMap = (config.agent as Record<string, unknown> | undefined) ?? {};
|
const layers = readConfigLayers(workingDirectory);
|
||||||
if (agentMap[agentName] !== undefined) {
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
||||||
|
if (jsonSource.exists && jsonSource.config && jsonSource.path) {
|
||||||
|
const targetConfig = jsonSource.config as Record<string, unknown>;
|
||||||
|
const agentMap = (targetConfig.agent as Record<string, unknown> | undefined) ?? {};
|
||||||
delete agentMap[agentName];
|
delete agentMap[agentName];
|
||||||
config.agent = agentMap;
|
targetConfig.agent = agentMap;
|
||||||
writeConfig(config);
|
writeConfig(targetConfig, jsonSource.path);
|
||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If nothing was deleted (built-in agent), disable it in highest-precedence config
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
config.agent = agentMap;
|
const jsonTarget = getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER);
|
||||||
|
const targetConfig = (jsonTarget.config || {}) as Record<string, unknown>;
|
||||||
|
const agentMap = (targetConfig.agent as Record<string, unknown> | undefined) ?? {};
|
||||||
agentMap[agentName] = { disable: true };
|
agentMap[agentName] = { disable: true };
|
||||||
writeConfig(config);
|
targetConfig.agent = agentMap;
|
||||||
|
writeConfig(targetConfig, jsonTarget.path || CONFIG_FILE);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,12 +536,14 @@ export const getCommandSources = (commandName: string, workingDirectory?: string
|
|||||||
const mdExists = !!mdPath;
|
const mdExists = !!mdPath;
|
||||||
const mdScope = projectExists ? COMMAND_SCOPE.PROJECT : (userExists ? COMMAND_SCOPE.USER : null);
|
const mdScope = projectExists ? COMMAND_SCOPE.PROJECT : (userExists ? COMMAND_SCOPE.USER : null);
|
||||||
|
|
||||||
const config = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
const commandSection = (config.command as Record<string, unknown> | undefined)?.[commandName] as Record<string, unknown> | undefined;
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
||||||
|
const commandSection = jsonSource.section as Record<string, unknown> | undefined;
|
||||||
|
const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
|
||||||
|
|
||||||
const sources: ConfigSources = {
|
const sources: ConfigSources = {
|
||||||
md: { exists: mdExists, path: mdPath, scope: mdScope, fields: [] },
|
md: { exists: mdExists, path: mdPath, scope: mdScope, fields: [] },
|
||||||
json: { exists: Boolean(commandSection), path: CONFIG_FILE, fields: [] },
|
json: { exists: jsonSource.exists, path: jsonPath || CONFIG_FILE, fields: [] },
|
||||||
projectMd: { exists: projectExists, path: projectPath },
|
projectMd: { exists: projectExists, path: projectPath },
|
||||||
userMd: { exists: userExists, path: userPath }
|
userMd: { exists: userExists, path: userPath }
|
||||||
};
|
};
|
||||||
@@ -326,9 +576,9 @@ export const createCommand = (commandName: string, config: Record<string, unknow
|
|||||||
throw new Error(`Command ${commandName} already exists as user-level .md file`);
|
throw new Error(`Command ${commandName} already exists as user-level .md file`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingConfig = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
const commandMap = existingConfig.command as Record<string, unknown> | undefined;
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
||||||
if (commandMap?.[commandName]) throw new Error(`Command ${commandName} already exists in opencode.json`);
|
if (jsonSource.exists) throw new Error(`Command ${commandName} already exists in opencode.json`);
|
||||||
|
|
||||||
// Determine target path based on requested scope
|
// Determine target path based on requested scope
|
||||||
let targetPath: string;
|
let targetPath: string;
|
||||||
@@ -352,31 +602,40 @@ export const updateCommand = (commandName: string, updates: Record<string, unkno
|
|||||||
// Determine correct path: project level takes precedence
|
// Determine correct path: project level takes precedence
|
||||||
const { path: mdPath } = getCommandWritePath(commandName, workingDirectory);
|
const { path: mdPath } = getCommandWritePath(commandName, workingDirectory);
|
||||||
const mdExists = mdPath ? fs.existsSync(mdPath) : false;
|
const mdExists = mdPath ? fs.existsSync(mdPath) : false;
|
||||||
|
|
||||||
// If no existing md file, we need to create one (for built-in command overrides)
|
const layers = readConfigLayers(workingDirectory);
|
||||||
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
||||||
|
const jsonSection = jsonSource.section as Record<string, unknown> | undefined;
|
||||||
|
const hasJsonFields = Boolean(jsonSource.exists && jsonSection && Object.keys(jsonSection).length > 0);
|
||||||
|
const jsonTarget = jsonSource.exists
|
||||||
|
? { config: jsonSource.config, path: jsonSource.path }
|
||||||
|
: getJsonWriteTarget(layers, workingDirectory ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER);
|
||||||
|
const config = (jsonTarget.config || {}) as Record<string, unknown>;
|
||||||
|
|
||||||
|
// Only create a new md file for built-in overrides (no md + no json)
|
||||||
|
const isBuiltinOverride = !mdExists && !hasJsonFields;
|
||||||
|
|
||||||
let targetPath = mdPath;
|
let targetPath = mdPath;
|
||||||
|
if (!mdExists && isBuiltinOverride) {
|
||||||
if (!mdExists) {
|
// Built-in command override - create at user level
|
||||||
// No existing md file - this is a built-in override, create at user level
|
|
||||||
targetPath = getUserCommandPath(commandName);
|
targetPath = getUserCommandPath(commandName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdData = mdExists && mdPath ? parseMdFile(mdPath) : { frontmatter: {} as Record<string, unknown>, body: '' };
|
const mdData = mdExists && mdPath ? parseMdFile(mdPath) : (isBuiltinOverride ? { frontmatter: {} as Record<string, unknown>, body: '' } : null);
|
||||||
const config = readConfig();
|
|
||||||
const commandMap = (config.command as Record<string, unknown> | undefined) ?? {};
|
|
||||||
const jsonSection = commandMap[commandName] as Record<string, unknown> | undefined;
|
|
||||||
|
|
||||||
let mdModified = false;
|
let mdModified = false;
|
||||||
let jsonModified = false;
|
let jsonModified = false;
|
||||||
let creatingNewMd = !mdExists;
|
let creatingNewMd = isBuiltinOverride;
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(updates || {})) {
|
for (const [field, value] of Object.entries(updates || {})) {
|
||||||
if (field === 'template') {
|
if (field === 'template') {
|
||||||
const normalizedValue = typeof value === 'string' ? value : value == null ? '' : String(value);
|
const normalizedValue = typeof value === 'string' ? value : value == null ? '' : String(value);
|
||||||
|
|
||||||
if (mdExists || creatingNewMd) {
|
if (mdExists || creatingNewMd) {
|
||||||
mdData.body = normalizedValue;
|
if (mdData) {
|
||||||
mdModified = true;
|
mdData.body = normalizedValue;
|
||||||
|
mdModified = true;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,22 +646,18 @@ export const updateCommand = (commandName: string, updates: Record<string, unkno
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new md file for the update
|
// For JSON-only commands, store template inline in JSON
|
||||||
mdData.body = normalizedValue;
|
if (!config.command) config.command = {};
|
||||||
mdModified = true;
|
const current = ((config.command as Record<string, unknown>)[commandName] as Record<string, unknown> | undefined) ?? {};
|
||||||
creatingNewMd = true;
|
(config.command as Record<string, unknown>)[commandName] = { ...current, template: normalizedValue };
|
||||||
|
jsonModified = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasMdField = Boolean(mdData?.frontmatter?.[field] !== undefined);
|
const hasMdField = Boolean(mdData?.frontmatter?.[field] !== undefined);
|
||||||
const hasJsonField = Boolean(jsonSection?.[field] !== undefined);
|
const hasJsonField = Boolean(jsonSection?.[field] !== undefined);
|
||||||
|
|
||||||
if (hasMdField || creatingNewMd) {
|
// JSON takes precedence over md, so update JSON first if field exists there
|
||||||
mdData.frontmatter[field] = value;
|
|
||||||
mdModified = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasJsonField) {
|
if (hasJsonField) {
|
||||||
if (!config.command) config.command = {};
|
if (!config.command) config.command = {};
|
||||||
const current = ((config.command as Record<string, unknown>)[commandName] as Record<string, unknown> | undefined) ?? {};
|
const current = ((config.command as Record<string, unknown>)[commandName] as Record<string, unknown> | undefined) ?? {};
|
||||||
@@ -411,8 +666,16 @@ export const updateCommand = (commandName: string, updates: Record<string, unkno
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New field - add to md if it exists or we're creating one
|
if (hasMdField || creatingNewMd) {
|
||||||
if (mdExists || creatingNewMd) {
|
if (mdData) {
|
||||||
|
mdData.frontmatter[field] = value;
|
||||||
|
mdModified = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New field - add to appropriate location based on command source
|
||||||
|
if ((mdExists || creatingNewMd) && mdData) {
|
||||||
mdData.frontmatter[field] = value;
|
mdData.frontmatter[field] = value;
|
||||||
mdModified = true;
|
mdModified = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -423,12 +686,12 @@ export const updateCommand = (commandName: string, updates: Record<string, unkno
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mdModified && targetPath) {
|
if (mdModified && mdData && targetPath) {
|
||||||
writeMdFile(targetPath, mdData.frontmatter, mdData.body);
|
writeMdFile(targetPath, mdData.frontmatter, mdData.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonModified) {
|
if (jsonModified) {
|
||||||
writeConfig(config);
|
writeConfig(config, jsonTarget.path || CONFIG_FILE);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -451,13 +714,15 @@ export const deleteCommand = (commandName: string, workingDirectory?: string) =>
|
|||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check json config
|
// Also check json config (highest precedence entry only)
|
||||||
const config = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
const commandMap = (config.command as Record<string, unknown> | undefined) ?? {};
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
||||||
if (commandMap[commandName] !== undefined) {
|
if (jsonSource.exists && jsonSource.config && jsonSource.path) {
|
||||||
|
const targetConfig = jsonSource.config as Record<string, unknown>;
|
||||||
|
const commandMap = (targetConfig.command as Record<string, unknown> | undefined) ?? {};
|
||||||
delete commandMap[commandName];
|
delete commandMap[commandName];
|
||||||
config.command = commandMap;
|
targetConfig.command = commandMap;
|
||||||
writeConfig(config);
|
writeConfig(targetConfig, jsonSource.path);
|
||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2165,6 +2165,7 @@ async function main(options = {}) {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
getAgentSources,
|
getAgentSources,
|
||||||
|
getAgentScope,
|
||||||
createAgent,
|
createAgent,
|
||||||
updateAgent,
|
updateAgent,
|
||||||
deleteAgent,
|
deleteAgent,
|
||||||
@@ -2173,17 +2174,20 @@ async function main(options = {}) {
|
|||||||
createCommand,
|
createCommand,
|
||||||
updateCommand,
|
updateCommand,
|
||||||
deleteCommand,
|
deleteCommand,
|
||||||
|
AGENT_SCOPE,
|
||||||
COMMAND_SCOPE
|
COMMAND_SCOPE
|
||||||
} = await import('./lib/opencode-config.js');
|
} = await import('./lib/opencode-config.js');
|
||||||
|
|
||||||
app.get('/api/config/agents/:name', (req, res) => {
|
app.get('/api/config/agents/:name', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const agentName = req.params.name;
|
const agentName = req.params.name;
|
||||||
const sources = getAgentSources(agentName);
|
const workingDirectory = req.query.directory || openCodeWorkingDirectory;
|
||||||
|
const sources = getAgentSources(agentName, workingDirectory);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
name: agentName,
|
name: agentName,
|
||||||
sources: sources,
|
sources: sources,
|
||||||
|
scope: sources.md.scope,
|
||||||
isBuiltIn: !sources.md.exists && !sources.json.exists
|
isBuiltIn: !sources.md.exists && !sources.json.exists
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2195,9 +2199,14 @@ async function main(options = {}) {
|
|||||||
app.post('/api/config/agents/:name', async (req, res) => {
|
app.post('/api/config/agents/:name', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const agentName = req.params.name;
|
const agentName = req.params.name;
|
||||||
const config = req.body;
|
const { scope, ...config } = req.body;
|
||||||
|
const workingDirectory = req.query.directory || openCodeWorkingDirectory;
|
||||||
|
|
||||||
createAgent(agentName, config);
|
console.log('[Server] Creating agent:', agentName);
|
||||||
|
console.log('[Server] Config received:', JSON.stringify(config, null, 2));
|
||||||
|
console.log('[Server] Scope:', scope, 'Working directory:', workingDirectory);
|
||||||
|
|
||||||
|
createAgent(agentName, config, workingDirectory, scope);
|
||||||
await refreshOpenCodeAfterConfigChange('agent creation', {
|
await refreshOpenCodeAfterConfigChange('agent creation', {
|
||||||
agentName
|
agentName
|
||||||
});
|
});
|
||||||
@@ -2218,11 +2227,13 @@ async function main(options = {}) {
|
|||||||
try {
|
try {
|
||||||
const agentName = req.params.name;
|
const agentName = req.params.name;
|
||||||
const updates = req.body;
|
const updates = req.body;
|
||||||
|
const workingDirectory = req.query.directory || openCodeWorkingDirectory;
|
||||||
|
|
||||||
console.log(`[Server] Updating agent: ${agentName}`);
|
console.log(`[Server] Updating agent: ${agentName}`);
|
||||||
console.log('[Server] Updates:', JSON.stringify(updates, null, 2));
|
console.log('[Server] Updates:', JSON.stringify(updates, null, 2));
|
||||||
|
console.log('[Server] Working directory:', workingDirectory);
|
||||||
|
|
||||||
updateAgent(agentName, updates);
|
updateAgent(agentName, updates, workingDirectory);
|
||||||
await refreshOpenCodeAfterConfigChange('agent update');
|
await refreshOpenCodeAfterConfigChange('agent update');
|
||||||
|
|
||||||
console.log(`[Server] Agent ${agentName} updated successfully`);
|
console.log(`[Server] Agent ${agentName} updated successfully`);
|
||||||
@@ -2243,8 +2254,9 @@ async function main(options = {}) {
|
|||||||
app.delete('/api/config/agents/:name', async (req, res) => {
|
app.delete('/api/config/agents/:name', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const agentName = req.params.name;
|
const agentName = req.params.name;
|
||||||
|
const workingDirectory = req.query.directory || openCodeWorkingDirectory;
|
||||||
|
|
||||||
deleteAgent(agentName);
|
deleteAgent(agentName, workingDirectory);
|
||||||
await refreshOpenCodeAfterConfigChange('agent deletion');
|
await refreshOpenCodeAfterConfigChange('agent deletion');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -8,9 +8,17 @@ const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|||||||
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agent');
|
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agent');
|
||||||
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'command');
|
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'command');
|
||||||
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
||||||
|
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
||||||
|
? path.resolve(process.env.OPENCODE_CONFIG)
|
||||||
|
: null;
|
||||||
const PROMPT_FILE_PATTERN = /^\{file:(.+)\}$/i;
|
const PROMPT_FILE_PATTERN = /^\{file:(.+)\}$/i;
|
||||||
|
|
||||||
// Command scope types
|
// Scope types (shared by agents and commands)
|
||||||
|
const AGENT_SCOPE = {
|
||||||
|
USER: 'user',
|
||||||
|
PROJECT: 'project'
|
||||||
|
};
|
||||||
|
|
||||||
const COMMAND_SCOPE = {
|
const COMMAND_SCOPE = {
|
||||||
USER: 'user',
|
USER: 'user',
|
||||||
PROJECT: 'project'
|
PROJECT: 'project'
|
||||||
@@ -28,6 +36,80 @@ function ensureDirs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============== AGENT SCOPE HELPERS ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure project-level agent directory exists
|
||||||
|
*/
|
||||||
|
function ensureProjectAgentDir(workingDirectory) {
|
||||||
|
const projectAgentDir = path.join(workingDirectory, '.opencode', 'agent');
|
||||||
|
if (!fs.existsSync(projectAgentDir)) {
|
||||||
|
fs.mkdirSync(projectAgentDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return projectAgentDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get project-level agent path
|
||||||
|
*/
|
||||||
|
function getProjectAgentPath(workingDirectory, agentName) {
|
||||||
|
return path.join(workingDirectory, '.opencode', 'agent', `${agentName}.md`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user-level agent path
|
||||||
|
*/
|
||||||
|
function getUserAgentPath(agentName) {
|
||||||
|
return path.join(AGENT_DIR, `${agentName}.md`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine agent scope based on where the .md file exists
|
||||||
|
* Priority: project level > user level > null (built-in only)
|
||||||
|
*/
|
||||||
|
function getAgentScope(agentName, workingDirectory) {
|
||||||
|
if (workingDirectory) {
|
||||||
|
const projectPath = getProjectAgentPath(workingDirectory, agentName);
|
||||||
|
if (fs.existsSync(projectPath)) {
|
||||||
|
return { scope: AGENT_SCOPE.PROJECT, path: projectPath };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPath = getUserAgentPath(agentName);
|
||||||
|
if (fs.existsSync(userPath)) {
|
||||||
|
return { scope: AGENT_SCOPE.USER, path: userPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { scope: null, path: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path where an agent should be written based on scope
|
||||||
|
*/
|
||||||
|
function getAgentWritePath(agentName, workingDirectory, requestedScope) {
|
||||||
|
// For updates: check existing location first (project takes precedence)
|
||||||
|
const existing = getAgentScope(agentName, workingDirectory);
|
||||||
|
if (existing.path) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For new agents or built-in overrides: use requested scope or default to user
|
||||||
|
const scope = requestedScope || AGENT_SCOPE.USER;
|
||||||
|
if (scope === AGENT_SCOPE.PROJECT && workingDirectory) {
|
||||||
|
return {
|
||||||
|
scope: AGENT_SCOPE.PROJECT,
|
||||||
|
path: getProjectAgentPath(workingDirectory, agentName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: AGENT_SCOPE.USER,
|
||||||
|
path: getUserAgentPath(agentName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== COMMAND SCOPE HELPERS ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure project-level command directory exists
|
* Ensure project-level command directory exists
|
||||||
*/
|
*/
|
||||||
@@ -132,40 +214,131 @@ function writePromptFile(filePath, content) {
|
|||||||
console.log(`Updated prompt file: ${filePath}`);
|
console.log(`Updated prompt file: ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readConfig() {
|
function getProjectConfigPath(workingDirectory) {
|
||||||
if (!fs.existsSync(CONFIG_FILE)) {
|
if (!workingDirectory) return null;
|
||||||
|
return path.join(workingDirectory, 'opencode.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigPaths(workingDirectory) {
|
||||||
|
return {
|
||||||
|
userPath: CONFIG_FILE,
|
||||||
|
projectPath: getProjectConfigPath(workingDirectory),
|
||||||
|
customPath: CUSTOM_CONFIG_FILE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfigFile(filePath) {
|
||||||
|
if (!filePath || !fs.existsSync(filePath)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
const normalized = stripJsonComments(content).trim();
|
const normalized = stripJsonComments(content).trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return JSON.parse(normalized);
|
return JSON.parse(normalized);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to read config file:', error);
|
console.error(`Failed to read config file: ${filePath}`, error);
|
||||||
throw new Error('Failed to read OpenCode configuration');
|
throw new Error('Failed to read OpenCode configuration');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeConfig(config) {
|
function isPlainObject(value) {
|
||||||
try {
|
return value && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
function mergeConfigs(base, override) {
|
||||||
const backupFile = `${CONFIG_FILE}.openchamber.backup`;
|
if (!isPlainObject(base) || !isPlainObject(override)) {
|
||||||
fs.copyFileSync(CONFIG_FILE, backupFile);
|
return override;
|
||||||
|
}
|
||||||
|
const result = { ...base };
|
||||||
|
for (const [key, value] of Object.entries(override)) {
|
||||||
|
if (key in result) {
|
||||||
|
const baseValue = result[key];
|
||||||
|
if (isPlainObject(baseValue) && isPlainObject(value)) {
|
||||||
|
result[key] = mergeConfigs(baseValue, value);
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfigLayers(workingDirectory) {
|
||||||
|
const { userPath, projectPath, customPath } = getConfigPaths(workingDirectory);
|
||||||
|
const userConfig = readConfigFile(userPath);
|
||||||
|
const projectConfig = readConfigFile(projectPath);
|
||||||
|
const customConfig = readConfigFile(customPath);
|
||||||
|
const mergedConfig = mergeConfigs(mergeConfigs(userConfig, projectConfig), customConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userConfig,
|
||||||
|
projectConfig,
|
||||||
|
customConfig,
|
||||||
|
mergedConfig,
|
||||||
|
paths: { userPath, projectPath, customPath }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfig(workingDirectory) {
|
||||||
|
return readConfigLayers(workingDirectory).mergedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfig(config, filePath = CONFIG_FILE) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const backupFile = `${filePath}.openchamber.backup`;
|
||||||
|
fs.copyFileSync(filePath, backupFile);
|
||||||
console.log(`Created config backup: ${backupFile}`);
|
console.log(`Created config backup: ${backupFile}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
console.log('Successfully wrote config file');
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
|
||||||
|
console.log(`Successfully wrote config file: ${filePath}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to write config file:', error);
|
console.error(`Failed to write config file: ${filePath}`, error);
|
||||||
throw new Error('Failed to write OpenCode configuration');
|
throw new Error('Failed to write OpenCode configuration');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getJsonEntrySource(layers, sectionKey, entryName) {
|
||||||
|
const { userConfig, projectConfig, customConfig, paths } = layers;
|
||||||
|
const customSection = customConfig?.[sectionKey]?.[entryName];
|
||||||
|
if (customSection !== undefined) {
|
||||||
|
return { section: customSection, config: customConfig, path: paths.customPath, exists: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectSection = projectConfig?.[sectionKey]?.[entryName];
|
||||||
|
if (projectSection !== undefined) {
|
||||||
|
return { section: projectSection, config: projectConfig, path: paths.projectPath, exists: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSection = userConfig?.[sectionKey]?.[entryName];
|
||||||
|
if (userSection !== undefined) {
|
||||||
|
return { section: userSection, config: userConfig, path: paths.userPath, exists: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { section: null, config: null, path: null, exists: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJsonWriteTarget(layers, preferredScope) {
|
||||||
|
const { userConfig, projectConfig, customConfig, paths } = layers;
|
||||||
|
if (paths.customPath) {
|
||||||
|
return { config: customConfig, path: paths.customPath };
|
||||||
|
}
|
||||||
|
if (preferredScope === AGENT_SCOPE.PROJECT && paths.projectPath) {
|
||||||
|
return { config: projectConfig, path: paths.projectPath };
|
||||||
|
}
|
||||||
|
if (paths.projectPath) {
|
||||||
|
return { config: projectConfig, path: paths.projectPath };
|
||||||
|
}
|
||||||
|
return { config: userConfig, path: paths.userPath };
|
||||||
|
}
|
||||||
|
|
||||||
function parseMdFile(filePath) {
|
function parseMdFile(filePath) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filePath, 'utf8');
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
@@ -201,23 +374,53 @@ function writeMdFile(filePath, frontmatter, body) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAgentSources(agentName) {
|
function getAgentSources(agentName, workingDirectory) {
|
||||||
const mdPath = path.join(AGENT_DIR, `${agentName}.md`);
|
// Check project level first (takes precedence)
|
||||||
const mdExists = fs.existsSync(mdPath);
|
const projectPath = workingDirectory ? getProjectAgentPath(workingDirectory, agentName) : null;
|
||||||
|
const projectExists = projectPath && fs.existsSync(projectPath);
|
||||||
|
|
||||||
|
// Then check user level
|
||||||
|
const userPath = getUserAgentPath(agentName);
|
||||||
|
const userExists = fs.existsSync(userPath);
|
||||||
|
|
||||||
|
// Determine which md file to use (project takes precedence)
|
||||||
|
const mdPath = projectExists ? projectPath : (userExists ? userPath : null);
|
||||||
|
const mdExists = !!mdPath;
|
||||||
|
const mdScope = projectExists ? AGENT_SCOPE.PROJECT : (userExists ? AGENT_SCOPE.USER : null);
|
||||||
|
|
||||||
const config = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
const jsonSection = config.agent?.[agentName];
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
||||||
|
const jsonSection = jsonSource.section;
|
||||||
|
const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
|
||||||
|
const jsonScope = jsonSource.path === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
|
||||||
|
|
||||||
const sources = {
|
const sources = {
|
||||||
md: {
|
md: {
|
||||||
exists: mdExists,
|
exists: mdExists,
|
||||||
path: mdExists ? mdPath : null,
|
path: mdPath,
|
||||||
|
scope: mdScope,
|
||||||
fields: []
|
fields: []
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
exists: !!jsonSection,
|
exists: jsonSource.exists,
|
||||||
path: CONFIG_FILE,
|
path: jsonPath,
|
||||||
|
scope: jsonSource.exists ? jsonScope : null,
|
||||||
fields: []
|
fields: []
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
exists: jsonSource.exists,
|
||||||
|
path: jsonPath,
|
||||||
|
scope: jsonSource.exists ? jsonScope : null,
|
||||||
|
fields: []
|
||||||
|
},
|
||||||
|
// Additional info about both levels
|
||||||
|
projectMd: {
|
||||||
|
exists: projectExists,
|
||||||
|
path: projectPath
|
||||||
|
},
|
||||||
|
userMd: {
|
||||||
|
exists: userExists,
|
||||||
|
path: userPath
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,94 +439,140 @@ function getAgentSources(agentName) {
|
|||||||
return sources;
|
return sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAgent(agentName, config) {
|
function createAgent(agentName, config, workingDirectory, scope) {
|
||||||
ensureDirs();
|
ensureDirs();
|
||||||
|
|
||||||
const mdPath = path.join(AGENT_DIR, `${agentName}.md`);
|
// Check if agent already exists at either level
|
||||||
|
const projectPath = workingDirectory ? getProjectAgentPath(workingDirectory, agentName) : null;
|
||||||
if (fs.existsSync(mdPath)) {
|
const userPath = getUserAgentPath(agentName);
|
||||||
throw new Error(`Agent ${agentName} already exists as .md file`);
|
|
||||||
|
if (projectPath && fs.existsSync(projectPath)) {
|
||||||
|
throw new Error(`Agent ${agentName} already exists as project-level .md file`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(userPath)) {
|
||||||
|
throw new Error(`Agent ${agentName} already exists as user-level .md file`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingConfig = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
if (existingConfig.agent?.[agentName]) {
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
||||||
|
if (jsonSource.exists) {
|
||||||
throw new Error(`Agent ${agentName} already exists in opencode.json`);
|
throw new Error(`Agent ${agentName} already exists in opencode.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { prompt, ...frontmatter } = config;
|
// Determine target path based on requested scope
|
||||||
|
let targetPath;
|
||||||
|
let targetScope;
|
||||||
|
|
||||||
|
if (scope === AGENT_SCOPE.PROJECT && workingDirectory) {
|
||||||
|
ensureProjectAgentDir(workingDirectory);
|
||||||
|
targetPath = projectPath;
|
||||||
|
targetScope = AGENT_SCOPE.PROJECT;
|
||||||
|
} else {
|
||||||
|
targetPath = userPath;
|
||||||
|
targetScope = AGENT_SCOPE.USER;
|
||||||
|
}
|
||||||
|
|
||||||
writeMdFile(mdPath, frontmatter, prompt || '');
|
// Extract scope and prompt from config - scope is only used for path determination, not written to file
|
||||||
console.log(`Created new agent: ${agentName}`);
|
const { prompt, scope: _scopeFromConfig, ...frontmatter } = config;
|
||||||
|
|
||||||
|
writeMdFile(targetPath, frontmatter, prompt || '');
|
||||||
|
console.log(`Created new agent: ${agentName} (scope: ${targetScope}, path: ${targetPath})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAgent(agentName, updates) {
|
function updateAgent(agentName, updates, workingDirectory) {
|
||||||
ensureDirs();
|
ensureDirs();
|
||||||
|
|
||||||
const mdPath = path.join(AGENT_DIR, `${agentName}.md`);
|
// Determine correct path: project level takes precedence
|
||||||
const mdExists = fs.existsSync(mdPath);
|
const { scope, path: mdPath } = getAgentWritePath(agentName, workingDirectory);
|
||||||
|
const mdExists = mdPath && fs.existsSync(mdPath);
|
||||||
|
|
||||||
|
// Check if agent exists in opencode.json across all config layers
|
||||||
|
const layers = readConfigLayers(workingDirectory);
|
||||||
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
||||||
|
const jsonSection = jsonSource.section;
|
||||||
|
const hasJsonFields = jsonSource.exists && jsonSection && Object.keys(jsonSection).length > 0;
|
||||||
|
const jsonTarget = jsonSource.exists
|
||||||
|
? { config: jsonSource.config, path: jsonSource.path }
|
||||||
|
: getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER);
|
||||||
|
let config = jsonTarget.config || {};
|
||||||
|
|
||||||
|
// Determine if we should create a new md file:
|
||||||
|
// Only for built-in agents (no md file AND no json config)
|
||||||
|
const isBuiltinOverride = !mdExists && !hasJsonFields;
|
||||||
|
|
||||||
|
let targetPath = mdPath;
|
||||||
|
let targetScope = scope;
|
||||||
|
|
||||||
|
if (!mdExists && isBuiltinOverride) {
|
||||||
|
// Built-in agent override - create at user level
|
||||||
|
targetPath = getUserAgentPath(agentName);
|
||||||
|
targetScope = AGENT_SCOPE.USER;
|
||||||
|
}
|
||||||
|
|
||||||
let mdData = mdExists ? parseMdFile(mdPath) : null;
|
// Only create md data for existing md files or built-in overrides
|
||||||
let config = readConfig();
|
let mdData = mdExists ? parseMdFile(mdPath) : (isBuiltinOverride ? { frontmatter: {}, body: '' } : null);
|
||||||
const jsonSection = config.agent?.[agentName];
|
|
||||||
|
|
||||||
let mdModified = false;
|
let mdModified = false;
|
||||||
let jsonModified = false;
|
let jsonModified = false;
|
||||||
|
// Only create new md if it's a built-in override
|
||||||
|
let creatingNewMd = isBuiltinOverride;
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(updates)) {
|
for (const [field, value] of Object.entries(updates)) {
|
||||||
|
|
||||||
if (field === 'prompt') {
|
if (field === 'prompt') {
|
||||||
const normalizedValue = typeof value === 'string' ? value : (value == null ? '' : String(value));
|
const normalizedValue = typeof value === 'string' ? value : (value == null ? '' : String(value));
|
||||||
|
|
||||||
if (mdExists) {
|
if (mdExists || creatingNewMd) {
|
||||||
mdData.body = normalizedValue;
|
if (mdData) {
|
||||||
mdModified = true;
|
mdData.body = normalizedValue;
|
||||||
|
mdModified = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
} else if (isPromptFileReference(jsonSection?.prompt)) {
|
} else if (isPromptFileReference(jsonSection?.prompt)) {
|
||||||
const promptFilePath = resolvePromptFilePath(jsonSection.prompt);
|
const promptFilePath = resolvePromptFilePath(jsonSection.prompt);
|
||||||
if (!promptFilePath) {
|
if (!promptFilePath) {
|
||||||
throw new Error(`Invalid prompt file reference for agent ${agentName}`);
|
throw new Error(`Invalid prompt file reference for agent ${agentName}`);
|
||||||
}
|
}
|
||||||
writePromptFile(promptFilePath, normalizedValue);
|
writePromptFile(promptFilePath, normalizedValue);
|
||||||
|
continue;
|
||||||
} else if (isPromptFileReference(normalizedValue)) {
|
} else if (isPromptFileReference(normalizedValue)) {
|
||||||
if (!config.agent) config.agent = {};
|
if (!config.agent) config.agent = {};
|
||||||
if (!config.agent[agentName]) config.agent[agentName] = {};
|
if (!config.agent[agentName]) config.agent[agentName] = {};
|
||||||
config.agent[agentName].prompt = normalizedValue;
|
config.agent[agentName].prompt = normalizedValue;
|
||||||
jsonModified = true;
|
jsonModified = true;
|
||||||
} else {
|
continue;
|
||||||
if (!config.agent) config.agent = {};
|
|
||||||
if (!config.agent[agentName]) config.agent[agentName] = {};
|
|
||||||
config.agent[agentName].prompt = normalizedValue;
|
|
||||||
jsonModified = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For JSON-only agents, store prompt inline in JSON
|
||||||
|
if (!config.agent) config.agent = {};
|
||||||
|
if (!config.agent[agentName]) config.agent[agentName] = {};
|
||||||
|
config.agent[agentName].prompt = normalizedValue;
|
||||||
|
jsonModified = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inMd = mdData?.frontmatter?.[field] !== undefined;
|
const inMd = mdData?.frontmatter?.[field] !== undefined;
|
||||||
const inJson = jsonSection?.[field] !== undefined;
|
const inJson = jsonSection?.[field] !== undefined;
|
||||||
|
|
||||||
if (inMd) {
|
// JSON takes precedence over md, so update JSON first if field exists there
|
||||||
|
if (inJson) {
|
||||||
mdData.frontmatter[field] = value;
|
|
||||||
mdModified = true;
|
|
||||||
} else if (inJson) {
|
|
||||||
|
|
||||||
if (!config.agent) config.agent = {};
|
if (!config.agent) config.agent = {};
|
||||||
if (!config.agent[agentName]) config.agent[agentName] = {};
|
if (!config.agent[agentName]) config.agent[agentName] = {};
|
||||||
config.agent[agentName][field] = value;
|
config.agent[agentName][field] = value;
|
||||||
jsonModified = true;
|
jsonModified = true;
|
||||||
|
} else if (inMd || creatingNewMd) {
|
||||||
|
if (mdData) {
|
||||||
|
mdData.frontmatter[field] = value;
|
||||||
|
mdModified = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// New field - add to the appropriate location based on agent source
|
||||||
if (mdExists && jsonSection) {
|
if ((mdExists || creatingNewMd) && mdData) {
|
||||||
|
|
||||||
if (!config.agent) config.agent = {};
|
|
||||||
if (!config.agent[agentName]) config.agent[agentName] = {};
|
|
||||||
config.agent[agentName][field] = value;
|
|
||||||
jsonModified = true;
|
|
||||||
} else if (mdExists) {
|
|
||||||
|
|
||||||
mdData.frontmatter[field] = value;
|
mdData.frontmatter[field] = value;
|
||||||
mdModified = true;
|
mdModified = true;
|
||||||
} else {
|
} else {
|
||||||
|
// JSON-only agent or has JSON fields - add to JSON
|
||||||
if (!config.agent) config.agent = {};
|
if (!config.agent) config.agent = {};
|
||||||
if (!config.agent[agentName]) config.agent[agentName] = {};
|
if (!config.agent[agentName]) config.agent[agentName] = {};
|
||||||
config.agent[agentName][field] = value;
|
config.agent[agentName][field] = value;
|
||||||
@@ -332,39 +581,56 @@ function updateAgent(agentName, updates) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mdModified) {
|
if (mdModified && mdData) {
|
||||||
writeMdFile(mdPath, mdData.frontmatter, mdData.body);
|
writeMdFile(targetPath, mdData.frontmatter, mdData.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonModified) {
|
if (jsonModified) {
|
||||||
writeConfig(config);
|
writeConfig(config, jsonTarget.path || CONFIG_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Updated agent: ${agentName} (md: ${mdModified}, json: ${jsonModified})`);
|
console.log(`Updated agent: ${agentName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAgent(agentName) {
|
function deleteAgent(agentName, workingDirectory) {
|
||||||
const mdPath = path.join(AGENT_DIR, `${agentName}.md`);
|
|
||||||
let deleted = false;
|
let deleted = false;
|
||||||
|
|
||||||
if (fs.existsSync(mdPath)) {
|
// Check project level first (takes precedence)
|
||||||
fs.unlinkSync(mdPath);
|
if (workingDirectory) {
|
||||||
console.log(`Deleted agent .md file: ${mdPath}`);
|
const projectPath = getProjectAgentPath(workingDirectory, agentName);
|
||||||
|
if (fs.existsSync(projectPath)) {
|
||||||
|
fs.unlinkSync(projectPath);
|
||||||
|
console.log(`Deleted project-level agent .md file: ${projectPath}`);
|
||||||
|
deleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check user level
|
||||||
|
const userPath = getUserAgentPath(agentName);
|
||||||
|
if (fs.existsSync(userPath)) {
|
||||||
|
fs.unlinkSync(userPath);
|
||||||
|
console.log(`Deleted user-level agent .md file: ${userPath}`);
|
||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = readConfig();
|
// Also check json config (highest precedence entry only)
|
||||||
if (config.agent?.[agentName]) {
|
const layers = readConfigLayers(workingDirectory);
|
||||||
delete config.agent[agentName];
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
||||||
writeConfig(config);
|
if (jsonSource.exists && jsonSource.config && jsonSource.path) {
|
||||||
|
if (!jsonSource.config.agent) jsonSource.config.agent = {};
|
||||||
|
delete jsonSource.config.agent[agentName];
|
||||||
|
writeConfig(jsonSource.config, jsonSource.path);
|
||||||
console.log(`Removed agent from opencode.json: ${agentName}`);
|
console.log(`Removed agent from opencode.json: ${agentName}`);
|
||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If nothing was deleted (built-in agent), disable it in highest-precedence config
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
if (!config.agent) config.agent = {};
|
const jsonTarget = getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER);
|
||||||
config.agent[agentName] = { disable: true };
|
const targetConfig = jsonTarget.config || {};
|
||||||
writeConfig(config);
|
if (!targetConfig.agent) targetConfig.agent = {};
|
||||||
|
targetConfig.agent[agentName] = { disable: true };
|
||||||
|
writeConfig(targetConfig, jsonTarget.path || CONFIG_FILE);
|
||||||
console.log(`Disabled built-in agent: ${agentName}`);
|
console.log(`Disabled built-in agent: ${agentName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,8 +649,10 @@ function getCommandSources(commandName, workingDirectory) {
|
|||||||
const mdExists = !!mdPath;
|
const mdExists = !!mdPath;
|
||||||
const mdScope = projectExists ? COMMAND_SCOPE.PROJECT : (userExists ? COMMAND_SCOPE.USER : null);
|
const mdScope = projectExists ? COMMAND_SCOPE.PROJECT : (userExists ? COMMAND_SCOPE.USER : null);
|
||||||
|
|
||||||
const config = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
const jsonSection = config.command?.[commandName];
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
||||||
|
const jsonSection = jsonSource.section;
|
||||||
|
const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
|
||||||
|
|
||||||
const sources = {
|
const sources = {
|
||||||
md: {
|
md: {
|
||||||
@@ -394,8 +662,8 @@ function getCommandSources(commandName, workingDirectory) {
|
|||||||
fields: []
|
fields: []
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
exists: !!jsonSection,
|
exists: jsonSource.exists,
|
||||||
path: CONFIG_FILE,
|
path: jsonPath,
|
||||||
fields: []
|
fields: []
|
||||||
},
|
},
|
||||||
// Additional info about both levels
|
// Additional info about both levels
|
||||||
@@ -439,8 +707,9 @@ function createCommand(commandName, config, workingDirectory, scope) {
|
|||||||
throw new Error(`Command ${commandName} already exists as user-level .md file`);
|
throw new Error(`Command ${commandName} already exists as user-level .md file`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingConfig = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
if (existingConfig.command?.[commandName]) {
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
||||||
|
if (jsonSource.exists) {
|
||||||
throw new Error(`Command ${commandName} already exists in opencode.json`);
|
throw new Error(`Command ${commandName} already exists in opencode.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,25 +739,33 @@ function updateCommand(commandName, updates, workingDirectory) {
|
|||||||
// Determine correct path: project level takes precedence
|
// Determine correct path: project level takes precedence
|
||||||
const { scope, path: mdPath } = getCommandWritePath(commandName, workingDirectory);
|
const { scope, path: mdPath } = getCommandWritePath(commandName, workingDirectory);
|
||||||
const mdExists = mdPath && fs.existsSync(mdPath);
|
const mdExists = mdPath && fs.existsSync(mdPath);
|
||||||
|
|
||||||
// If no existing md file, we need to create one (for built-in command overrides)
|
const layers = readConfigLayers(workingDirectory);
|
||||||
// Default to user level for built-in overrides
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
||||||
|
const jsonSection = jsonSource.section;
|
||||||
|
const hasJsonFields = jsonSource.exists && jsonSection && Object.keys(jsonSection).length > 0;
|
||||||
|
const jsonTarget = jsonSource.exists
|
||||||
|
? { config: jsonSource.config, path: jsonSource.path }
|
||||||
|
: getJsonWriteTarget(layers, workingDirectory ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER);
|
||||||
|
let config = jsonTarget.config || {};
|
||||||
|
|
||||||
|
// Only create a new md file for built-in overrides (no md + no json)
|
||||||
|
const isBuiltinOverride = !mdExists && !hasJsonFields;
|
||||||
|
|
||||||
let targetPath = mdPath;
|
let targetPath = mdPath;
|
||||||
let targetScope = scope;
|
let targetScope = scope;
|
||||||
|
|
||||||
if (!mdExists) {
|
if (!mdExists && isBuiltinOverride) {
|
||||||
// No existing md file - this is a built-in override, create at user level
|
// Built-in command override - create at user level
|
||||||
targetPath = getUserCommandPath(commandName);
|
targetPath = getUserCommandPath(commandName);
|
||||||
targetScope = COMMAND_SCOPE.USER;
|
targetScope = COMMAND_SCOPE.USER;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mdData = mdExists ? parseMdFile(mdPath) : { frontmatter: {}, body: '' };
|
const mdData = mdExists ? parseMdFile(mdPath) : (isBuiltinOverride ? { frontmatter: {}, body: '' } : null);
|
||||||
let config = readConfig();
|
|
||||||
const jsonSection = config.command?.[commandName];
|
|
||||||
|
|
||||||
let mdModified = false;
|
let mdModified = false;
|
||||||
let jsonModified = false;
|
let jsonModified = false;
|
||||||
let creatingNewMd = !mdExists;
|
let creatingNewMd = isBuiltinOverride;
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(updates)) {
|
for (const [field, value] of Object.entries(updates)) {
|
||||||
|
|
||||||
@@ -496,42 +773,51 @@ function updateCommand(commandName, updates, workingDirectory) {
|
|||||||
const normalizedValue = typeof value === 'string' ? value : (value == null ? '' : String(value));
|
const normalizedValue = typeof value === 'string' ? value : (value == null ? '' : String(value));
|
||||||
|
|
||||||
if (mdExists || creatingNewMd) {
|
if (mdExists || creatingNewMd) {
|
||||||
mdData.body = normalizedValue;
|
if (mdData) {
|
||||||
mdModified = true;
|
mdData.body = normalizedValue;
|
||||||
|
mdModified = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
} else if (isPromptFileReference(jsonSection?.template)) {
|
} else if (isPromptFileReference(jsonSection?.template)) {
|
||||||
const templateFilePath = resolvePromptFilePath(jsonSection.template);
|
const templateFilePath = resolvePromptFilePath(jsonSection.template);
|
||||||
if (!templateFilePath) {
|
if (!templateFilePath) {
|
||||||
throw new Error(`Invalid template file reference for command ${commandName}`);
|
throw new Error(`Invalid template file reference for command ${commandName}`);
|
||||||
}
|
}
|
||||||
writePromptFile(templateFilePath, normalizedValue);
|
writePromptFile(templateFilePath, normalizedValue);
|
||||||
|
continue;
|
||||||
} else if (isPromptFileReference(normalizedValue)) {
|
} else if (isPromptFileReference(normalizedValue)) {
|
||||||
if (!config.command) config.command = {};
|
if (!config.command) config.command = {};
|
||||||
if (!config.command[commandName]) config.command[commandName] = {};
|
if (!config.command[commandName]) config.command[commandName] = {};
|
||||||
config.command[commandName].template = normalizedValue;
|
config.command[commandName].template = normalizedValue;
|
||||||
jsonModified = true;
|
jsonModified = true;
|
||||||
} else {
|
continue;
|
||||||
// Create new md file for the update
|
|
||||||
mdData.body = normalizedValue;
|
|
||||||
mdModified = true;
|
|
||||||
creatingNewMd = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For JSON-only commands, store template inline in JSON
|
||||||
|
if (!config.command) config.command = {};
|
||||||
|
if (!config.command[commandName]) config.command[commandName] = {};
|
||||||
|
config.command[commandName].template = normalizedValue;
|
||||||
|
jsonModified = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inMd = mdData?.frontmatter?.[field] !== undefined;
|
const inMd = mdData?.frontmatter?.[field] !== undefined;
|
||||||
const inJson = jsonSection?.[field] !== undefined;
|
const inJson = jsonSection?.[field] !== undefined;
|
||||||
|
|
||||||
if (inMd || creatingNewMd) {
|
// JSON takes precedence over md, so update JSON first if field exists there
|
||||||
mdData.frontmatter[field] = value;
|
if (inJson) {
|
||||||
mdModified = true;
|
|
||||||
} else if (inJson) {
|
|
||||||
if (!config.command) config.command = {};
|
if (!config.command) config.command = {};
|
||||||
if (!config.command[commandName]) config.command[commandName] = {};
|
if (!config.command[commandName]) config.command[commandName] = {};
|
||||||
config.command[commandName][field] = value;
|
config.command[commandName][field] = value;
|
||||||
jsonModified = true;
|
jsonModified = true;
|
||||||
|
} else if (inMd || creatingNewMd) {
|
||||||
|
if (mdData) {
|
||||||
|
mdData.frontmatter[field] = value;
|
||||||
|
mdModified = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// New field - add to md if it exists or we're creating one
|
// New field - add to appropriate location based on command source
|
||||||
if (mdExists || creatingNewMd) {
|
if ((mdExists || creatingNewMd) && mdData) {
|
||||||
mdData.frontmatter[field] = value;
|
mdData.frontmatter[field] = value;
|
||||||
mdModified = true;
|
mdModified = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -543,12 +829,12 @@ function updateCommand(commandName, updates, workingDirectory) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mdModified) {
|
if (mdModified && mdData) {
|
||||||
writeMdFile(targetPath, mdData.frontmatter, mdData.body);
|
writeMdFile(targetPath, mdData.frontmatter, mdData.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonModified) {
|
if (jsonModified) {
|
||||||
writeConfig(config);
|
writeConfig(config, jsonTarget.path || CONFIG_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Updated command: ${commandName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`);
|
console.log(`Updated command: ${commandName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`);
|
||||||
@@ -575,11 +861,13 @@ function deleteCommand(commandName, workingDirectory) {
|
|||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check json config
|
// Also check json config (highest precedence entry only)
|
||||||
const config = readConfig();
|
const layers = readConfigLayers(workingDirectory);
|
||||||
if (config.command?.[commandName]) {
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
||||||
delete config.command[commandName];
|
if (jsonSource.exists && jsonSource.config && jsonSource.path) {
|
||||||
writeConfig(config);
|
if (!jsonSource.config.command) jsonSource.config.command = {};
|
||||||
|
delete jsonSource.config.command[commandName];
|
||||||
|
writeConfig(jsonSource.config, jsonSource.path);
|
||||||
console.log(`Removed command from opencode.json: ${commandName}`);
|
console.log(`Removed command from opencode.json: ${commandName}`);
|
||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
@@ -591,6 +879,7 @@ function deleteCommand(commandName, workingDirectory) {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
getAgentSources,
|
getAgentSources,
|
||||||
|
getAgentScope,
|
||||||
createAgent,
|
createAgent,
|
||||||
updateAgent,
|
updateAgent,
|
||||||
deleteAgent,
|
deleteAgent,
|
||||||
@@ -604,5 +893,6 @@ export {
|
|||||||
AGENT_DIR,
|
AGENT_DIR,
|
||||||
COMMAND_DIR,
|
COMMAND_DIR,
|
||||||
CONFIG_FILE,
|
CONFIG_FILE,
|
||||||
|
AGENT_SCOPE,
|
||||||
COMMAND_SCOPE
|
COMMAND_SCOPE
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user