@foundation0/api 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/agents.ts +431 -6
- package/git.ts +12 -23
- package/mcp/cli.mjs +1 -1
- package/mcp/cli.ts +81 -18
- package/mcp/client.test.ts +129 -0
- package/mcp/client.ts +417 -9
- package/mcp/index.ts +9 -9
- package/mcp/server.test.ts +117 -0
- package/mcp/server.ts +71 -10
- package/package.json +7 -9
- package/projects.ts +11 -11
- package/dist/git.js +0 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# example-org MCP Server
|
|
2
2
|
|
|
3
|
-
Install and register the
|
|
3
|
+
Install and register the example-org MCP server with the MCP-enabled agent/tooling you use.
|
|
4
4
|
|
|
5
5
|
## 1) Install
|
|
6
6
|
|
|
@@ -23,9 +23,13 @@ The runnable entrypoint is `f0-mcp` (available from `bin` when installed globall
|
|
|
23
23
|
The server supports:
|
|
24
24
|
|
|
25
25
|
- `GITEA_HOST` (recommended, required by git-backed tools)
|
|
26
|
-
- `
|
|
26
|
+
- `FALLBACK_GITEA_HOST` (fallback host variable)
|
|
27
27
|
- `GITEA_TOKEN` (for authenticated actions)
|
|
28
28
|
- `MCP_TOOLS_PREFIX` (or `--tools-prefix`) to namespace tools in clients
|
|
29
|
+
- `MCP_ALLOWED_ROOT_ENDPOINTS` (or `--allowed-root-endpoints`) to whitelist API root endpoints
|
|
30
|
+
- `MCP_DISABLE_WRITE` (or `--disable-write`) to expose read-only tools only
|
|
31
|
+
- `MCP_ENABLE_ISSUES` (or `--enable-issues`) to allow issue endpoints when write mode is disabled
|
|
32
|
+
- `MCP_ADMIN` (or `--admin`) to expose admin-only destructive endpoints
|
|
29
33
|
|
|
30
34
|
Useful defaults:
|
|
31
35
|
|
|
@@ -37,7 +41,7 @@ Useful defaults:
|
|
|
37
41
|
### A) `.codex/config.toml`
|
|
38
42
|
|
|
39
43
|
```toml
|
|
40
|
-
[mcp_servers.
|
|
44
|
+
[mcp_servers.example]
|
|
41
45
|
command = "bun"
|
|
42
46
|
args = ["x", "@foundation0/api", "f0-mcp"]
|
|
43
47
|
enabled = true
|
|
@@ -45,7 +49,7 @@ startup_timeout_ms = 20_000
|
|
|
45
49
|
env = {
|
|
46
50
|
GITEA_HOST = "https://gitea.example.com",
|
|
47
51
|
GITEA_TOKEN = "your-token",
|
|
48
|
-
MCP_TOOLS_PREFIX = "
|
|
52
|
+
MCP_TOOLS_PREFIX = "example"
|
|
49
53
|
}
|
|
50
54
|
```
|
|
51
55
|
|
|
@@ -61,9 +65,9 @@ args = []
|
|
|
61
65
|
```json
|
|
62
66
|
{
|
|
63
67
|
"mcpServers": {
|
|
64
|
-
"
|
|
68
|
+
"example": {
|
|
65
69
|
"command": "bun",
|
|
66
|
-
"args": ["x", "@foundation0/api", "f0-mcp", "--tools-prefix", "
|
|
70
|
+
"args": ["x", "@foundation0/api", "f0-mcp", "--tools-prefix", "example"],
|
|
67
71
|
"env": {
|
|
68
72
|
"GITEA_HOST": "https://gitea.example.com",
|
|
69
73
|
"GITEA_TOKEN": "your-token"
|
|
@@ -82,7 +86,7 @@ Use the same command/args/env block in your MCP server configuration area:
|
|
|
82
86
|
```json
|
|
83
87
|
{
|
|
84
88
|
"command": "bun",
|
|
85
|
-
"args": ["x", "@foundation0/api", "f0-mcp", "--tools-prefix", "
|
|
89
|
+
"args": ["x", "@foundation0/api", "f0-mcp", "--tools-prefix", "example"],
|
|
86
90
|
"env": {
|
|
87
91
|
"GITEA_HOST": "https://gitea.example.com",
|
|
88
92
|
"GITEA_TOKEN": "your-token"
|
|
@@ -105,9 +109,20 @@ Add environment variables for host/token and optional `MCP_TOOLS_PREFIX`.
|
|
|
105
109
|
f0-mcp --help
|
|
106
110
|
|
|
107
111
|
# examples
|
|
108
|
-
f0-mcp --tools-prefix=
|
|
112
|
+
f0-mcp --tools-prefix=example --server-name=my-example
|
|
109
113
|
f0-mcp --tools-prefix api --server-version 1.2.3
|
|
114
|
+
f0-mcp --allowed-root-endpoints projects
|
|
115
|
+
f0-mcp --allowed-root-endpoints agents,projects
|
|
116
|
+
f0-mcp --disable-write
|
|
117
|
+
f0-mcp --allowed-root-endpoints projects --disable-write
|
|
118
|
+
f0-mcp --disable-write --enable-issues
|
|
119
|
+
f0-mcp --admin
|
|
120
|
+
f0-mcp --admin --disable-write --enable-issues
|
|
110
121
|
```
|
|
111
122
|
|
|
112
123
|
- `--tools-prefix` is useful when running multiple MCP servers side-by-side.
|
|
113
124
|
- `--server-name` and `--server-version` are mostly metadata but can help identify logs and client tool sets.
|
|
125
|
+
- `--allowed-root-endpoints` restricts exposed tools to selected root namespaces (`agents`, `projects`).
|
|
126
|
+
- `--disable-write` removes write-capable tools (for example create/update/delete/sync/set/main/run operations).
|
|
127
|
+
- `--enable-issues` is a special-case override for `--disable-write`: issue endpoints remain enabled (`fetchGitTasks`, `readGitTask`, `writeGitTask`).
|
|
128
|
+
- `projects.syncTasks` and `projects.clearIssues` are admin-only and hidden by default; they are exposed only with `--admin` (or `MCP_ADMIN=true`).
|
package/agents.ts
CHANGED
|
@@ -23,12 +23,25 @@ export interface AgentLoadResult {
|
|
|
23
23
|
loaded: unknown
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface AgentRunResult {
|
|
27
|
+
agentName: string
|
|
28
|
+
exitCode: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AgentCreateResult {
|
|
32
|
+
agentName: string
|
|
33
|
+
agentRoot: string
|
|
34
|
+
createdPaths: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
interface ActiveConfigInput {
|
|
27
38
|
path: string
|
|
28
39
|
required: boolean
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
const CLI_NAME = '
|
|
42
|
+
const CLI_NAME = 'example'
|
|
43
|
+
const AGENT_INITIAL_VERSION = 'v0.0.1'
|
|
44
|
+
const DEFAULT_SKILL_NAME = 'coding-standards'
|
|
32
45
|
const VERSION_RE = 'v?\\d+(?:\\.\\d+){2,}'
|
|
33
46
|
const VERSION_RE_CORE = `(?:${VERSION_RE})`
|
|
34
47
|
const VERSION_WITH_EXT_RE = new RegExp(`^(.+)\\.(${VERSION_RE})\\.([A-Za-z][A-Za-z0-9_-]*)$`)
|
|
@@ -36,13 +49,16 @@ const VERSION_ONLY_RE = new RegExp(`^(.+)\\.(${VERSION_RE})$`)
|
|
|
36
49
|
const PRIORITIZED_EXTS = ['.md', '.json', '.yaml', '.yml', '.ts', '.js', '.txt']
|
|
37
50
|
|
|
38
51
|
export function usage(): string {
|
|
39
|
-
return `Usage:\n ${CLI_NAME} agents <agent-name> <file-path> --set-active\n ${CLI_NAME} agents --list\n ${CLI_NAME} agents <agent-name> load\n\n` +
|
|
40
|
-
`Examples:\n ${CLI_NAME} agents coder /system/boot.v0.0.1 --set-active\n ${CLI_NAME} agents coder /system/boot --set-active --latest\n ${CLI_NAME} agents coder /skills/coding-standards.v0.0.1 --set-active\n ${CLI_NAME} agents --list\n` +
|
|
52
|
+
return `Usage:\n ${CLI_NAME} agents create <agent-name>\n ${CLI_NAME} agents <agent-name> <file-path> --set-active\n ${CLI_NAME} agents --list\n ${CLI_NAME} agents <agent-name> load\n ${CLI_NAME} agents <agent-name> run [codex-args...]\n\n` +
|
|
53
|
+
`Examples:\n ${CLI_NAME} agents create reviewer\n ${CLI_NAME} agents coder /system/boot.v0.0.1 --set-active\n ${CLI_NAME} agents coder /system/boot --set-active --latest\n ${CLI_NAME} agents coder /skills/coding-standards.v0.0.1 --set-active\n ${CLI_NAME} agents --list\n` +
|
|
41
54
|
` ${CLI_NAME} agents coder load\n` +
|
|
55
|
+
` ${CLI_NAME} agents coder run --model gpt-5\n` +
|
|
42
56
|
`\n` +
|
|
43
57
|
`file-path is relative to the agent root (leading slash required).\n` +
|
|
44
58
|
`Use --latest to resolve /file-name to the latest version.\n` +
|
|
45
|
-
`The active file created is [file].active.<ext>.\n`
|
|
59
|
+
`The active file created is [file].active.<ext>.\n` +
|
|
60
|
+
`Use "run" to start codex with developer_instructions from system/prompt.ts.\n` +
|
|
61
|
+
`Set EXAMPLE_CODEX_BIN to pin a specific codex binary.\n`
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
export function resolveAgentsRoot(processRoot: string = process.cwd()): string {
|
|
@@ -76,6 +92,200 @@ export function listAgents(processRoot: string = process.cwd()): string[] {
|
|
|
76
92
|
.sort((a, b) => a.localeCompare(b))
|
|
77
93
|
}
|
|
78
94
|
|
|
95
|
+
function normalizeAgentName(agentName: string): string {
|
|
96
|
+
if (!agentName || typeof agentName !== 'string') {
|
|
97
|
+
throw new Error('agent-name is required.')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const normalized = agentName.trim()
|
|
101
|
+
if (!normalized) {
|
|
102
|
+
throw new Error('agent-name is required.')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (normalized.startsWith('-')) {
|
|
106
|
+
throw new Error(`Invalid agent-name: ${agentName}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (normalized === '.' || normalized === '..' || normalized.includes('/') || normalized.includes('\\')) {
|
|
110
|
+
throw new Error(`Invalid agent-name: ${agentName}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(normalized)) {
|
|
114
|
+
throw new Error(`Invalid agent-name: ${agentName}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return normalized
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function titleCaseAgentName(agentName: string): string {
|
|
121
|
+
return agentName
|
|
122
|
+
.split(/[-_]+/)
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
125
|
+
.join(' ')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildBootDocument(agentName: string): string {
|
|
129
|
+
const title = titleCaseAgentName(agentName) || agentName
|
|
130
|
+
return `# ${title} Agent Boot (${AGENT_INITIAL_VERSION})
|
|
131
|
+
|
|
132
|
+
You are the \`${agentName}\` agent.
|
|
133
|
+
|
|
134
|
+
## Role
|
|
135
|
+
- Follow the active workflow and execute scoped tasks.
|
|
136
|
+
- Keep outputs concise, concrete, and implementation-focused.
|
|
137
|
+
- Avoid changing unrelated files or behavior.
|
|
138
|
+
|
|
139
|
+
## Startup Sequence
|
|
140
|
+
1. Read active boot/workflow docs and enabled skills.
|
|
141
|
+
2. Confirm scope and constraints before changes.
|
|
142
|
+
3. Apply the smallest safe change set.
|
|
143
|
+
4. Report changed files and outcomes.
|
|
144
|
+
`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildWorkflowDocument(agentName: string): string {
|
|
148
|
+
return `# ${titleCaseAgentName(agentName) || agentName} Workflow (${AGENT_INITIAL_VERSION})
|
|
149
|
+
|
|
150
|
+
1. Parse request and constraints.
|
|
151
|
+
2. Identify exact files and minimal edits.
|
|
152
|
+
3. Implement and verify changes.
|
|
153
|
+
4. If blocked, stop and report the blocker.
|
|
154
|
+
5. Return result summary with next actions if needed.
|
|
155
|
+
`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildToolsConfig(): string {
|
|
159
|
+
return `${JSON.stringify({
|
|
160
|
+
tools: [
|
|
161
|
+
{
|
|
162
|
+
name: 'shell_command',
|
|
163
|
+
enabled: true,
|
|
164
|
+
description: 'Execute shell commands for file and workspace operations.',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'apply_patch',
|
|
168
|
+
enabled: true,
|
|
169
|
+
description: 'Apply focused file edits safely.',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'read_file',
|
|
173
|
+
enabled: true,
|
|
174
|
+
description: 'Read files when needed for context.',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
permissions: {
|
|
178
|
+
network: false,
|
|
179
|
+
write: true,
|
|
180
|
+
delete: true,
|
|
181
|
+
},
|
|
182
|
+
}, null, 2)}
|
|
183
|
+
`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildModelConfig(): string {
|
|
187
|
+
return `${JSON.stringify({
|
|
188
|
+
provider: 'openai',
|
|
189
|
+
model: 'gpt-5',
|
|
190
|
+
temperature: 0.2,
|
|
191
|
+
max_tokens: 4096,
|
|
192
|
+
top_p: 0.95,
|
|
193
|
+
}, null, 2)}
|
|
194
|
+
`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildDefaultSkillDocument(): string {
|
|
198
|
+
return `# Coding Standards (${AGENT_INITIAL_VERSION})
|
|
199
|
+
|
|
200
|
+
- Keep changes minimal and focused on the request.
|
|
201
|
+
- Preserve existing behavior unless explicitly changing it.
|
|
202
|
+
- Prefer readable, deterministic implementations.
|
|
203
|
+
`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const SYSTEM_PROMPT_TS_TEMPLATE = `import * as fs from 'fs'
|
|
207
|
+
import * as path from 'path'
|
|
208
|
+
|
|
209
|
+
const base = path.join(__dirname)
|
|
210
|
+
const readActive = (p: string) => fs.readFileSync(path.join(base, p), 'utf8')
|
|
211
|
+
|
|
212
|
+
export const boot = readActive('boot.active.md')
|
|
213
|
+
export const workflow = readActive('workflow.active.md')
|
|
214
|
+
export const tools = JSON.parse(readActive('tools.active.json'))
|
|
215
|
+
export const model = JSON.parse(readActive('model.active.json'))
|
|
216
|
+
|
|
217
|
+
export const prompt = [boot, workflow].join('\\n\\n')
|
|
218
|
+
|
|
219
|
+
export const agentConfig = {
|
|
220
|
+
boot,
|
|
221
|
+
workflow,
|
|
222
|
+
tools,
|
|
223
|
+
model,
|
|
224
|
+
prompt,
|
|
225
|
+
}
|
|
226
|
+
`
|
|
227
|
+
|
|
228
|
+
const SKILLS_TS_TEMPLATE = `import * as fs from 'fs'
|
|
229
|
+
import * as path from 'path'
|
|
230
|
+
|
|
231
|
+
export interface SkillFile {
|
|
232
|
+
id: string
|
|
233
|
+
version: string
|
|
234
|
+
activeFile: string
|
|
235
|
+
resolvedFile: string
|
|
236
|
+
content: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const skillsDir = path.join(__dirname)
|
|
240
|
+
const enabledListPath = path.join(skillsDir, 'enabled-skills.md')
|
|
241
|
+
|
|
242
|
+
const resolveVersionAndId = (activeFile: string) => {
|
|
243
|
+
const baseName = path.basename(activeFile, '.active.md')
|
|
244
|
+
const resolved = \`\${baseName}.${AGENT_INITIAL_VERSION}.md\`
|
|
245
|
+
return {
|
|
246
|
+
id: baseName,
|
|
247
|
+
version: '${AGENT_INITIAL_VERSION}',
|
|
248
|
+
resolvedFile: resolved,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const enabledLines = fs
|
|
253
|
+
.readFileSync(enabledListPath, 'utf8')
|
|
254
|
+
.split(/\\r?\\n/)
|
|
255
|
+
.map((line) => line.trim())
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
|
|
258
|
+
export const skills: SkillFile[] = enabledLines.map((activeFile) => {
|
|
259
|
+
const resolved = resolveVersionAndId(activeFile)
|
|
260
|
+
return {
|
|
261
|
+
...resolved,
|
|
262
|
+
activeFile,
|
|
263
|
+
content: fs.readFileSync(path.join(skillsDir, resolved.resolvedFile), 'utf8'),
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
`
|
|
267
|
+
|
|
268
|
+
const LOAD_TS_TEMPLATE = `import { readFileSync } from 'fs'
|
|
269
|
+
import * as path from 'path'
|
|
270
|
+
|
|
271
|
+
import { agentConfig } from './system/prompt'
|
|
272
|
+
import { skills } from './skills/skills'
|
|
273
|
+
|
|
274
|
+
const activeDoc = path.join(__dirname, 'system')
|
|
275
|
+
|
|
276
|
+
const loadActive = (name: string) =>
|
|
277
|
+
readFileSync(path.join(activeDoc, name), 'utf8').trim()
|
|
278
|
+
|
|
279
|
+
export const load = () => ({
|
|
280
|
+
...agentConfig,
|
|
281
|
+
bootDoc: loadActive('boot.active.md'),
|
|
282
|
+
workflowDoc: loadActive('workflow.active.md'),
|
|
283
|
+
skills,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
export type AgentBundle = ReturnType<typeof load>
|
|
287
|
+
`
|
|
288
|
+
|
|
79
289
|
export function parseTargetSpec(spec: string): VersionedFileSpec {
|
|
80
290
|
if (!spec || typeof spec !== 'string') {
|
|
81
291
|
throw new Error('file-path is required.')
|
|
@@ -285,6 +495,68 @@ function createWindowsSymlink(activeFile: string, sourceFile: string): boolean {
|
|
|
285
495
|
return result.status === 0
|
|
286
496
|
}
|
|
287
497
|
|
|
498
|
+
export function createAgent(agentName: string, processRoot: string = process.cwd()): AgentCreateResult {
|
|
499
|
+
const normalizedAgentName = normalizeAgentName(agentName)
|
|
500
|
+
const agentsRoot = resolveAgentsRootFrom(processRoot)
|
|
501
|
+
const agentRoot = path.join(agentsRoot, normalizedAgentName)
|
|
502
|
+
|
|
503
|
+
if (fs.existsSync(agentRoot)) {
|
|
504
|
+
throw new Error(`Agent folder already exists: ${agentRoot}`)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const systemDir = path.join(agentRoot, 'system')
|
|
508
|
+
const skillsDir = path.join(agentRoot, 'skills')
|
|
509
|
+
|
|
510
|
+
fs.mkdirSync(systemDir, { recursive: true })
|
|
511
|
+
fs.mkdirSync(skillsDir, { recursive: true })
|
|
512
|
+
|
|
513
|
+
const createdPaths: string[] = []
|
|
514
|
+
const writeFile = (relativePath: string, content: string) => {
|
|
515
|
+
const absolutePath = path.join(agentRoot, relativePath)
|
|
516
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true })
|
|
517
|
+
fs.writeFileSync(absolutePath, content, 'utf8')
|
|
518
|
+
createdPaths.push(absolutePath)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const bootFileName = `boot.${AGENT_INITIAL_VERSION}.md`
|
|
522
|
+
const workflowFileName = `workflow.${AGENT_INITIAL_VERSION}.md`
|
|
523
|
+
const toolsFileName = `tools.${AGENT_INITIAL_VERSION}.json`
|
|
524
|
+
const modelFileName = `model.${AGENT_INITIAL_VERSION}.json`
|
|
525
|
+
const skillFileName = `${DEFAULT_SKILL_NAME}.${AGENT_INITIAL_VERSION}.md`
|
|
526
|
+
const activeSkillFileName = `${DEFAULT_SKILL_NAME}.active.md`
|
|
527
|
+
|
|
528
|
+
writeFile(path.join('system', bootFileName), buildBootDocument(normalizedAgentName))
|
|
529
|
+
writeFile(path.join('system', workflowFileName), buildWorkflowDocument(normalizedAgentName))
|
|
530
|
+
writeFile(path.join('system', toolsFileName), buildToolsConfig())
|
|
531
|
+
writeFile(path.join('system', modelFileName), buildModelConfig())
|
|
532
|
+
writeFile(path.join('system', 'prompt.ts'), SYSTEM_PROMPT_TS_TEMPLATE)
|
|
533
|
+
|
|
534
|
+
writeFile(path.join('skills', skillFileName), buildDefaultSkillDocument())
|
|
535
|
+
writeFile(path.join('skills', 'enabled-skills.md'), `${activeSkillFileName}\n`)
|
|
536
|
+
writeFile(path.join('skills', 'skills.ts'), SKILLS_TS_TEMPLATE)
|
|
537
|
+
|
|
538
|
+
writeFile('load.ts', LOAD_TS_TEMPLATE)
|
|
539
|
+
|
|
540
|
+
const createActiveLink = (versionedFile: string, activeFile: string) => {
|
|
541
|
+
const source = path.join(agentRoot, versionedFile)
|
|
542
|
+
const target = path.join(agentRoot, activeFile)
|
|
543
|
+
setActiveLink(source, target)
|
|
544
|
+
createdPaths.push(target)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
createActiveLink(path.join('system', bootFileName), path.join('system', 'boot.active.md'))
|
|
548
|
+
createActiveLink(path.join('system', workflowFileName), path.join('system', 'workflow.active.md'))
|
|
549
|
+
createActiveLink(path.join('system', toolsFileName), path.join('system', 'tools.active.json'))
|
|
550
|
+
createActiveLink(path.join('system', modelFileName), path.join('system', 'model.active.json'))
|
|
551
|
+
createActiveLink(path.join('skills', skillFileName), path.join('skills', activeSkillFileName))
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
agentName: normalizedAgentName,
|
|
555
|
+
agentRoot,
|
|
556
|
+
createdPaths: createdPaths.sort((a, b) => a.localeCompare(b)),
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
288
560
|
export function setActive(
|
|
289
561
|
agentName: string,
|
|
290
562
|
target: string,
|
|
@@ -362,6 +634,135 @@ export async function loadAgent(agentName: string, processRoot: string = process
|
|
|
362
634
|
}
|
|
363
635
|
}
|
|
364
636
|
|
|
637
|
+
type SpawnCodexResult = {
|
|
638
|
+
status: number | null
|
|
639
|
+
signal: NodeJS.Signals | null
|
|
640
|
+
error?: Error
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
type SpawnCodexFn = (args: string[]) => SpawnCodexResult
|
|
644
|
+
|
|
645
|
+
function getCodexCommand(): string {
|
|
646
|
+
const override = process.env.EXAMPLE_CODEX_BIN?.trim()
|
|
647
|
+
if (override) {
|
|
648
|
+
return override
|
|
649
|
+
}
|
|
650
|
+
if (process.platform === 'win32') {
|
|
651
|
+
// Prefer npm/pnpm cmd shim over stale codex.exe binaries.
|
|
652
|
+
return 'codex.cmd'
|
|
653
|
+
}
|
|
654
|
+
return 'codex'
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const defaultSpawnCodex: SpawnCodexFn = (args) => {
|
|
658
|
+
const spawnOptions = {
|
|
659
|
+
stdio: 'inherit' as const,
|
|
660
|
+
env: process.env,
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const primaryCommand = getCodexCommand()
|
|
664
|
+
const primary = spawnSync(primaryCommand, args, spawnOptions)
|
|
665
|
+
if (primary.error) {
|
|
666
|
+
const err = primary.error as NodeJS.ErrnoException
|
|
667
|
+
if (
|
|
668
|
+
process.platform === 'win32'
|
|
669
|
+
&& !process.env.EXAMPLE_CODEX_BIN
|
|
670
|
+
&& primaryCommand === 'codex.cmd'
|
|
671
|
+
&& err.code === 'ENOENT'
|
|
672
|
+
) {
|
|
673
|
+
return spawnSync('codex', args, spawnOptions)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return primary
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function toExitCode(signal: NodeJS.Signals | null): number {
|
|
681
|
+
if (!signal) {
|
|
682
|
+
return 1
|
|
683
|
+
}
|
|
684
|
+
if (signal === 'SIGINT') {
|
|
685
|
+
return 130
|
|
686
|
+
}
|
|
687
|
+
if (signal === 'SIGTERM') {
|
|
688
|
+
return 143
|
|
689
|
+
}
|
|
690
|
+
return 1
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
694
|
+
return typeof value === 'object' && value !== null
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function resolvePromptValue(value: unknown): Promise<unknown> {
|
|
698
|
+
if (typeof value === 'function') {
|
|
699
|
+
return await value()
|
|
700
|
+
}
|
|
701
|
+
return value
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function extractPromptString(value: unknown): string | null {
|
|
705
|
+
if (typeof value === 'string') {
|
|
706
|
+
return value
|
|
707
|
+
}
|
|
708
|
+
if (isRecord(value) && typeof value.prompt === 'string') {
|
|
709
|
+
return value.prompt
|
|
710
|
+
}
|
|
711
|
+
return null
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export async function loadAgentPrompt(agentName: string, processRoot: string = process.cwd()): Promise<string> {
|
|
715
|
+
const agentsRoot = resolveAgentsRoot(processRoot)
|
|
716
|
+
const agentDir = path.join(agentsRoot, agentName)
|
|
717
|
+
|
|
718
|
+
if (!existsDir(agentDir)) {
|
|
719
|
+
throw new Error(`Agent folder not found: ${agentDir}`)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const promptFile = path.join(agentDir, 'system', 'prompt.ts')
|
|
723
|
+
if (!existsFile(promptFile)) {
|
|
724
|
+
throw new Error(`Prompt file not found for agent '${agentName}': ${promptFile}`)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const promptModuleUrl = pathToFileURL(promptFile).href
|
|
728
|
+
const promptModule = await import(promptModuleUrl)
|
|
729
|
+
const candidates = [
|
|
730
|
+
await resolvePromptValue(promptModule.prompt),
|
|
731
|
+
await resolvePromptValue(promptModule.agentConfig),
|
|
732
|
+
await resolvePromptValue(promptModule.default),
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
for (const candidate of candidates) {
|
|
736
|
+
const promptText = extractPromptString(candidate)
|
|
737
|
+
if (promptText !== null) {
|
|
738
|
+
return promptText
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
throw new Error(`Prompt module for agent '${agentName}' must export a prompt string.`)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export async function runAgent(
|
|
746
|
+
agentName: string,
|
|
747
|
+
codexArgs: string[] = [],
|
|
748
|
+
processRoot: string = process.cwd(),
|
|
749
|
+
options: { spawnCodex?: SpawnCodexFn } = {}
|
|
750
|
+
): Promise<AgentRunResult> {
|
|
751
|
+
const prompt = await loadAgentPrompt(agentName, processRoot)
|
|
752
|
+
const spawnCodex = options.spawnCodex ?? defaultSpawnCodex
|
|
753
|
+
const args = ['--config', `developer_instructions=${prompt}`, ...codexArgs]
|
|
754
|
+
const result = spawnCodex(args)
|
|
755
|
+
|
|
756
|
+
if (result.error) {
|
|
757
|
+
throw new Error(`Failed to start codex: ${result.error.message}`)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
agentName,
|
|
762
|
+
exitCode: typeof result.status === 'number' ? result.status : toExitCode(result.signal),
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
365
766
|
function validateActiveConfigInputs(agentDir: string): void {
|
|
366
767
|
const issues: string[] = []
|
|
367
768
|
|
|
@@ -432,6 +833,7 @@ export async function main(argv: string[], processRoot: string = process.cwd()):
|
|
|
432
833
|
|
|
433
834
|
const [scope, maybeAgentOrFlag, maybeTarget, ...rest] = argv
|
|
434
835
|
const listMode = scope === 'agents' && maybeAgentOrFlag === '--list'
|
|
836
|
+
const createMode = scope === 'agents' && maybeAgentOrFlag === 'create'
|
|
435
837
|
|
|
436
838
|
if (scope !== 'agents') {
|
|
437
839
|
throw new Error('Expected command `agents` as first positional argument.')
|
|
@@ -445,9 +847,24 @@ export async function main(argv: string[], processRoot: string = process.cwd()):
|
|
|
445
847
|
return listAgents(processRoot).join('\n')
|
|
446
848
|
}
|
|
447
849
|
|
|
850
|
+
if (createMode) {
|
|
851
|
+
if (!maybeTarget) {
|
|
852
|
+
throw new Error('Missing required argument: <agent-name>.')
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const unknownFlags = rest.filter(Boolean)
|
|
856
|
+
if (unknownFlags.length > 0) {
|
|
857
|
+
throw new Error(`Unknown flags for create mode: ${unknownFlags.join(', ')}`)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const result = createAgent(maybeTarget, processRoot)
|
|
861
|
+
const displayPath = path.relative(processRoot, result.agentRoot) || result.agentRoot
|
|
862
|
+
return `[${CLI_NAME}] created agent: ${result.agentName} (${displayPath})`
|
|
863
|
+
}
|
|
864
|
+
|
|
448
865
|
const agentName = maybeAgentOrFlag
|
|
449
866
|
const target = maybeTarget
|
|
450
|
-
const
|
|
867
|
+
const setActiveRequested = rest.includes('--set-active')
|
|
451
868
|
const useLatest = rest.includes('--latest')
|
|
452
869
|
|
|
453
870
|
if (target === 'load') {
|
|
@@ -458,11 +875,19 @@ export async function main(argv: string[], processRoot: string = process.cwd()):
|
|
|
458
875
|
return JSON.stringify(result.loaded, null, 2)
|
|
459
876
|
}
|
|
460
877
|
|
|
878
|
+
if (target === 'run') {
|
|
879
|
+
const result = await runAgent(agentName, rest, processRoot)
|
|
880
|
+
if (result.exitCode !== 0) {
|
|
881
|
+
process.exitCode = result.exitCode
|
|
882
|
+
}
|
|
883
|
+
return ''
|
|
884
|
+
}
|
|
885
|
+
|
|
461
886
|
if (!agentName || !target) {
|
|
462
887
|
throw new Error('Missing required arguments: <agent-name> and <file-path>.')
|
|
463
888
|
}
|
|
464
889
|
|
|
465
|
-
if (!
|
|
890
|
+
if (!setActiveRequested) {
|
|
466
891
|
throw new Error('`--set-active` is required for this operation.')
|
|
467
892
|
}
|
|
468
893
|
|
package/git.ts
CHANGED
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
export type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
response: {
|
|
11
|
-
headers: Record<string, string>
|
|
12
|
-
}
|
|
13
|
-
status: number
|
|
14
|
-
ok: boolean
|
|
15
|
-
body: T
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type GitServiceApiMethod = (...args: unknown[]) => Promise<GitServiceApiExecutionResult>
|
|
19
|
-
|
|
20
|
-
export type GitServiceApi = {
|
|
21
|
-
[key: string]: GitServiceApi | GitServiceApiMethod
|
|
22
|
-
}
|
|
1
|
+
export type {
|
|
2
|
+
GitLabelManagementApi,
|
|
3
|
+
GitLabelManagementDefaults,
|
|
4
|
+
GitRepositoryLabel,
|
|
5
|
+
GitServiceApi,
|
|
6
|
+
GitServiceApiExecutionResult,
|
|
7
|
+
GitServiceApiMethod,
|
|
8
|
+
} from '../git/packages/git/src/index.ts'
|
|
23
9
|
|
|
24
10
|
export {
|
|
11
|
+
attachGitLabelManagementApi,
|
|
25
12
|
buildGitApiMockResponse,
|
|
26
13
|
callIssueDependenciesApi,
|
|
14
|
+
createGitLabelManagementApi,
|
|
27
15
|
createGitServiceApi,
|
|
16
|
+
extractRepositoryLabels,
|
|
28
17
|
extractDependencyIssueNumbers,
|
|
29
18
|
resolveProjectRepoIdentity,
|
|
30
19
|
syncIssueDependencies,
|
|
31
|
-
} from '
|
|
20
|
+
} from '../git/packages/git/src/index.ts'
|