Skip to content

Commit 42f20ce

Browse files
committed
Release v0.0.14
1 parent 95fbdc3 commit 42f20ce

25 files changed

+2086
-445
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "21st-desktop",
3-
"version": "0.0.13",
3+
"version": "0.0.14",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": "21st.dev",
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import * as fs from "fs/promises"
2+
import * as path from "path"
3+
import * as os from "os"
4+
import matter from "gray-matter"
5+
6+
// Valid model values for agents
7+
export const VALID_AGENT_MODELS = ["sonnet", "opus", "haiku", "inherit"] as const
8+
export type AgentModel = (typeof VALID_AGENT_MODELS)[number]
9+
10+
// Agent definition parsed from markdown file
11+
export interface ParsedAgent {
12+
name: string
13+
description: string
14+
prompt: string
15+
tools?: string[]
16+
disallowedTools?: string[]
17+
model?: AgentModel
18+
}
19+
20+
// Agent with source/path metadata
21+
export interface FileAgent extends ParsedAgent {
22+
source: "user" | "project"
23+
path: string
24+
}
25+
26+
/**
27+
* Parse agent markdown file with YAML frontmatter
28+
* Format:
29+
* ---
30+
* name: code-reviewer
31+
* description: Reviews code for quality
32+
* tools: Read, Glob, Grep
33+
* model: sonnet
34+
* ---
35+
*
36+
* You are a code reviewer. When invoked...
37+
*/
38+
export function parseAgentMd(
39+
content: string,
40+
filename: string
41+
): Partial<ParsedAgent> {
42+
try {
43+
const { data, content: body } = matter(content)
44+
45+
// Parse tools - can be comma-separated string or array
46+
let tools: string[] | undefined
47+
if (typeof data.tools === "string") {
48+
tools = data.tools
49+
.split(",")
50+
.map((t: string) => t.trim())
51+
.filter(Boolean)
52+
} else if (Array.isArray(data.tools)) {
53+
tools = data.tools
54+
}
55+
56+
// Parse disallowedTools
57+
let disallowedTools: string[] | undefined
58+
if (typeof data.disallowedTools === "string") {
59+
disallowedTools = data.disallowedTools
60+
.split(",")
61+
.map((t: string) => t.trim())
62+
.filter(Boolean)
63+
} else if (Array.isArray(data.disallowedTools)) {
64+
disallowedTools = data.disallowedTools
65+
}
66+
67+
// Validate model
68+
const model =
69+
data.model && VALID_AGENT_MODELS.includes(data.model)
70+
? (data.model as AgentModel)
71+
: undefined
72+
73+
return {
74+
name:
75+
typeof data.name === "string" ? data.name : filename.replace(".md", ""),
76+
description: typeof data.description === "string" ? data.description : "",
77+
prompt: body.trim(),
78+
tools,
79+
disallowedTools,
80+
model,
81+
}
82+
} catch (err) {
83+
console.error("[agents] Failed to parse markdown:", err)
84+
return {}
85+
}
86+
}
87+
88+
/**
89+
* Generate markdown content for agent file
90+
*/
91+
export function generateAgentMd(agent: {
92+
name: string
93+
description: string
94+
prompt: string
95+
tools?: string[]
96+
disallowedTools?: string[]
97+
model?: AgentModel
98+
}): string {
99+
const frontmatter: string[] = []
100+
frontmatter.push(`name: ${agent.name}`)
101+
frontmatter.push(`description: ${agent.description}`)
102+
if (agent.tools && agent.tools.length > 0) {
103+
frontmatter.push(`tools: ${agent.tools.join(", ")}`)
104+
}
105+
if (agent.disallowedTools && agent.disallowedTools.length > 0) {
106+
frontmatter.push(`disallowedTools: ${agent.disallowedTools.join(", ")}`)
107+
}
108+
if (agent.model && agent.model !== "inherit") {
109+
frontmatter.push(`model: ${agent.model}`)
110+
}
111+
112+
return `---\n${frontmatter.join("\n")}\n---\n\n${agent.prompt}`
113+
}
114+
115+
/**
116+
* Load agent definition from filesystem by name
117+
* Searches in user (~/.claude/agents/) and project (.claude/agents/) directories
118+
*/
119+
export async function loadAgent(
120+
name: string,
121+
cwd?: string
122+
): Promise<ParsedAgent | null> {
123+
const locations = [
124+
path.join(os.homedir(), ".claude", "agents"),
125+
...(cwd ? [path.join(cwd, ".claude", "agents")] : []),
126+
]
127+
128+
for (const dir of locations) {
129+
const agentPath = path.join(dir, `${name}.md`)
130+
try {
131+
const content = await fs.readFile(agentPath, "utf-8")
132+
const parsed = parseAgentMd(content, `${name}.md`)
133+
134+
if (parsed.description && parsed.prompt) {
135+
return {
136+
name: parsed.name || name,
137+
description: parsed.description,
138+
prompt: parsed.prompt,
139+
tools: parsed.tools,
140+
disallowedTools: parsed.disallowedTools,
141+
model: parsed.model,
142+
}
143+
}
144+
} catch {
145+
continue
146+
}
147+
}
148+
149+
return null
150+
}
151+
152+
/**
153+
* Scan directory for agent .md files
154+
* Format: .claude/agents/agent-name.md
155+
*/
156+
export async function scanAgentsDirectory(
157+
dir: string,
158+
source: "user" | "project"
159+
): Promise<FileAgent[]> {
160+
const agents: FileAgent[] = []
161+
162+
try {
163+
await fs.access(dir)
164+
const entries = await fs.readdir(dir, { withFileTypes: true })
165+
166+
for (const entry of entries) {
167+
// Validate entry name for security (prevent path traversal)
168+
if (
169+
entry.name.includes("..") ||
170+
entry.name.includes("/") ||
171+
entry.name.includes("\\")
172+
) {
173+
console.warn(`[agents] Skipping invalid filename: ${entry.name}`)
174+
continue
175+
}
176+
177+
// Accept .md files (Claude Code native format)
178+
if (entry.isFile() && entry.name.endsWith(".md")) {
179+
const agentPath = path.join(dir, entry.name)
180+
try {
181+
const content = await fs.readFile(agentPath, "utf-8")
182+
const parsed = parseAgentMd(content, entry.name)
183+
184+
if (parsed.description && parsed.prompt) {
185+
agents.push({
186+
name: parsed.name || entry.name.replace(".md", ""),
187+
description: parsed.description,
188+
prompt: parsed.prompt,
189+
tools: parsed.tools,
190+
disallowedTools: parsed.disallowedTools,
191+
model: parsed.model,
192+
source,
193+
path: agentPath,
194+
})
195+
}
196+
} catch (err) {
197+
console.error(`[agents] Failed to read agent ${entry.name}:`, err)
198+
}
199+
}
200+
}
201+
} catch (err) {
202+
// Directory doesn't exist or not accessible
203+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
204+
console.warn(`[agents] Could not scan directory ${dir}:`, err)
205+
}
206+
}
207+
208+
return agents
209+
}
210+
211+
/**
212+
* Build agents Record for SDK Options
213+
* This properly registers agents with the SDK so Claude can invoke them via Task tool
214+
*/
215+
export async function buildAgentsOption(
216+
agentNames: string[],
217+
cwd?: string
218+
): Promise<
219+
Record<
220+
string,
221+
{ description: string; prompt: string; tools?: string[]; model?: AgentModel }
222+
>
223+
> {
224+
if (agentNames.length === 0) return {}
225+
226+
const agents: Record<
227+
string,
228+
{ description: string; prompt: string; tools?: string[]; model?: AgentModel }
229+
> = {}
230+
231+
for (const name of agentNames) {
232+
const agent = await loadAgent(name, cwd)
233+
if (agent) {
234+
agents[name] = {
235+
description: agent.description,
236+
prompt: agent.prompt,
237+
...(agent.tools && { tools: agent.tools }),
238+
...(agent.model && { model: agent.model }),
239+
}
240+
}
241+
}
242+
243+
return agents
244+
}

0 commit comments

Comments
 (0)