opencode-agenthub 0.1.0
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/LICENSE +21 -0
- package/README.md +373 -0
- package/dist/composer/bootstrap.js +493 -0
- package/dist/composer/builtin-assets.js +139 -0
- package/dist/composer/capabilities.js +20 -0
- package/dist/composer/compose.js +824 -0
- package/dist/composer/defaults.js +10 -0
- package/dist/composer/home-transfer.js +288 -0
- package/dist/composer/install-home.js +5 -0
- package/dist/composer/library/README.md +93 -0
- package/dist/composer/library/bundles/auto.json +18 -0
- package/dist/composer/library/bundles/build.json +17 -0
- package/dist/composer/library/bundles/hr-adapter.json +26 -0
- package/dist/composer/library/bundles/hr-cto.json +24 -0
- package/dist/composer/library/bundles/hr-evaluator.json +26 -0
- package/dist/composer/library/bundles/hr-planner.json +26 -0
- package/dist/composer/library/bundles/hr-sourcer.json +24 -0
- package/dist/composer/library/bundles/hr-verifier.json +26 -0
- package/dist/composer/library/bundles/hr.json +35 -0
- package/dist/composer/library/bundles/plan.json +19 -0
- package/dist/composer/library/instructions/hr-boundaries.md +38 -0
- package/dist/composer/library/instructions/hr-protocol.md +102 -0
- package/dist/composer/library/profiles/auto.json +9 -0
- package/dist/composer/library/profiles/hr.json +9 -0
- package/dist/composer/library/souls/auto.md +29 -0
- package/dist/composer/library/souls/build.md +21 -0
- package/dist/composer/library/souls/hr-adapter.md +64 -0
- package/dist/composer/library/souls/hr-cto.md +57 -0
- package/dist/composer/library/souls/hr-evaluator.md +64 -0
- package/dist/composer/library/souls/hr-planner.md +48 -0
- package/dist/composer/library/souls/hr-sourcer.md +70 -0
- package/dist/composer/library/souls/hr-verifier.md +62 -0
- package/dist/composer/library/souls/hr.md +186 -0
- package/dist/composer/library/souls/plan.md +23 -0
- package/dist/composer/library/workflow/auto-mode.json +139 -0
- package/dist/composer/model-utils.js +39 -0
- package/dist/composer/opencode-profile.js +2299 -0
- package/dist/composer/package-manager.js +75 -0
- package/dist/composer/package-version.js +20 -0
- package/dist/composer/platform.js +48 -0
- package/dist/composer/query.js +133 -0
- package/dist/composer/settings.js +400 -0
- package/dist/plugins/opencode-agenthub.js +310 -0
- package/dist/plugins/opencode-question.js +223 -0
- package/dist/plugins/plan-guidance.js +263 -0
- package/dist/plugins/runtime-config.js +57 -0
- package/dist/skills/agenthub-doctor/SKILL.md +238 -0
- package/dist/skills/agenthub-doctor/diagnose.js +213 -0
- package/dist/skills/agenthub-doctor/fix.js +293 -0
- package/dist/skills/agenthub-doctor/index.js +30 -0
- package/dist/skills/agenthub-doctor/interactive.js +756 -0
- package/dist/skills/hr-assembly/SKILL.md +121 -0
- package/dist/skills/hr-final-check/SKILL.md +98 -0
- package/dist/skills/hr-review/SKILL.md +100 -0
- package/dist/skills/hr-staffing/SKILL.md +85 -0
- package/dist/skills/hr-support/bin/sync_sources.py +560 -0
- package/dist/skills/hr-support/bin/validate_staged_package.py +290 -0
- package/dist/skills/hr-support/bin/vendor_stage_mcps.py +234 -0
- package/dist/skills/hr-support/bin/vendor_stage_skills.py +104 -0
- package/dist/types.js +11 -0
- package/package.json +54 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { spawnOptions } from "./platform.js";
|
|
6
|
+
const execFile = promisify(execFileCallback);
|
|
7
|
+
const pathExists = async (target) => {
|
|
8
|
+
try {
|
|
9
|
+
await access(target);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const commandAvailable = async (command) => {
|
|
16
|
+
try {
|
|
17
|
+
await execFile(command, ["--version"], spawnOptions());
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const detectPackageManagerForRoot = async (targetRoot) => {
|
|
24
|
+
const packageJsonPath = path.join(targetRoot, "package.json");
|
|
25
|
+
if (await pathExists(packageJsonPath)) {
|
|
26
|
+
const pkg = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
|
27
|
+
const packageManager = pkg.packageManager?.split("@")[0];
|
|
28
|
+
if (packageManager) {
|
|
29
|
+
if (packageManager === "npm" || packageManager === "bun") {
|
|
30
|
+
if (await commandAvailable(packageManager)) return packageManager;
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Package manager '${packageManager}' is declared in ${packageJsonPath} but is not available on PATH.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Unsupported package manager '${packageManager}' declared in ${packageJsonPath}. Supported package managers: npm, bun.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (await pathExists(path.join(targetRoot, "pnpm-lock.yaml"))) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Detected pnpm-lock.yaml in ${targetRoot}, but pnpm is not supported for MCP dependency installation.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (await pathExists(path.join(targetRoot, "yarn.lock"))) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Detected yarn.lock in ${targetRoot}, but yarn is not supported for MCP dependency installation.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (await pathExists(path.join(targetRoot, "bun.lock"))) {
|
|
51
|
+
if (await commandAvailable("bun")) return "bun";
|
|
52
|
+
throw new Error(`Detected bun.lock in ${targetRoot} but 'bun' is not available on PATH.`);
|
|
53
|
+
}
|
|
54
|
+
if (await pathExists(path.join(targetRoot, "package-lock.json"))) {
|
|
55
|
+
if (await commandAvailable("npm")) return "npm";
|
|
56
|
+
throw new Error(`Detected package-lock.json in ${targetRoot} but 'npm' is not available on PATH.`);
|
|
57
|
+
}
|
|
58
|
+
if (await commandAvailable("npm")) return "npm";
|
|
59
|
+
throw new Error(
|
|
60
|
+
`No supported package manager found for ${targetRoot}. Tried packageManager field, lockfiles, and npm fallback.`
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
const installPackageDependencies = async (targetRoot) => {
|
|
64
|
+
const packageManager = await detectPackageManagerForRoot(targetRoot);
|
|
65
|
+
if (packageManager === "bun") {
|
|
66
|
+
await execFile("bun", ["install"], { cwd: targetRoot, ...spawnOptions() });
|
|
67
|
+
return packageManager;
|
|
68
|
+
}
|
|
69
|
+
await execFile("npm", ["install"], { cwd: targetRoot, ...spawnOptions() });
|
|
70
|
+
return packageManager;
|
|
71
|
+
};
|
|
72
|
+
export {
|
|
73
|
+
detectPackageManagerForRoot,
|
|
74
|
+
installPackageDependencies
|
|
75
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const readPackageVersion = () => {
|
|
5
|
+
try {
|
|
6
|
+
const pkgPath = path.join(
|
|
7
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
"..",
|
|
9
|
+
"..",
|
|
10
|
+
"package.json"
|
|
11
|
+
);
|
|
12
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
13
|
+
if (pkg.version) return pkg.version;
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
return process.env.npm_package_version ?? "0.0.0";
|
|
17
|
+
};
|
|
18
|
+
export {
|
|
19
|
+
readPackageVersion
|
|
20
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const detectWindows = () => process.platform === "win32";
|
|
2
|
+
const isWindows = (win) => win ?? detectWindows();
|
|
3
|
+
const symlinkType = (win) => isWindows(win) ? "junction" : "dir";
|
|
4
|
+
const shouldChmod = (win) => !isWindows(win);
|
|
5
|
+
const shouldOfferEnvrc = (win) => !isWindows(win);
|
|
6
|
+
const resolvePythonCommand = (win) => isWindows(win) ? "python" : "python3";
|
|
7
|
+
const spawnOptions = (win) => isWindows(win) ? { shell: true } : {};
|
|
8
|
+
const generateRunScript = () => `#!/usr/bin/env bash
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
export XDG_CONFIG_HOME="$SCRIPT_DIR/xdg"
|
|
13
|
+
export OPENCODE_DISABLE_PROJECT_CONFIG=true
|
|
14
|
+
export OPENCODE_CONFIG_DIR="$SCRIPT_DIR"
|
|
15
|
+
|
|
16
|
+
exec opencode "$@"
|
|
17
|
+
`;
|
|
18
|
+
const generateRunCmd = () => `@echo off
|
|
19
|
+
set "SCRIPT_DIR=%~dp0"
|
|
20
|
+
set "XDG_CONFIG_HOME=%SCRIPT_DIR%xdg"
|
|
21
|
+
set "OPENCODE_DISABLE_PROJECT_CONFIG=true"
|
|
22
|
+
set "OPENCODE_CONFIG_DIR=%SCRIPT_DIR%"
|
|
23
|
+
|
|
24
|
+
opencode %*
|
|
25
|
+
`;
|
|
26
|
+
const windowsStartupNotice = (win = detectWindows()) => {
|
|
27
|
+
if (!isWindows(win)) return null;
|
|
28
|
+
return [
|
|
29
|
+
"[agenthub] Notice: native Windows detected.",
|
|
30
|
+
"Windows users should use WSL 2 for the best experience; native Windows remains best-effort in alpha.",
|
|
31
|
+
"Install WSL: https://learn.microsoft.com/en-us/windows/wsl/install"
|
|
32
|
+
].join("\n");
|
|
33
|
+
};
|
|
34
|
+
const resolveHomeConfigRoot = (homeDir, appName, win) => isWindows(win) ? `${homeDir}\\.config\\${appName}` : `${homeDir}/.config/${appName}`;
|
|
35
|
+
const displayHomeConfigPath = (appName, subpaths = [], win) => isWindows(win) ? ["%USERPROFILE%", ".config", appName, ...subpaths].join("\\") : ["~", ".config", appName, ...subpaths].join("/");
|
|
36
|
+
export {
|
|
37
|
+
displayHomeConfigPath,
|
|
38
|
+
generateRunCmd,
|
|
39
|
+
generateRunScript,
|
|
40
|
+
isWindows,
|
|
41
|
+
resolveHomeConfigRoot,
|
|
42
|
+
resolvePythonCommand,
|
|
43
|
+
shouldChmod,
|
|
44
|
+
shouldOfferEnvrc,
|
|
45
|
+
spawnOptions,
|
|
46
|
+
symlinkType,
|
|
47
|
+
windowsStartupNotice
|
|
48
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { access, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const libraryRoot = path.join(currentDir, "library");
|
|
6
|
+
const pathExists = async (p) => {
|
|
7
|
+
try {
|
|
8
|
+
await access(p);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const readJsonIfExists = async (filePath) => {
|
|
15
|
+
try {
|
|
16
|
+
const content = await readFile(filePath, "utf-8");
|
|
17
|
+
return JSON.parse(content);
|
|
18
|
+
} catch {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const listSouls = async (targetRoot) => {
|
|
23
|
+
const dir = path.join(targetRoot, "souls");
|
|
24
|
+
if (!await pathExists(dir)) return [];
|
|
25
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
26
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, "")).sort();
|
|
27
|
+
};
|
|
28
|
+
const listSkills = async (targetRoot) => {
|
|
29
|
+
const dir = path.join(targetRoot, "skills");
|
|
30
|
+
if (!await pathExists(dir)) return [];
|
|
31
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
32
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
33
|
+
};
|
|
34
|
+
const listBundles = async (targetRoot) => {
|
|
35
|
+
const dir = path.join(targetRoot, "bundles");
|
|
36
|
+
if (!await pathExists(dir)) return [];
|
|
37
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
38
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name.replace(/\.json$/, "")).sort();
|
|
39
|
+
};
|
|
40
|
+
const listProfiles = async (targetRoot) => {
|
|
41
|
+
const dir = path.join(targetRoot, "profiles");
|
|
42
|
+
if (!await pathExists(dir)) return [];
|
|
43
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
44
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name.replace(/\.json$/, "")).sort();
|
|
45
|
+
};
|
|
46
|
+
const listInstructions = async (targetRoot) => {
|
|
47
|
+
const dir = path.join(targetRoot, "instructions");
|
|
48
|
+
if (!await pathExists(dir)) return [];
|
|
49
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
50
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, "")).sort();
|
|
51
|
+
};
|
|
52
|
+
const readLibraryNames = async (subdir, ext) => {
|
|
53
|
+
const dir = path.join(libraryRoot, subdir);
|
|
54
|
+
try {
|
|
55
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
56
|
+
return new Set(
|
|
57
|
+
entries.filter((e) => e.isFile() && e.name.endsWith(ext)).map((e) => e.name.slice(0, -ext.length))
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
return /* @__PURE__ */ new Set();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const readLibrarySkillNames = async () => {
|
|
64
|
+
return /* @__PURE__ */ new Set([
|
|
65
|
+
"hr-staffing",
|
|
66
|
+
"hr-review",
|
|
67
|
+
"hr-assembly",
|
|
68
|
+
"hr-final-check"
|
|
69
|
+
]);
|
|
70
|
+
};
|
|
71
|
+
const labelSouls = async (targetRoot) => {
|
|
72
|
+
const [names, builtIns] = await Promise.all([
|
|
73
|
+
listSouls(targetRoot),
|
|
74
|
+
readLibraryNames("souls", ".md")
|
|
75
|
+
]);
|
|
76
|
+
return names.map((name) => ({
|
|
77
|
+
name,
|
|
78
|
+
source: builtIns.has(name) ? "built-in" : "custom"
|
|
79
|
+
}));
|
|
80
|
+
};
|
|
81
|
+
const labelBundles = async (targetRoot) => {
|
|
82
|
+
const [names, builtIns] = await Promise.all([
|
|
83
|
+
listBundles(targetRoot),
|
|
84
|
+
readLibraryNames("bundles", ".json")
|
|
85
|
+
]);
|
|
86
|
+
return names.map((name) => ({
|
|
87
|
+
name,
|
|
88
|
+
source: builtIns.has(name) ? "built-in" : "custom"
|
|
89
|
+
}));
|
|
90
|
+
};
|
|
91
|
+
const labelProfiles = async (targetRoot) => {
|
|
92
|
+
const [names, builtIns] = await Promise.all([
|
|
93
|
+
listProfiles(targetRoot),
|
|
94
|
+
readLibraryNames("profiles", ".json")
|
|
95
|
+
]);
|
|
96
|
+
return names.map((name) => ({
|
|
97
|
+
name,
|
|
98
|
+
source: builtIns.has(name) ? "built-in" : "custom"
|
|
99
|
+
}));
|
|
100
|
+
};
|
|
101
|
+
const labelSkills = async (targetRoot) => {
|
|
102
|
+
const [names, builtIns] = await Promise.all([
|
|
103
|
+
listSkills(targetRoot),
|
|
104
|
+
readLibrarySkillNames()
|
|
105
|
+
]);
|
|
106
|
+
return names.map((name) => ({
|
|
107
|
+
name,
|
|
108
|
+
source: builtIns.has(name) ? "built-in" : "custom"
|
|
109
|
+
}));
|
|
110
|
+
};
|
|
111
|
+
const labelInstructions = async (targetRoot) => {
|
|
112
|
+
const [names, builtIns] = await Promise.all([
|
|
113
|
+
listInstructions(targetRoot),
|
|
114
|
+
readLibraryNames("instructions", ".md")
|
|
115
|
+
]);
|
|
116
|
+
return names.map((name) => ({
|
|
117
|
+
name,
|
|
118
|
+
source: builtIns.has(name) ? "built-in" : "custom"
|
|
119
|
+
}));
|
|
120
|
+
};
|
|
121
|
+
export {
|
|
122
|
+
labelBundles,
|
|
123
|
+
labelInstructions,
|
|
124
|
+
labelProfiles,
|
|
125
|
+
labelSkills,
|
|
126
|
+
labelSouls,
|
|
127
|
+
listBundles,
|
|
128
|
+
listInstructions,
|
|
129
|
+
listProfiles,
|
|
130
|
+
listSkills,
|
|
131
|
+
listSouls,
|
|
132
|
+
readJsonIfExists
|
|
133
|
+
};
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalizeModelSelection, pickModelSelection } from "./model-utils.js";
|
|
5
|
+
import { buildBuiltinVersionManifest } from "./builtin-assets.js";
|
|
6
|
+
import { getDefaultProfilePlugins } from "./defaults.js";
|
|
7
|
+
import { readPackageVersion } from "./package-version.js";
|
|
8
|
+
import { resolveHomeConfigRoot } from "./platform.js";
|
|
9
|
+
const hrPrimaryAgentName = "hr";
|
|
10
|
+
const recommendedHrBootstrapModel = "openai/gpt-5.4-mini";
|
|
11
|
+
const recommendedHrBootstrapVariant = "high";
|
|
12
|
+
const hrSubagentNames = [
|
|
13
|
+
"hr-planner",
|
|
14
|
+
"hr-sourcer",
|
|
15
|
+
"hr-evaluator",
|
|
16
|
+
"hr-cto",
|
|
17
|
+
"hr-adapter",
|
|
18
|
+
"hr-verifier"
|
|
19
|
+
];
|
|
20
|
+
const hrAgentNames = [hrPrimaryAgentName, ...hrSubagentNames];
|
|
21
|
+
const readJson = async (filePath) => {
|
|
22
|
+
const content = await readFile(filePath, "utf-8");
|
|
23
|
+
return JSON.parse(content);
|
|
24
|
+
};
|
|
25
|
+
const titleCaseMode = (modeId) => modeId.length > 0 ? `${modeId[0]?.toUpperCase() ?? ""}${modeId.slice(1)}` : modeId;
|
|
26
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
const workflowClassificationTrigger = (label) => ({
|
|
28
|
+
type: "regex",
|
|
29
|
+
value: `(?:^|\\n)\\s*(?:#{1,6}\\s*)?(?:\\*\\*)?Classification(?:\\s*:|:)(?:\\*\\*)?\\s*(?:\\[\\s*${escapeRegex(label)}\\s*\\]|${escapeRegex(label)})(?=[\\s:;,.!?-]|$)`,
|
|
30
|
+
confidence: "high"
|
|
31
|
+
});
|
|
32
|
+
const workflowIDetectTriggers = (label) => [
|
|
33
|
+
{
|
|
34
|
+
type: "regex",
|
|
35
|
+
value: `(?:^|\\n)\\s*I\\s+detect\\s+(?:\\[\\s*${escapeRegex(label)}\\s*\\]|${escapeRegex(label)})(?=[\\s:;,.!?-]|$)`,
|
|
36
|
+
confidence: "medium"
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
const joinWorkflowLines = (...groups) => groups.flatMap((group) => group ?? []).map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
40
|
+
const hasRuleArray = (config) => Array.isArray(config.rules);
|
|
41
|
+
const normalizeWorkflowMode = (modeId, mode, defaults) => {
|
|
42
|
+
const label = mode.label?.trim() || titleCaseMode(modeId);
|
|
43
|
+
const useIDetectFallback = defaults?.useIDetectFallback ?? true;
|
|
44
|
+
const triggers = mode.triggers && mode.triggers.length > 0 ? mode.triggers : [
|
|
45
|
+
workflowClassificationTrigger(label),
|
|
46
|
+
...useIDetectFallback ? workflowIDetectTriggers(label) : []
|
|
47
|
+
];
|
|
48
|
+
return {
|
|
49
|
+
id: modeId,
|
|
50
|
+
description: mode.description,
|
|
51
|
+
enabled: mode.enabled,
|
|
52
|
+
match: mode.match ?? defaults?.match ?? "any",
|
|
53
|
+
triggers,
|
|
54
|
+
reminderTemplate: joinWorkflowLines(
|
|
55
|
+
mode.reminderPrefix,
|
|
56
|
+
defaults?.reminderPrefix,
|
|
57
|
+
mode.reminder,
|
|
58
|
+
defaults?.reminderSuffix,
|
|
59
|
+
mode.reminderSuffix
|
|
60
|
+
),
|
|
61
|
+
queueVisibleReminderTemplate: mode.queueVisibleReminderTemplate
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const normalizeWorkflowInjectionConfig = (config) => {
|
|
65
|
+
if (hasRuleArray(config)) {
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
68
|
+
const entries = Object.entries(config.modes ?? {}).filter(
|
|
69
|
+
([modeId]) => modeId.trim().length > 0
|
|
70
|
+
);
|
|
71
|
+
if (entries.length === 0) {
|
|
72
|
+
throw new Error("Workflow injection authoring config must define at least one mode.");
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
enabled: config.enabled,
|
|
76
|
+
bundles: config.bundles,
|
|
77
|
+
debugLog: config.debugLog,
|
|
78
|
+
queueVisibleReminder: config.queueVisibleReminder,
|
|
79
|
+
queueVisibleReminderTemplate: config.queueVisibleReminderTemplate,
|
|
80
|
+
scanLineLimit: config.scanLineLimit,
|
|
81
|
+
scanCharLimit: config.scanCharLimit,
|
|
82
|
+
maxInjectionsPerSession: config.maxInjectionsPerSession,
|
|
83
|
+
rules: entries.map(
|
|
84
|
+
([modeId, mode]) => normalizeWorkflowMode(modeId, mode, config.defaults)
|
|
85
|
+
)
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
const formatNativeConfigError = (filePath, error) => {
|
|
89
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
90
|
+
return [
|
|
91
|
+
`Failed to load native OpenCode config: ${filePath}`,
|
|
92
|
+
`Reason: ${reason}`,
|
|
93
|
+
"What you can do:",
|
|
94
|
+
" 1. Fix the JSON or file permissions in that config file",
|
|
95
|
+
" 2. Run with 'agenthub setup minimal' to keep setup minimal",
|
|
96
|
+
" 3. Or set OPENCODE_AGENTHUB_NATIVE_CONFIG to another valid config"
|
|
97
|
+
].join("\n");
|
|
98
|
+
};
|
|
99
|
+
const settingsPathForRoot = (targetRoot) => path.join(targetRoot, "settings.json");
|
|
100
|
+
const workflowInjectionPathForRoot = (targetRoot, name = "auto-mode") => path.join(targetRoot, "workflow", `${name}.json`);
|
|
101
|
+
const readAgentHubSettings = async (targetRoot) => {
|
|
102
|
+
try {
|
|
103
|
+
return await readJson(settingsPathForRoot(targetRoot));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code === "ENOENT") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const writeAgentHubSettings = async (targetRoot, settings) => {
|
|
112
|
+
await writeFile(
|
|
113
|
+
settingsPathForRoot(targetRoot),
|
|
114
|
+
`${JSON.stringify(settings, null, 2)}
|
|
115
|
+
`,
|
|
116
|
+
"utf-8"
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
const readWorkflowInjectionConfig = async (targetRoot, name = "auto-mode") => {
|
|
120
|
+
try {
|
|
121
|
+
const sourceConfig = await readJson(
|
|
122
|
+
workflowInjectionPathForRoot(targetRoot, name)
|
|
123
|
+
);
|
|
124
|
+
return normalizeWorkflowInjectionConfig(sourceConfig);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error.code === "ENOENT") {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const defaultOpenCodeConfigPath = path.join(
|
|
133
|
+
resolveHomeConfigRoot(os.homedir(), "opencode"),
|
|
134
|
+
"opencode.json"
|
|
135
|
+
);
|
|
136
|
+
const defaultPlanDetectionSettings = () => ({
|
|
137
|
+
enabled: true,
|
|
138
|
+
queueVisibleReminder: true,
|
|
139
|
+
queueVisibleReminderTemplate: "[agenthub] Plan reminder injected for this turn."
|
|
140
|
+
});
|
|
141
|
+
const mergeAgentHubSettingsDefaults = (settings) => ({
|
|
142
|
+
...settings,
|
|
143
|
+
preferences: settings.preferences ? { ...settings.preferences } : void 0,
|
|
144
|
+
meta: {
|
|
145
|
+
...settings.meta,
|
|
146
|
+
builtinVersion: settings.meta?.builtinVersion && Object.keys(settings.meta.builtinVersion).length > 0 ? settings.meta.builtinVersion : void 0
|
|
147
|
+
},
|
|
148
|
+
planDetection: settings.planDetection ? { ...defaultPlanDetectionSettings(), ...settings.planDetection } : defaultPlanDetectionSettings()
|
|
149
|
+
});
|
|
150
|
+
const nativeOpenCodeConfigPath = () => process.env.OPENCODE_AGENTHUB_NATIVE_CONFIG || defaultOpenCodeConfigPath;
|
|
151
|
+
const loadNativeOpenCodeConfig = async () => {
|
|
152
|
+
const filePath = nativeOpenCodeConfigPath();
|
|
153
|
+
try {
|
|
154
|
+
return await readJson(filePath);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error.code === "ENOENT") {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
throw new Error(formatNativeConfigError(filePath, error), {
|
|
160
|
+
cause: error
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const readNativePluginEntries = async () => {
|
|
165
|
+
const config = await loadNativeOpenCodeConfig();
|
|
166
|
+
return Array.isArray(config?.plugin) ? config.plugin.filter(
|
|
167
|
+
(entry) => typeof entry === "string" && entry.trim().length > 0
|
|
168
|
+
) : [];
|
|
169
|
+
};
|
|
170
|
+
const loadNativeOpenCodePreferences = async () => {
|
|
171
|
+
try {
|
|
172
|
+
const config = await loadNativeOpenCodeConfig();
|
|
173
|
+
if (!config) return null;
|
|
174
|
+
const safe = {};
|
|
175
|
+
if (config.provider && typeof config.provider === "object") {
|
|
176
|
+
safe.provider = config.provider;
|
|
177
|
+
}
|
|
178
|
+
if (typeof config.model === "string" && config.model.trim()) {
|
|
179
|
+
safe.model = config.model;
|
|
180
|
+
}
|
|
181
|
+
if (typeof config.small_model === "string" && config.small_model.trim()) {
|
|
182
|
+
safe.small_model = config.small_model;
|
|
183
|
+
}
|
|
184
|
+
return Object.keys(safe).length > 0 ? safe : null;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error.code === "ENOENT") {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const readNativeAgentOverrides = async () => {
|
|
193
|
+
const config = await loadNativeOpenCodeConfig();
|
|
194
|
+
if (!config?.agent || typeof config.agent !== "object") return {};
|
|
195
|
+
return Object.fromEntries(
|
|
196
|
+
Object.entries(config.agent).filter(([name, agent]) => {
|
|
197
|
+
if (!name.trim() || !agent || typeof agent !== "object") return false;
|
|
198
|
+
return typeof agent.model === "string" && agent.model.trim().length > 0 || typeof agent.variant === "string" && agent.variant.trim().length > 0 || agent.permission && typeof agent.permission === "object";
|
|
199
|
+
}).map(([name, agent]) => [
|
|
200
|
+
name,
|
|
201
|
+
{
|
|
202
|
+
...normalizeModelSelection(
|
|
203
|
+
typeof agent.model === "string" ? agent.model : void 0,
|
|
204
|
+
typeof agent.variant === "string" ? agent.variant : void 0
|
|
205
|
+
),
|
|
206
|
+
...agent.permission && typeof agent.permission === "object" ? { permission: agent.permission } : {}
|
|
207
|
+
}
|
|
208
|
+
])
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
const modePrimaryAgentName = (mode) => {
|
|
212
|
+
if (mode === "auto") return "auto";
|
|
213
|
+
if (mode === "hr-office") return "hr";
|
|
214
|
+
return void 0;
|
|
215
|
+
};
|
|
216
|
+
const readInstalledBundleModels = async (targetRoot) => {
|
|
217
|
+
const bundlesDir = path.join(targetRoot, "bundles");
|
|
218
|
+
try {
|
|
219
|
+
const entries = await readdir(bundlesDir, { withFileTypes: true });
|
|
220
|
+
const bundleFiles = entries.filter(
|
|
221
|
+
(entry) => entry.isFile() && path.extname(entry.name) === ".json"
|
|
222
|
+
);
|
|
223
|
+
const specs = await Promise.all(
|
|
224
|
+
bundleFiles.map(
|
|
225
|
+
(entry) => readJson(path.join(bundlesDir, entry.name))
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
return Object.fromEntries(
|
|
229
|
+
specs.filter(
|
|
230
|
+
(spec) => typeof spec.agent?.name === "string" && typeof spec.agent?.model === "string" && spec.agent.name.trim() && spec.agent.model.trim()
|
|
231
|
+
).map((spec) => {
|
|
232
|
+
const agent = spec.agent;
|
|
233
|
+
if (!agent)
|
|
234
|
+
throw new Error(`Missing agent config for bundle ${spec.name}`);
|
|
235
|
+
const modelSelection = normalizeModelSelection(agent.model, agent.variant);
|
|
236
|
+
return [
|
|
237
|
+
agent.name,
|
|
238
|
+
{
|
|
239
|
+
...modelSelection.model ? { model: modelSelection.model } : {},
|
|
240
|
+
...modelSelection.variant ? { variant: modelSelection.variant } : {}
|
|
241
|
+
}
|
|
242
|
+
];
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error.code === "ENOENT") return {};
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const normalizeConfiguredModel = (value) => {
|
|
251
|
+
if (typeof value !== "string") return void 0;
|
|
252
|
+
const trimmed = value.trim();
|
|
253
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
254
|
+
};
|
|
255
|
+
const fallbackInstalledModel = (installedModels, nativeModel) => installedModels[hrPrimaryAgentName]?.model || installedModels.auto?.model || Object.values(installedModels)[0]?.model || nativeModel;
|
|
256
|
+
const resolveHrBootstrapAgentModels = async ({
|
|
257
|
+
targetRoot,
|
|
258
|
+
selection
|
|
259
|
+
}) => {
|
|
260
|
+
const imported = await loadNativeOpenCodePreferences();
|
|
261
|
+
const installedModels = await readInstalledBundleModels(targetRoot);
|
|
262
|
+
const nativeModel = normalizeModelSelection(imported?.model);
|
|
263
|
+
const fallbackModel = recommendedHrBootstrapModel || fallbackInstalledModel(installedModels, nativeModel.model);
|
|
264
|
+
if (!fallbackModel) {
|
|
265
|
+
throw new Error(`Unable to resolve an initial HR model for ${targetRoot}`);
|
|
266
|
+
}
|
|
267
|
+
const requestedStrategy = selection?.subagentStrategy || "auto";
|
|
268
|
+
const effectiveStrategy = requestedStrategy === "auto" ? "recommended" : requestedStrategy === "native" && !nativeModel ? "recommended" : requestedStrategy;
|
|
269
|
+
const explicitConsoleModel = normalizeModelSelection(selection?.consoleModel);
|
|
270
|
+
const explicitSharedModel = normalizeModelSelection(selection?.sharedSubagentModel);
|
|
271
|
+
const recommendedModel = {
|
|
272
|
+
model: recommendedHrBootstrapModel,
|
|
273
|
+
variant: recommendedHrBootstrapVariant
|
|
274
|
+
};
|
|
275
|
+
const fallbackSelection = { model: fallbackModel };
|
|
276
|
+
const sharedAgentModel = pickModelSelection(
|
|
277
|
+
explicitSharedModel,
|
|
278
|
+
explicitConsoleModel,
|
|
279
|
+
effectiveStrategy === "native" ? nativeModel : void 0,
|
|
280
|
+
recommendedModel,
|
|
281
|
+
fallbackSelection
|
|
282
|
+
);
|
|
283
|
+
const consoleModel = pickModelSelection(explicitConsoleModel, sharedAgentModel);
|
|
284
|
+
const subagentModels = Object.fromEntries(
|
|
285
|
+
hrSubagentNames.map((agentName) => [
|
|
286
|
+
agentName,
|
|
287
|
+
effectiveStrategy === "custom" || effectiveStrategy === "free" || effectiveStrategy === "recommended" ? sharedAgentModel : effectiveStrategy === "native" ? pickModelSelection(nativeModel, sharedAgentModel) : sharedAgentModel
|
|
288
|
+
])
|
|
289
|
+
);
|
|
290
|
+
return {
|
|
291
|
+
agentModels: {
|
|
292
|
+
[hrPrimaryAgentName]: consoleModel,
|
|
293
|
+
...subagentModels
|
|
294
|
+
},
|
|
295
|
+
strategy: effectiveStrategy
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
const buildInitialAgentHubSettings = async ({
|
|
299
|
+
targetRoot,
|
|
300
|
+
mode,
|
|
301
|
+
hrResolvedModels
|
|
302
|
+
}) => {
|
|
303
|
+
const shouldImportNativeBasics = mode !== "minimal";
|
|
304
|
+
const shouldImportNativeAgents = mode !== "minimal";
|
|
305
|
+
const imported = shouldImportNativeBasics ? await loadNativeOpenCodePreferences() : null;
|
|
306
|
+
const installedModels = await readInstalledBundleModels(targetRoot);
|
|
307
|
+
const nativeAgentOverrides = shouldImportNativeAgents ? await readNativeAgentOverrides() : {};
|
|
308
|
+
const primaryAgentName = modePrimaryAgentName(mode);
|
|
309
|
+
const fallbackModel = primaryAgentName && installedModels[primaryAgentName]?.model || installedModels.auto?.model || Object.values(installedModels)[0]?.model;
|
|
310
|
+
const sharedModel = imported?.model || fallbackModel;
|
|
311
|
+
const agentOverrides = { ...nativeAgentOverrides };
|
|
312
|
+
if (mode === "hr-office") {
|
|
313
|
+
const resolvedHrModels = hrResolvedModels || await resolveHrBootstrapAgentModels({
|
|
314
|
+
targetRoot
|
|
315
|
+
});
|
|
316
|
+
for (const [agentName, modelSelection] of Object.entries(resolvedHrModels.agentModels)) {
|
|
317
|
+
agentOverrides[agentName] = {
|
|
318
|
+
...agentOverrides[agentName] || {},
|
|
319
|
+
model: modelSelection.model,
|
|
320
|
+
...modelSelection.variant ? { variant: modelSelection.variant } : {}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const guardDefinitions = {
|
|
325
|
+
read_only: {
|
|
326
|
+
description: "Read-only access - no file modifications",
|
|
327
|
+
permission: {
|
|
328
|
+
edit: "deny",
|
|
329
|
+
write: "deny",
|
|
330
|
+
bash: "deny"
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
no_subagent: {
|
|
334
|
+
description: "Legacy alias for no_task",
|
|
335
|
+
blockedTools: ["task"],
|
|
336
|
+
permission: {
|
|
337
|
+
task: { "*": "deny" }
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
no_task: {
|
|
341
|
+
description: "Block task tool",
|
|
342
|
+
blockedTools: ["task"],
|
|
343
|
+
permission: {
|
|
344
|
+
task: { "*": "deny" }
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
no_omo: {
|
|
348
|
+
description: "Block OMO (Oh-My-OpenCode) multi-agent calls - for native agents in OMO profiles",
|
|
349
|
+
blockedTools: ["call_omo_agent"],
|
|
350
|
+
permission: {
|
|
351
|
+
call_omo_agent: "deny"
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const settings = {
|
|
356
|
+
...imported?.provider || imported?.model || imported?.small_model || sharedModel ? {
|
|
357
|
+
opencode: {
|
|
358
|
+
...imported?.provider ? { provider: imported.provider } : {},
|
|
359
|
+
...sharedModel ? { model: sharedModel } : {},
|
|
360
|
+
...imported?.small_model ? { small_model: imported.small_model } : {}
|
|
361
|
+
}
|
|
362
|
+
} : {},
|
|
363
|
+
agents: agentOverrides,
|
|
364
|
+
guards: guardDefinitions,
|
|
365
|
+
planDetection: defaultPlanDetectionSettings(),
|
|
366
|
+
meta: {
|
|
367
|
+
onboarding: {
|
|
368
|
+
mode,
|
|
369
|
+
...mode === "hr-office" && hrResolvedModels ? { modelStrategy: hrResolvedModels.strategy } : {},
|
|
370
|
+
importedNativeBasics: shouldImportNativeBasics,
|
|
371
|
+
importedNativeAgents: shouldImportNativeAgents,
|
|
372
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
373
|
+
},
|
|
374
|
+
builtinVersion: buildBuiltinVersionManifest(mode, readPackageVersion())
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
return settings;
|
|
378
|
+
};
|
|
379
|
+
export {
|
|
380
|
+
buildInitialAgentHubSettings,
|
|
381
|
+
defaultPlanDetectionSettings,
|
|
382
|
+
getDefaultProfilePlugins,
|
|
383
|
+
hrAgentNames,
|
|
384
|
+
hrPrimaryAgentName,
|
|
385
|
+
hrSubagentNames,
|
|
386
|
+
loadNativeOpenCodeConfig,
|
|
387
|
+
loadNativeOpenCodePreferences,
|
|
388
|
+
mergeAgentHubSettingsDefaults,
|
|
389
|
+
nativeOpenCodeConfigPath,
|
|
390
|
+
readAgentHubSettings,
|
|
391
|
+
readNativeAgentOverrides,
|
|
392
|
+
readNativePluginEntries,
|
|
393
|
+
readWorkflowInjectionConfig,
|
|
394
|
+
recommendedHrBootstrapModel,
|
|
395
|
+
recommendedHrBootstrapVariant,
|
|
396
|
+
resolveHrBootstrapAgentModels,
|
|
397
|
+
settingsPathForRoot,
|
|
398
|
+
workflowInjectionPathForRoot,
|
|
399
|
+
writeAgentHubSettings
|
|
400
|
+
};
|