orcastrator 0.2.4 → 0.2.6
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 +20 -0
- package/dist/agents/claude/session.js +13 -4
- package/dist/agents/codex/session.js +6 -5
- package/dist/cli/commands/plan.js +3 -1
- package/dist/cli/commands/run.js +7 -2
- package/dist/core/codex-config.js +58 -0
- package/dist/core/config-loader.js +13 -0
- package/dist/core/planner.js +17 -2
- package/dist/core/task-runner.js +14 -1
- package/dist/utils/skill-loader.js +115 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,10 +82,30 @@ export default {
|
|
|
82
82
|
onTaskComplete: "echo task done: $ORCA_TASK_NAME",
|
|
83
83
|
onComplete: "echo run complete",
|
|
84
84
|
onError: "echo run failed"
|
|
85
|
+
},
|
|
86
|
+
codex: {
|
|
87
|
+
model: "gpt-5.3-codex", // override the codex model
|
|
88
|
+
multiAgent: true, // enable codex multi-agent (see below)
|
|
85
89
|
}
|
|
86
90
|
};
|
|
87
91
|
```
|
|
88
92
|
|
|
93
|
+
### Multi-agent mode
|
|
94
|
+
|
|
95
|
+
Codex supports experimental [multi-agent workflows](https://developers.openai.com/codex/multi-agent) where it can spawn parallel sub-agents for complex tasks.
|
|
96
|
+
|
|
97
|
+
To enable it in orca, set `codex.multiAgent: true` in your config:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
export default {
|
|
101
|
+
codex: { multiAgent: true }
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
When enabled, orca adds `multi_agent = true` to your global `~/.codex/config.toml`. If you already have multi-agent enabled in your Codex config, it will work automatically without setting anything in orca.
|
|
106
|
+
|
|
107
|
+
> **Note:** Multi-agent is off by default because enabling it modifies your global Codex configuration. It is currently an experimental Codex feature.
|
|
108
|
+
|
|
89
109
|
## Reference
|
|
90
110
|
|
|
91
111
|
### Flags
|
|
@@ -13,8 +13,9 @@ function buildPlanningPrompt(spec, systemContext) {
|
|
|
13
13
|
spec
|
|
14
14
|
].join("\n\n");
|
|
15
15
|
}
|
|
16
|
-
function buildTaskExecutionPrompt(task, runId, cwd) {
|
|
16
|
+
function buildTaskExecutionPrompt(task, runId, cwd, systemContext) {
|
|
17
17
|
return [
|
|
18
|
+
...(systemContext ? [systemContext] : []),
|
|
18
19
|
"You are Orca's task execution assistant.",
|
|
19
20
|
`Run ID: ${runId}`,
|
|
20
21
|
`Repository CWD: ${cwd}`,
|
|
@@ -54,7 +55,15 @@ function parseTaskArray(raw) {
|
|
|
54
55
|
if (!Array.isArray(parsed)) {
|
|
55
56
|
throw new Error("Claude plan response was not a JSON array");
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
// Coerce numeric task IDs and dependency refs to strings.
|
|
59
|
+
// LLMs sometimes emit numeric IDs (1, 2, 3) even when we ask for strings.
|
|
60
|
+
return parsed.map((task) => ({
|
|
61
|
+
...task,
|
|
62
|
+
id: String(task.id),
|
|
63
|
+
dependencies: Array.isArray(task.dependencies)
|
|
64
|
+
? task.dependencies.map(String)
|
|
65
|
+
: [],
|
|
66
|
+
}));
|
|
58
67
|
}
|
|
59
68
|
function parseTaskExecution(raw) {
|
|
60
69
|
const parsed = JSON.parse(raw);
|
|
@@ -120,14 +129,14 @@ export async function planSpec(spec, systemContext) {
|
|
|
120
129
|
session.close();
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
|
-
export async function executeTask(task, runId, config) {
|
|
132
|
+
export async function executeTask(task, runId, config, systemContext) {
|
|
124
133
|
const session = unstable_v2_createSession({
|
|
125
134
|
model: getModel(config),
|
|
126
135
|
permissionMode: "bypassPermissions",
|
|
127
136
|
});
|
|
128
137
|
try {
|
|
129
138
|
const streamPromise = collectSessionResult(session);
|
|
130
|
-
await session.send(buildTaskExecutionPrompt(task, runId, process.cwd()));
|
|
139
|
+
await session.send(buildTaskExecutionPrompt(task, runId, process.cwd(), systemContext));
|
|
131
140
|
const rawResponse = await streamPromise;
|
|
132
141
|
return parseTaskExecution(rawResponse);
|
|
133
142
|
}
|
|
@@ -13,8 +13,9 @@ function buildPlanningPrompt(spec, systemContext) {
|
|
|
13
13
|
spec,
|
|
14
14
|
].join("\n\n");
|
|
15
15
|
}
|
|
16
|
-
function buildTaskExecutionPrompt(task, runId, cwd) {
|
|
16
|
+
function buildTaskExecutionPrompt(task, runId, cwd, systemContext) {
|
|
17
17
|
return [
|
|
18
|
+
...(systemContext ? [systemContext] : []),
|
|
18
19
|
"You are Orca's task execution assistant.",
|
|
19
20
|
`Run ID: ${runId}`,
|
|
20
21
|
`Repository CWD: ${cwd}`,
|
|
@@ -164,13 +165,13 @@ export async function createCodexSession(cwd, config) {
|
|
|
164
165
|
rawResponse,
|
|
165
166
|
};
|
|
166
167
|
},
|
|
167
|
-
async executeTask(task, runId) {
|
|
168
|
+
async executeTask(task, runId, systemContext) {
|
|
168
169
|
const result = await client.runTurn({
|
|
169
170
|
threadId,
|
|
170
171
|
input: [
|
|
171
172
|
{
|
|
172
173
|
type: "text",
|
|
173
|
-
text: buildTaskExecutionPrompt(task, runId, cwd),
|
|
174
|
+
text: buildTaskExecutionPrompt(task, runId, cwd, systemContext),
|
|
174
175
|
},
|
|
175
176
|
],
|
|
176
177
|
});
|
|
@@ -236,10 +237,10 @@ export async function planSpec(spec, systemContext, config) {
|
|
|
236
237
|
await session.disconnect();
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
|
-
export async function executeTask(task, runId, config) {
|
|
240
|
+
export async function executeTask(task, runId, config, systemContext) {
|
|
240
241
|
const session = await createCodexSession(process.cwd(), config);
|
|
241
242
|
try {
|
|
242
|
-
return await session.executeTask(task, runId);
|
|
243
|
+
return await session.executeTask(task, runId, systemContext);
|
|
243
244
|
}
|
|
244
245
|
finally {
|
|
245
246
|
await session.disconnect();
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { resolveConfig } from "../../core/config-loader.js";
|
|
4
5
|
import { runPlanner } from "../../core/planner.js";
|
|
5
6
|
import { RunStore } from "../../state/store.js";
|
|
6
7
|
import { generateRunId } from "../../utils/ids.js";
|
|
7
8
|
export async function planCommand(options) {
|
|
8
9
|
const specPath = path.resolve(options.spec);
|
|
9
10
|
await access(specPath, fsConstants.R_OK);
|
|
11
|
+
const orcaConfig = await resolveConfig(options.config);
|
|
10
12
|
const runId = generateRunId(specPath);
|
|
11
13
|
console.log(`Run ID: ${runId}`);
|
|
12
14
|
const store = new RunStore();
|
|
13
15
|
await store.createRun(runId, specPath);
|
|
14
|
-
await runPlanner(specPath, store, runId);
|
|
16
|
+
await runPlanner(specPath, store, runId, orcaConfig);
|
|
15
17
|
const run = await store.getRun(runId);
|
|
16
18
|
if (!run) {
|
|
17
19
|
throw new Error(`Run not found after planning: ${runId}`);
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { createCodexSession } from "../../agents/codex/session.js";
|
|
7
|
+
import { ensureCodexMultiAgent } from "../../core/codex-config.js";
|
|
7
8
|
import { resolveConfig } from "../../core/config-loader.js";
|
|
8
9
|
import { runPlanner } from "../../core/planner.js";
|
|
9
10
|
import { runTaskRunner } from "../../core/task-runner.js";
|
|
@@ -79,7 +80,7 @@ export async function runCommandHandler(options) {
|
|
|
79
80
|
console.log(`Run ID: ${runId}`);
|
|
80
81
|
const store = createStore();
|
|
81
82
|
await store.createRun(runId, specPath);
|
|
82
|
-
await runPlanner(specPath, store, runId);
|
|
83
|
+
await runPlanner(specPath, store, runId, orcaConfig);
|
|
83
84
|
await store.updateRun(runId, {
|
|
84
85
|
mode: "run",
|
|
85
86
|
overallStatus: "running"
|
|
@@ -124,6 +125,10 @@ export async function runCommandHandler(options) {
|
|
|
124
125
|
await dispatcher.dispatch(event);
|
|
125
126
|
};
|
|
126
127
|
const cwd = process.cwd();
|
|
128
|
+
const multiAgentResult = await ensureCodexMultiAgent(orcaConfig ?? undefined);
|
|
129
|
+
if (multiAgentResult.action === "created" || multiAgentResult.action === "appended") {
|
|
130
|
+
console.log(`Multi-agent: enabled (updated ${multiAgentResult.path})`);
|
|
131
|
+
}
|
|
127
132
|
const codexSession = await createCodexSession(cwd, orcaConfig ?? undefined);
|
|
128
133
|
try {
|
|
129
134
|
// Phase 4: Codex consults the task graph before execution begins.
|
|
@@ -150,7 +155,7 @@ export async function runCommandHandler(options) {
|
|
|
150
155
|
store,
|
|
151
156
|
...(orcaConfig ? { config: orcaConfig } : {}),
|
|
152
157
|
emitHook,
|
|
153
|
-
executeTask: (task, runId, _config) => codexSession.executeTask(task, runId),
|
|
158
|
+
executeTask: (task, runId, _config, systemContext) => codexSession.executeTask(task, runId, systemContext),
|
|
154
159
|
});
|
|
155
160
|
const reviewText = await codexSession.reviewChanges();
|
|
156
161
|
console.log("Codex post-execution review:");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const CODEX_HOME = path.join(os.homedir(), ".codex");
|
|
6
|
+
const GLOBAL_CONFIG_FILE = path.join(CODEX_HOME, "config.toml");
|
|
7
|
+
const ORCA_MULTI_AGENT_BLOCK = `# Added by orca — remove or set multi_agent = false to disable
|
|
8
|
+
[features]
|
|
9
|
+
multi_agent = true
|
|
10
|
+
`;
|
|
11
|
+
function isMultiAgentEnabled(config) {
|
|
12
|
+
// Default: off. Only enable if explicitly set to true.
|
|
13
|
+
return config?.codex?.multiAgent === true;
|
|
14
|
+
}
|
|
15
|
+
function containsMultiAgentSetting(content) {
|
|
16
|
+
return /multi_agent\s*=/.test(content);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Ensures `~/.codex/config.toml` has `multi_agent = true` set.
|
|
20
|
+
*
|
|
21
|
+
* Uses the global config (not project-scoped) to avoid the "trusted projects"
|
|
22
|
+
* restriction that prevents project-level config from being loaded headlessly.
|
|
23
|
+
*
|
|
24
|
+
* - If the file doesn't exist: creates it with the multi_agent block.
|
|
25
|
+
* - If the file exists and already has `multi_agent`: leaves it alone.
|
|
26
|
+
* - If the file exists but has no `multi_agent`: appends the feature block.
|
|
27
|
+
* - If `multiAgent` is false in orca config: skips entirely.
|
|
28
|
+
*
|
|
29
|
+
* @param config - Orca config (checks codex.multiAgent flag)
|
|
30
|
+
* @param _configFile - Override config file path (for testing only)
|
|
31
|
+
*/
|
|
32
|
+
export async function ensureCodexMultiAgent(config, _configFile) {
|
|
33
|
+
const configFile = _configFile ?? GLOBAL_CONFIG_FILE;
|
|
34
|
+
if (!isMultiAgentEnabled(config)) {
|
|
35
|
+
return { action: "skipped", path: configFile };
|
|
36
|
+
}
|
|
37
|
+
let existingContent = null;
|
|
38
|
+
try {
|
|
39
|
+
await access(configFile, fsConstants.R_OK);
|
|
40
|
+
existingContent = await readFile(configFile, "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err.code !== "ENOENT") {
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (existingContent === null) {
|
|
48
|
+
await mkdir(path.dirname(configFile), { recursive: true });
|
|
49
|
+
await writeFile(configFile, ORCA_MULTI_AGENT_BLOCK, "utf8");
|
|
50
|
+
return { action: "created", path: configFile };
|
|
51
|
+
}
|
|
52
|
+
if (containsMultiAgentSetting(existingContent)) {
|
|
53
|
+
return { action: "already-set", path: configFile };
|
|
54
|
+
}
|
|
55
|
+
const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
|
|
56
|
+
await writeFile(configFile, `${existingContent}${separator}${ORCA_MULTI_AGENT_BLOCK}`, "utf8");
|
|
57
|
+
return { action: "appended", path: configFile };
|
|
58
|
+
}
|
|
@@ -19,6 +19,16 @@ function coerceConfig(candidate) {
|
|
|
19
19
|
if (!isObject(candidate)) {
|
|
20
20
|
throw new Error("Config module must export an object");
|
|
21
21
|
}
|
|
22
|
+
if ("skills" in candidate && candidate.skills !== undefined) {
|
|
23
|
+
if (!Array.isArray(candidate.skills)) {
|
|
24
|
+
throw new Error(`Config.skills must be an array, got ${describeType(candidate.skills)}`);
|
|
25
|
+
}
|
|
26
|
+
for (const skillPath of candidate.skills) {
|
|
27
|
+
if (typeof skillPath !== "string") {
|
|
28
|
+
throw new Error(`Config.skills entries must be strings, got ${describeType(skillPath)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
22
32
|
if ("hooks" in candidate && candidate.hooks !== undefined) {
|
|
23
33
|
if (!isObject(candidate.hooks)) {
|
|
24
34
|
throw new Error(`Config.hooks must be an object, got ${describeType(candidate.hooks)}`);
|
|
@@ -80,6 +90,9 @@ export function mergeConfigs(...configs) {
|
|
|
80
90
|
if (merged.hookCommands !== undefined || config.hookCommands !== undefined) {
|
|
81
91
|
merged.hookCommands = { ...merged.hookCommands, ...config.hookCommands };
|
|
82
92
|
}
|
|
93
|
+
if (config.skills !== undefined) {
|
|
94
|
+
merged.skills = [...new Set([...(merged.skills ?? []), ...config.skills])];
|
|
95
|
+
}
|
|
83
96
|
}
|
|
84
97
|
return merged;
|
|
85
98
|
}
|
package/dist/core/planner.js
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { planSpec } from "../agents/claude/session.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
|
+
import { loadSkills } from "../utils/skill-loader.js";
|
|
4
5
|
import { validateDAG } from "./dependency-graph.js";
|
|
5
6
|
const DEFAULT_SYSTEM_CONTEXT = "You are Orca planner.";
|
|
6
7
|
let planSpecImpl = planSpec;
|
|
7
8
|
export function setPlanSpecForTests(fn) {
|
|
8
9
|
planSpecImpl = fn ?? planSpec;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
function formatSkillsSection(skills) {
|
|
12
|
+
const formattedSkills = skills.map((skill) => [
|
|
13
|
+
`### ${skill.name}`,
|
|
14
|
+
"",
|
|
15
|
+
`Description: ${skill.description}`,
|
|
16
|
+
"",
|
|
17
|
+
skill.body
|
|
18
|
+
].join("\n"));
|
|
19
|
+
return ["## Available Skills", "", ...formattedSkills].join("\n");
|
|
20
|
+
}
|
|
21
|
+
export async function runPlanner(specPath, store, runId, config) {
|
|
11
22
|
const spec = await fs.readFile(specPath, "utf8");
|
|
12
|
-
const
|
|
23
|
+
const skills = await loadSkills(config);
|
|
24
|
+
const systemContext = skills.length === 0
|
|
25
|
+
? DEFAULT_SYSTEM_CONTEXT
|
|
26
|
+
: `${DEFAULT_SYSTEM_CONTEXT}\n\n${formatSkillsSection(skills)}`;
|
|
27
|
+
const result = await planSpecImpl(spec, systemContext);
|
|
13
28
|
validateDAG(result.tasks);
|
|
14
29
|
await store.writeTasks(runId, result.tasks);
|
|
15
30
|
await store.updateRun(runId, {
|
package/dist/core/task-runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import { executeTask } from "../agents/claude/session.js";
|
|
4
|
+
import { loadSkills } from "../utils/skill-loader.js";
|
|
4
5
|
import { getRunnable, validateDAG } from "./dependency-graph.js";
|
|
5
6
|
import { shouldRetry } from "./retry-policy.js";
|
|
6
7
|
let executeTaskImpl = executeTask;
|
|
@@ -40,6 +41,16 @@ function stripOptionalFields(task, fields) {
|
|
|
40
41
|
function hasPendingTasks(tasks) {
|
|
41
42
|
return tasks.some((task) => task.status === "pending" || task.status === "in_progress");
|
|
42
43
|
}
|
|
44
|
+
function formatSkillsSection(skills) {
|
|
45
|
+
const formattedSkills = skills.map((skill) => [
|
|
46
|
+
`### ${skill.name}`,
|
|
47
|
+
"",
|
|
48
|
+
`Description: ${skill.description}`,
|
|
49
|
+
"",
|
|
50
|
+
skill.body
|
|
51
|
+
].join("\n"));
|
|
52
|
+
return ["## Available Skills", "", ...formattedSkills].join("\n");
|
|
53
|
+
}
|
|
43
54
|
function buildSessionSummary(run) {
|
|
44
55
|
const taskRows = run.tasks.length === 0
|
|
45
56
|
? "| (none) | - | - | - | - |\n"
|
|
@@ -85,6 +96,8 @@ export async function runTaskRunner(options) {
|
|
|
85
96
|
const emitHook = options.emitHook ?? defaultEmitHook;
|
|
86
97
|
const executeTaskFn = options.executeTask ?? executeTaskImpl;
|
|
87
98
|
const { runId, store, config } = options;
|
|
99
|
+
const skills = await loadSkills(config);
|
|
100
|
+
const taskSystemContext = skills.length === 0 ? undefined : formatSkillsSection(skills);
|
|
88
101
|
let run = await store.getRun(runId);
|
|
89
102
|
if (!run) {
|
|
90
103
|
throw new Error(`Run not found: ${runId}`);
|
|
@@ -199,7 +212,7 @@ export async function runTaskRunner(options) {
|
|
|
199
212
|
tasks: inProgressTasks
|
|
200
213
|
});
|
|
201
214
|
try {
|
|
202
|
-
const result = await executeTaskFn(task, runId, config);
|
|
215
|
+
const result = await executeTaskFn(task, runId, config, taskSystemContext);
|
|
203
216
|
if (result.outcome === "done") {
|
|
204
217
|
const doneTasks = inProgressTasks.map((candidate) => {
|
|
205
218
|
if (candidate.id !== task.id) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function expandHome(inputPath) {
|
|
5
|
+
if (inputPath === "~") {
|
|
6
|
+
return os.homedir();
|
|
7
|
+
}
|
|
8
|
+
if (inputPath.startsWith("~/") || inputPath.startsWith("~\\")) {
|
|
9
|
+
return path.join(os.homedir(), inputPath.slice(2));
|
|
10
|
+
}
|
|
11
|
+
return inputPath;
|
|
12
|
+
}
|
|
13
|
+
function parseFrontmatter(frontmatter) {
|
|
14
|
+
const parsed = {};
|
|
15
|
+
for (const rawLine of frontmatter.split(/\r?\n/u)) {
|
|
16
|
+
const line = rawLine.trim();
|
|
17
|
+
if (line === "" || line.startsWith("#")) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const separatorIndex = line.indexOf(":");
|
|
21
|
+
if (separatorIndex <= 0) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
25
|
+
if (key === "") {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let value = line.slice(separatorIndex + 1).trim();
|
|
29
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
30
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
31
|
+
value = value.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
parsed[key] = value;
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
export function parseSkillFile(fileContent) {
|
|
38
|
+
const frontmatterMatch = fileContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u);
|
|
39
|
+
if (!frontmatterMatch) {
|
|
40
|
+
return {
|
|
41
|
+
name: "",
|
|
42
|
+
description: "",
|
|
43
|
+
body: fileContent
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const frontmatter = parseFrontmatter(frontmatterMatch[1] ?? "");
|
|
47
|
+
const body = fileContent.slice(frontmatterMatch[0].length);
|
|
48
|
+
return {
|
|
49
|
+
name: frontmatter.name ?? "",
|
|
50
|
+
description: frontmatter.description ?? "",
|
|
51
|
+
body
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function loadSkill(skillDirPath) {
|
|
55
|
+
const expandedDirPath = path.resolve(expandHome(skillDirPath));
|
|
56
|
+
const skillFilePath = path.join(expandedDirPath, "SKILL.md");
|
|
57
|
+
let skillFileContent;
|
|
58
|
+
try {
|
|
59
|
+
skillFileContent = await readFile(skillFilePath, "utf8");
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code === "ENOENT") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const parsed = parseSkillFile(skillFileContent);
|
|
68
|
+
const inferredName = path.basename(expandedDirPath);
|
|
69
|
+
return {
|
|
70
|
+
name: parsed.name || inferredName,
|
|
71
|
+
description: parsed.description,
|
|
72
|
+
body: parsed.body,
|
|
73
|
+
dirPath: expandedDirPath,
|
|
74
|
+
filePath: skillFilePath
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function loadSkillsFromDir(skillsDirPath) {
|
|
78
|
+
const expandedDirPath = path.resolve(expandHome(skillsDirPath));
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = await readdir(expandedDirPath, { withFileTypes: true, encoding: "utf8" });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code === "ENOENT") {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const subdirectories = entries
|
|
90
|
+
.filter((entry) => entry.isDirectory())
|
|
91
|
+
.map((entry) => entry.name)
|
|
92
|
+
.sort((a, b) => a.localeCompare(b));
|
|
93
|
+
const loaded = await Promise.all(subdirectories.map((subdirName) => loadSkill(path.join(expandedDirPath, subdirName))));
|
|
94
|
+
return loaded.filter((skill) => skill !== null);
|
|
95
|
+
}
|
|
96
|
+
export async function loadSkills(config) {
|
|
97
|
+
const fromConfig = await Promise.all((config?.skills ?? []).map((skillPath) => loadSkill(skillPath)));
|
|
98
|
+
const projectSkills = await loadSkillsFromDir(path.join(process.cwd(), ".orca", "skills"));
|
|
99
|
+
const globalSkills = await loadSkillsFromDir(path.join(os.homedir(), ".orca", "skills"));
|
|
100
|
+
const allSkills = [
|
|
101
|
+
...fromConfig.filter((skill) => skill !== null),
|
|
102
|
+
...projectSkills,
|
|
103
|
+
...globalSkills
|
|
104
|
+
];
|
|
105
|
+
const seenNames = new Set();
|
|
106
|
+
const deduped = [];
|
|
107
|
+
for (const skill of allSkills) {
|
|
108
|
+
if (seenNames.has(skill.name)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
seenNames.add(skill.name);
|
|
112
|
+
deduped.push(skill);
|
|
113
|
+
}
|
|
114
|
+
return deduped;
|
|
115
|
+
}
|