@fro.bot/systematic 2.8.1 → 2.9.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 +26 -0
- package/dist/cli.js +1 -1
- package/dist/{index-k9tdxh0p.js → index-d5ewqz8w.js} +151 -16
- package/dist/index.js +452 -32
- package/dist/lib/agent-overlays.d.ts +44 -0
- package/dist/lib/agents.d.ts +2 -0
- package/dist/lib/config-handler.d.ts +3 -2
- package/dist/lib/config.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -297,6 +297,20 @@ Configuration is loaded from multiple locations and merged (later sources overri
|
|
|
297
297
|
{
|
|
298
298
|
"disabled_skills": ["git-worktree"],
|
|
299
299
|
"disabled_agents": [],
|
|
300
|
+
"categories": {
|
|
301
|
+
"review": {
|
|
302
|
+
"temperature": 0.1,
|
|
303
|
+
"steps": 12
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"agents": {
|
|
307
|
+
"security-sentinel": {
|
|
308
|
+
"variant": "thinking"
|
|
309
|
+
},
|
|
310
|
+
"workflow/systematic-implementer": {
|
|
311
|
+
"steps": 20
|
|
312
|
+
}
|
|
313
|
+
},
|
|
300
314
|
"bootstrap": {
|
|
301
315
|
"enabled": true
|
|
302
316
|
}
|
|
@@ -307,9 +321,21 @@ Configuration is loaded from multiple locations and merged (later sources overri
|
|
|
307
321
|
|--------|------|---------|-------------|
|
|
308
322
|
| `disabled_skills` | `string[]` | `[]` | Skills to exclude from registration |
|
|
309
323
|
| `disabled_agents` | `string[]` | `[]` | Agents to exclude from registration |
|
|
324
|
+
| `categories` | `object` | `{}` | Overlay bundled agents by category (`design`, `docs`, `document-review`, `research`, `review`, `workflow`) |
|
|
325
|
+
| `agents` | `object` | `{}` | Overlay exact bundled agents by unique stem or `<category>/<stem>` key |
|
|
310
326
|
| `bootstrap.enabled` | `boolean` | `true` | Inject the `using-systematic` guide into system prompts |
|
|
311
327
|
| `bootstrap.file` | `string` | — | Custom bootstrap file path (overrides default) |
|
|
312
328
|
|
|
329
|
+
Agent overlays support `model`, `variant`, `temperature`, `top_p`, `permission`, `mode`, `color`, `steps`, `hidden`, exact-agent-only `disable`, and managed `skills`. `color` accepts `#RGB`, `#RRGGBB`, or OpenCode named color tokens matching `[a-zA-Z][a-zA-Z0-9-]*`; whitespace/freeform numeric strings are rejected. `skills` uses bundled skill frontmatter names like `ce:review`; it is a shortcut that writes OpenCode `permission.skill` rules, not a native OpenCode agent field. Because `model` controls provider routing/cost/privacy and `permission`/`skills` control tool access, those fields are only accepted from user config or `$OPENCODE_CONFIG_DIR/systematic.json`. Project config may tune non-sensitive presentation and runtime fields such as `variant`, `temperature`, `top_p`, `mode`, `color`, `steps`, `hidden`, or exact-agent `disable`, but it cannot choose model/provider routing or loosen permission/capability policy.
|
|
330
|
+
|
|
331
|
+
Systematic separates config-source precedence from overlay precedence. Config files merge in this order: user config, project config, then `$OPENCODE_CONFIG_DIR/systematic.json` if set. Higher-priority `agents.<key>` and `categories.<id>` entries replace lower-priority entries wholesale, while unrelated keys survive. Project overlays are the exception for trust-sensitive fields: same-key project overlays preserve user-level `model`, `permission`, and `skills` fields instead of erasing them. After the effective config is built, exact `agents` overlays beat category overlays, which beat built-in policy defaults, bundled markdown defaults, and OpenCode inherited defaults.
|
|
332
|
+
|
|
333
|
+
Bundled agents omit `model` by default so OpenCode model inheritance keeps working. Systematic emits a `model` only when you configure one explicitly in user or custom config; provider-specific zero-config model defaults are intentionally deferred.
|
|
334
|
+
|
|
335
|
+
Native OpenCode agents with the same emitted key are full replacements. An exact Systematic overlay for that key conflicts, while category overlays skip native replacements and continue applying to other bundled agents. Use one canonical agent key form across config sources (`security-sentinel` or `review/security-sentinel`) because alias collisions fail duplicate-target validation.
|
|
336
|
+
|
|
337
|
+
Category IDs are V1 public API because broad policy overlays are a core use case; future agent reorganizations must preserve aliases or provide migration warnings. Category overlays also apply to future bundled agents added to that category. V1 does not include an MCP allowlist shortcut.
|
|
338
|
+
|
|
313
339
|
### Project-Specific Content
|
|
314
340
|
|
|
315
341
|
Add your own skills and agents alongside bundled ones:
|
package/dist/cli.js
CHANGED
|
@@ -807,6 +807,43 @@ var ParseErrorCode;
|
|
|
807
807
|
ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
|
|
808
808
|
ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
|
|
809
809
|
})(ParseErrorCode || (ParseErrorCode = {}));
|
|
810
|
+
function printParseErrorCode(code) {
|
|
811
|
+
switch (code) {
|
|
812
|
+
case 1:
|
|
813
|
+
return "InvalidSymbol";
|
|
814
|
+
case 2:
|
|
815
|
+
return "InvalidNumberFormat";
|
|
816
|
+
case 3:
|
|
817
|
+
return "PropertyNameExpected";
|
|
818
|
+
case 4:
|
|
819
|
+
return "ValueExpected";
|
|
820
|
+
case 5:
|
|
821
|
+
return "ColonExpected";
|
|
822
|
+
case 6:
|
|
823
|
+
return "CommaExpected";
|
|
824
|
+
case 7:
|
|
825
|
+
return "CloseBraceExpected";
|
|
826
|
+
case 8:
|
|
827
|
+
return "CloseBracketExpected";
|
|
828
|
+
case 9:
|
|
829
|
+
return "EndOfFileExpected";
|
|
830
|
+
case 10:
|
|
831
|
+
return "InvalidCommentToken";
|
|
832
|
+
case 11:
|
|
833
|
+
return "UnexpectedEndOfComment";
|
|
834
|
+
case 12:
|
|
835
|
+
return "UnexpectedEndOfString";
|
|
836
|
+
case 13:
|
|
837
|
+
return "UnexpectedEndOfNumber";
|
|
838
|
+
case 14:
|
|
839
|
+
return "InvalidUnicode";
|
|
840
|
+
case 15:
|
|
841
|
+
return "InvalidEscapeCharacter";
|
|
842
|
+
case 16:
|
|
843
|
+
return "InvalidCharacter";
|
|
844
|
+
}
|
|
845
|
+
return "<unknown ParseErrorCode>";
|
|
846
|
+
}
|
|
810
847
|
|
|
811
848
|
// src/lib/config.ts
|
|
812
849
|
var DEFAULT_CONFIG = {
|
|
@@ -815,17 +852,43 @@ var DEFAULT_CONFIG = {
|
|
|
815
852
|
disabled_commands: [],
|
|
816
853
|
bootstrap: {
|
|
817
854
|
enabled: true
|
|
818
|
-
}
|
|
855
|
+
},
|
|
856
|
+
agents: {},
|
|
857
|
+
categories: {}
|
|
819
858
|
};
|
|
859
|
+
var SECURITY_OVERLAY_FIELDS = new Set(["model", "permission", "skills"]);
|
|
860
|
+
function isErrorWithCode(error) {
|
|
861
|
+
return error instanceof Error && "code" in error;
|
|
862
|
+
}
|
|
820
863
|
function loadJsoncFile(filePath) {
|
|
864
|
+
let content;
|
|
821
865
|
try {
|
|
822
|
-
|
|
866
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
867
|
+
} catch (error) {
|
|
868
|
+
if (isErrorWithCode(error) && error.code === "ENOENT")
|
|
823
869
|
return null;
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
870
|
+
throw new Error(`Invalid Systematic config in ${filePath}: unable to read file`, { cause: error });
|
|
871
|
+
}
|
|
872
|
+
const errors = [];
|
|
873
|
+
const parsed = parse2(content, errors);
|
|
874
|
+
if (errors.length > 0) {
|
|
875
|
+
const error = errors[0];
|
|
876
|
+
const message = error ? `${printParseErrorCode(error.error)} at offset ${error.offset}` : "unknown parse error";
|
|
877
|
+
throw new Error(`Invalid Systematic config in ${filePath}: JSONC parse error: ${message}`);
|
|
828
878
|
}
|
|
879
|
+
if (!isRecord(parsed)) {
|
|
880
|
+
throw new Error(`Invalid Systematic config in ${filePath}: root must be an object`);
|
|
881
|
+
}
|
|
882
|
+
return parsed;
|
|
883
|
+
}
|
|
884
|
+
function loadConfigSource(filePath, trust) {
|
|
885
|
+
const config = loadJsoncFile(filePath);
|
|
886
|
+
if (!config)
|
|
887
|
+
return null;
|
|
888
|
+
return { path: filePath, config, trust };
|
|
889
|
+
}
|
|
890
|
+
function isRecord(value) {
|
|
891
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
829
892
|
}
|
|
830
893
|
function mergeArraysUnique(arr1, arr2) {
|
|
831
894
|
const set = new Set;
|
|
@@ -838,10 +901,18 @@ function mergeArraysUnique(arr1, arr2) {
|
|
|
838
901
|
return Array.from(set);
|
|
839
902
|
}
|
|
840
903
|
function loadConfig(projectDir) {
|
|
904
|
+
return loadConfigWithSources(projectDir).config;
|
|
905
|
+
}
|
|
906
|
+
function loadConfigWithSources(projectDir) {
|
|
841
907
|
const paths = getConfigPaths(projectDir);
|
|
842
|
-
const
|
|
843
|
-
const
|
|
844
|
-
const
|
|
908
|
+
const userSource = loadConfigSource(paths.userConfig, "user");
|
|
909
|
+
const projectSource = loadConfigSource(paths.projectConfig, "project");
|
|
910
|
+
const customSource = paths.customConfig ? loadConfigSource(paths.customConfig, "custom") : null;
|
|
911
|
+
const sources = [userSource, projectSource, customSource].filter((source) => source !== null);
|
|
912
|
+
const overlays = mergeOverlaySources(sources);
|
|
913
|
+
const userConfig = userSource?.config;
|
|
914
|
+
const projectConfig = projectSource?.config;
|
|
915
|
+
const customConfig = customSource?.config;
|
|
845
916
|
const result = {
|
|
846
917
|
disabled_skills: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_skills, userConfig?.disabled_skills), projectConfig?.disabled_skills), customConfig?.disabled_skills),
|
|
847
918
|
disabled_agents: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_agents, userConfig?.disabled_agents), projectConfig?.disabled_agents), customConfig?.disabled_agents),
|
|
@@ -851,10 +922,73 @@ function loadConfig(projectDir) {
|
|
|
851
922
|
...userConfig?.bootstrap,
|
|
852
923
|
...projectConfig?.bootstrap,
|
|
853
924
|
...customConfig?.bootstrap
|
|
854
|
-
}
|
|
925
|
+
},
|
|
926
|
+
agents: overlayValues(overlays.agents),
|
|
927
|
+
categories: overlayValues(overlays.categories)
|
|
928
|
+
};
|
|
929
|
+
return { config: result, overlays };
|
|
930
|
+
}
|
|
931
|
+
function mergeOverlaySources(sources) {
|
|
932
|
+
const result = {
|
|
933
|
+
agents: {},
|
|
934
|
+
categories: {}
|
|
855
935
|
};
|
|
936
|
+
for (const source of sources) {
|
|
937
|
+
mergeOverlayMap(result.agents, source, "agents");
|
|
938
|
+
mergeOverlayMap(result.categories, source, "categories");
|
|
939
|
+
}
|
|
940
|
+
return result;
|
|
941
|
+
}
|
|
942
|
+
function mergeOverlayMap(target, source, mapKey) {
|
|
943
|
+
const overlayMap = source.config[mapKey];
|
|
944
|
+
if (overlayMap === undefined)
|
|
945
|
+
return;
|
|
946
|
+
if (!isRecord(overlayMap)) {
|
|
947
|
+
throwInvalidOverlay(source.path, mapKey);
|
|
948
|
+
}
|
|
949
|
+
for (const [key, value] of Object.entries(overlayMap)) {
|
|
950
|
+
const keyPath = `${mapKey}.${key}`;
|
|
951
|
+
if (!isRecord(value)) {
|
|
952
|
+
throwInvalidOverlay(source.path, keyPath);
|
|
953
|
+
}
|
|
954
|
+
if (source.trust === "project") {
|
|
955
|
+
rejectProjectSecurityOverlay(source.path, keyPath, value);
|
|
956
|
+
}
|
|
957
|
+
const previous = target[key];
|
|
958
|
+
const nextValue = source.trust === "project" && previous ? preserveSecurityFields(previous.value, value) : value;
|
|
959
|
+
target[key] = {
|
|
960
|
+
value: nextValue,
|
|
961
|
+
sourcePath: source.path,
|
|
962
|
+
keyPath
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function rejectProjectSecurityOverlay(sourcePath, keyPath, value) {
|
|
967
|
+
for (const field of SECURITY_OVERLAY_FIELDS) {
|
|
968
|
+
if (Object.hasOwn(value, field)) {
|
|
969
|
+
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath}.${field} is only valid in user config or OPENCODE_CONFIG_DIR config`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function preserveSecurityFields(previous, next) {
|
|
974
|
+
const result = { ...next };
|
|
975
|
+
for (const field of SECURITY_OVERLAY_FIELDS) {
|
|
976
|
+
if (Object.hasOwn(previous, field)) {
|
|
977
|
+
result[field] = previous[field];
|
|
978
|
+
}
|
|
979
|
+
}
|
|
856
980
|
return result;
|
|
857
981
|
}
|
|
982
|
+
function overlayValues(overlays) {
|
|
983
|
+
const result = {};
|
|
984
|
+
for (const [key, overlay] of Object.entries(overlays)) {
|
|
985
|
+
result[key] = overlay.value;
|
|
986
|
+
}
|
|
987
|
+
return result;
|
|
988
|
+
}
|
|
989
|
+
function throwInvalidOverlay(sourcePath, keyPath) {
|
|
990
|
+
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} must be an object`);
|
|
991
|
+
}
|
|
858
992
|
function getConfigPaths(projectDir) {
|
|
859
993
|
const homeDir = os.homedir();
|
|
860
994
|
const customConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
@@ -909,14 +1043,14 @@ function formatFrontmatter(data) {
|
|
|
909
1043
|
}
|
|
910
1044
|
|
|
911
1045
|
// src/lib/validation.ts
|
|
912
|
-
function
|
|
1046
|
+
function isRecord2(value) {
|
|
913
1047
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
914
1048
|
}
|
|
915
1049
|
function isPermissionSetting(value) {
|
|
916
1050
|
return value === "ask" || value === "allow" || value === "deny";
|
|
917
1051
|
}
|
|
918
1052
|
function isToolsMap(value) {
|
|
919
|
-
if (!
|
|
1053
|
+
if (!isRecord2(value))
|
|
920
1054
|
return false;
|
|
921
1055
|
return Object.values(value).every((entry) => typeof entry === "boolean");
|
|
922
1056
|
}
|
|
@@ -935,7 +1069,7 @@ function extractBashPermission(data) {
|
|
|
935
1069
|
const bash = data.bash;
|
|
936
1070
|
if (isPermissionSetting(bash))
|
|
937
1071
|
return bash;
|
|
938
|
-
if (
|
|
1072
|
+
if (isRecord2(bash)) {
|
|
939
1073
|
const entries = Object.entries(bash);
|
|
940
1074
|
if (entries.every(([, setting]) => isPermissionSetting(setting))) {
|
|
941
1075
|
return Object.fromEntries(entries);
|
|
@@ -962,7 +1096,7 @@ function buildPermissionObject(edit, bash, webfetch, doom_loop, external_directo
|
|
|
962
1096
|
return Object.keys(permission).length > 0 ? permission : undefined;
|
|
963
1097
|
}
|
|
964
1098
|
function normalizePermission(value) {
|
|
965
|
-
if (!
|
|
1099
|
+
if (!isRecord2(value))
|
|
966
1100
|
return;
|
|
967
1101
|
const bash = extractBashPermission(value);
|
|
968
1102
|
if (bash === null)
|
|
@@ -1071,6 +1205,7 @@ function extractAgentFrontmatter(content) {
|
|
|
1071
1205
|
description: extractString(data, "description"),
|
|
1072
1206
|
prompt: body.trim(),
|
|
1073
1207
|
model: extractNonEmptyString(data, "model"),
|
|
1208
|
+
variant: extractNonEmptyString(data, "variant"),
|
|
1074
1209
|
temperature: extractNumber(data, "temperature"),
|
|
1075
1210
|
top_p: extractNumber(data, "top_p"),
|
|
1076
1211
|
tools: isToolsMap(data.tools) ? data.tools : undefined,
|
|
@@ -1420,7 +1555,7 @@ function extractFrontmatter(filePath) {
|
|
|
1420
1555
|
}
|
|
1421
1556
|
const metadataRaw = data.metadata;
|
|
1422
1557
|
let metadata;
|
|
1423
|
-
if (
|
|
1558
|
+
if (isRecord2(metadataRaw)) {
|
|
1424
1559
|
const entries = Object.entries(metadataRaw);
|
|
1425
1560
|
if (entries.every(([, v]) => typeof v === "string")) {
|
|
1426
1561
|
metadata = Object.fromEntries(entries);
|
|
@@ -1477,4 +1612,4 @@ function findSkillsInDir(dir, maxDepth = 3) {
|
|
|
1477
1612
|
return skills;
|
|
1478
1613
|
}
|
|
1479
1614
|
|
|
1480
|
-
export { parseFrontmatter, loadConfig, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir };
|
|
1615
|
+
export { parseFrontmatter, loadConfig, loadConfigWithSources, getConfigPaths, isRecord2 as isRecord, isPermissionSetting, isAgentMode, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir };
|
package/dist/index.js
CHANGED
|
@@ -6,13 +6,17 @@ import {
|
|
|
6
6
|
findAgentsInDir,
|
|
7
7
|
findCommandsInDir,
|
|
8
8
|
findSkillsInDir,
|
|
9
|
+
isAgentMode,
|
|
10
|
+
isPermissionSetting,
|
|
11
|
+
isRecord,
|
|
9
12
|
loadConfig,
|
|
13
|
+
loadConfigWithSources,
|
|
10
14
|
parseFrontmatter
|
|
11
|
-
} from "./index-
|
|
15
|
+
} from "./index-d5ewqz8w.js";
|
|
12
16
|
|
|
13
17
|
// src/index.ts
|
|
14
|
-
import
|
|
15
|
-
import
|
|
18
|
+
import fs4 from "fs";
|
|
19
|
+
import path5 from "path";
|
|
16
20
|
import { fileURLToPath } from "url";
|
|
17
21
|
|
|
18
22
|
// src/lib/bootstrap.ts
|
|
@@ -72,8 +76,304 @@ ${toolMapping}
|
|
|
72
76
|
</SYSTEMATIC_WORKFLOWS>`;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
// src/lib/
|
|
79
|
+
// src/lib/agent-overlays.ts
|
|
80
|
+
import fs2 from "fs";
|
|
76
81
|
import path2 from "path";
|
|
82
|
+
var ALLOWED_OVERLAY_FIELDS = new Set([
|
|
83
|
+
"model",
|
|
84
|
+
"variant",
|
|
85
|
+
"temperature",
|
|
86
|
+
"top_p",
|
|
87
|
+
"permission",
|
|
88
|
+
"mode",
|
|
89
|
+
"color",
|
|
90
|
+
"steps",
|
|
91
|
+
"hidden",
|
|
92
|
+
"disable",
|
|
93
|
+
"skills"
|
|
94
|
+
]);
|
|
95
|
+
function buildBundledAgentInventory(agentsDir, disabledAgents) {
|
|
96
|
+
const categories = readCategoryDirs(agentsDir);
|
|
97
|
+
const agentsByQualifiedId = {};
|
|
98
|
+
const stemCategories = new Map;
|
|
99
|
+
const disabledSet = new Set(disabledAgents);
|
|
100
|
+
for (const category of categories) {
|
|
101
|
+
for (const fileName of readMarkdownFiles(path2.join(agentsDir, category))) {
|
|
102
|
+
const key = fileName.replace(/\.md$/, "");
|
|
103
|
+
const id = `${category}/${key}`;
|
|
104
|
+
agentsByQualifiedId[id] = {
|
|
105
|
+
id,
|
|
106
|
+
key,
|
|
107
|
+
category,
|
|
108
|
+
file: path2.join(agentsDir, category, fileName),
|
|
109
|
+
disabled: disabledSet.has(key) || disabledSet.has(id)
|
|
110
|
+
};
|
|
111
|
+
const existing = stemCategories.get(key) ?? [];
|
|
112
|
+
existing.push(category);
|
|
113
|
+
stemCategories.set(key, existing);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const duplicateStems = Array.from(stemCategories.entries()).filter(([, seenCategories]) => seenCategories.length > 1);
|
|
117
|
+
if (duplicateStems.length > 0) {
|
|
118
|
+
const details = duplicateStems.map(([stem, seenCategories]) => `Duplicate bundled agent stem "${stem}" in categories: ${seenCategories.join(", ")}`).join("; ");
|
|
119
|
+
throw new Error(details);
|
|
120
|
+
}
|
|
121
|
+
const aliases = {};
|
|
122
|
+
for (const entry of Object.values(agentsByQualifiedId)) {
|
|
123
|
+
aliases[entry.id] = entry.id;
|
|
124
|
+
aliases[entry.key] = entry.id;
|
|
125
|
+
}
|
|
126
|
+
return { agentsByQualifiedId, aliases, categories };
|
|
127
|
+
}
|
|
128
|
+
function validateAgentOverlays({
|
|
129
|
+
inventory,
|
|
130
|
+
overlays,
|
|
131
|
+
nativeAgents = {},
|
|
132
|
+
enabledSkills
|
|
133
|
+
}) {
|
|
134
|
+
const skillSet = enabledSkills ? new Set(enabledSkills) : undefined;
|
|
135
|
+
const agents = validateExactAgentOverlays(inventory, overlays, nativeAgents, skillSet);
|
|
136
|
+
const categories = validateCategoryOverlays(inventory, overlays, skillSet);
|
|
137
|
+
return { agents, categories };
|
|
138
|
+
}
|
|
139
|
+
function resolveAgentOverlaySet(overlays) {
|
|
140
|
+
return {
|
|
141
|
+
agentsByTargetId: new Map(overlays.agents.map((overlay) => [overlay.target.id, overlay])),
|
|
142
|
+
categoriesByKey: new Map(overlays.categories.map((overlay) => [overlay.key, overlay]))
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function inferBuiltInTemperature(name, description) {
|
|
146
|
+
const sample = `${name} ${description ?? ""}`.toLowerCase();
|
|
147
|
+
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
|
148
|
+
return 0.1;
|
|
149
|
+
}
|
|
150
|
+
if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
|
|
151
|
+
return 0.2;
|
|
152
|
+
}
|
|
153
|
+
if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
|
|
154
|
+
return 0.3;
|
|
155
|
+
}
|
|
156
|
+
if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
|
|
157
|
+
return 0.6;
|
|
158
|
+
}
|
|
159
|
+
return 0.3;
|
|
160
|
+
}
|
|
161
|
+
function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
|
|
162
|
+
const result = [];
|
|
163
|
+
const seenTargets = new Map;
|
|
164
|
+
for (const [key, overlay] of Object.entries(overlays.agents)) {
|
|
165
|
+
const targetId = inventory.aliases[key];
|
|
166
|
+
if (!targetId) {
|
|
167
|
+
throwConfigError(overlay.sourcePath, overlay.keyPath, `unknown bundled agent. Valid agents: ${validAgentKeys(inventory).join(", ")}`);
|
|
168
|
+
}
|
|
169
|
+
const previousKeyPath = seenTargets.get(targetId);
|
|
170
|
+
if (previousKeyPath) {
|
|
171
|
+
throwConfigError(overlay.sourcePath, overlay.keyPath, `Duplicate Systematic agent overlay target "${targetId}" from ${previousKeyPath} and ${overlay.keyPath}`);
|
|
172
|
+
}
|
|
173
|
+
seenTargets.set(targetId, overlay.keyPath);
|
|
174
|
+
const target = inventory.agentsByQualifiedId[targetId];
|
|
175
|
+
if (!target) {
|
|
176
|
+
throwConfigError(overlay.sourcePath, overlay.keyPath, `unknown bundled agent target "${targetId}"`);
|
|
177
|
+
}
|
|
178
|
+
if (Object.hasOwn(nativeAgents, target.key)) {
|
|
179
|
+
throwConfigError(overlay.sourcePath, overlay.keyPath, `conflicts with native OpenCode agent.${target.key}; native agents are replacements for bundled agents`);
|
|
180
|
+
}
|
|
181
|
+
validateOverlayFields(overlay, "agent", enabledSkills);
|
|
182
|
+
result.push({
|
|
183
|
+
key,
|
|
184
|
+
target,
|
|
185
|
+
value: overlay.value,
|
|
186
|
+
sourcePath: overlay.sourcePath,
|
|
187
|
+
keyPath: overlay.keyPath
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function validateCategoryOverlays(inventory, overlays, enabledSkills) {
|
|
193
|
+
const categories = new Set(inventory.categories);
|
|
194
|
+
const result = [];
|
|
195
|
+
for (const [key, overlay] of Object.entries(overlays.categories)) {
|
|
196
|
+
if (!categories.has(key)) {
|
|
197
|
+
throwConfigError(overlay.sourcePath, overlay.keyPath, `unknown bundled agent category. Valid categories: ${inventory.categories.join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
validateOverlayFields(overlay, "category", enabledSkills);
|
|
200
|
+
result.push({
|
|
201
|
+
key,
|
|
202
|
+
value: overlay.value,
|
|
203
|
+
sourcePath: overlay.sourcePath,
|
|
204
|
+
keyPath: overlay.keyPath
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
function validateOverlayFields(overlay, targetType, enabledSkills) {
|
|
210
|
+
if (Object.hasOwn(overlay.value, "skills") && hasPermissionSkill(overlay.value.permission)) {
|
|
211
|
+
throwConfigError(overlay.sourcePath, overlay.keyPath, "cannot set both skills and permission.skill in the same overlay object");
|
|
212
|
+
}
|
|
213
|
+
for (const [field, value] of Object.entries(overlay.value)) {
|
|
214
|
+
const keyPath = `${overlay.keyPath}.${field}`;
|
|
215
|
+
if (!ALLOWED_OVERLAY_FIELDS.has(field)) {
|
|
216
|
+
throwConfigError(overlay.sourcePath, keyPath, `unsupported agent overlay field "${field}"`);
|
|
217
|
+
}
|
|
218
|
+
if (targetType === "category" && field === "disable") {
|
|
219
|
+
throwConfigError(overlay.sourcePath, keyPath, "disable is only valid for exact agent overlays");
|
|
220
|
+
}
|
|
221
|
+
validateOverlayFieldValue(overlay.sourcePath, keyPath, field, value, enabledSkills);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function hasPermissionSkill(permission) {
|
|
225
|
+
return isRecord(permission) && Object.hasOwn(permission, "skill");
|
|
226
|
+
}
|
|
227
|
+
function validateOverlayFieldValue(sourcePath, keyPath, field, value, enabledSkills) {
|
|
228
|
+
switch (field) {
|
|
229
|
+
case "model":
|
|
230
|
+
validateModel(sourcePath, keyPath, value);
|
|
231
|
+
return;
|
|
232
|
+
case "variant":
|
|
233
|
+
validateNonEmptyString(sourcePath, keyPath, value);
|
|
234
|
+
return;
|
|
235
|
+
case "temperature":
|
|
236
|
+
validateTemperature(sourcePath, keyPath, value);
|
|
237
|
+
return;
|
|
238
|
+
case "top_p":
|
|
239
|
+
validateTopP(sourcePath, keyPath, value);
|
|
240
|
+
return;
|
|
241
|
+
case "permission":
|
|
242
|
+
validatePermission(sourcePath, keyPath, value);
|
|
243
|
+
return;
|
|
244
|
+
case "mode":
|
|
245
|
+
validateMode(sourcePath, keyPath, value);
|
|
246
|
+
return;
|
|
247
|
+
case "color":
|
|
248
|
+
validateColor(sourcePath, keyPath, value);
|
|
249
|
+
return;
|
|
250
|
+
case "steps":
|
|
251
|
+
validatePositiveInteger(sourcePath, keyPath, value);
|
|
252
|
+
return;
|
|
253
|
+
case "hidden":
|
|
254
|
+
case "disable":
|
|
255
|
+
validateBoolean(sourcePath, keyPath, value);
|
|
256
|
+
return;
|
|
257
|
+
case "skills":
|
|
258
|
+
validateSkills(sourcePath, keyPath, value, enabledSkills);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function validateModel(sourcePath, keyPath, value) {
|
|
263
|
+
if (typeof value !== "string") {
|
|
264
|
+
throwConfigError(sourcePath, keyPath, "must be a provider/model string");
|
|
265
|
+
}
|
|
266
|
+
if (value !== value.trim() || /\s/.test(value)) {
|
|
267
|
+
throwConfigError(sourcePath, keyPath, "must be a provider/model string");
|
|
268
|
+
}
|
|
269
|
+
const slashIndex = value.indexOf("/");
|
|
270
|
+
if (value === "" || slashIndex <= 0 || slashIndex === value.length - 1) {
|
|
271
|
+
throwConfigError(sourcePath, keyPath, "must be a provider/model string");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function validateNonEmptyString(sourcePath, keyPath, value) {
|
|
275
|
+
if (typeof value !== "string" || value === "" || value !== value.trim()) {
|
|
276
|
+
throwConfigError(sourcePath, keyPath, "must be a non-empty string");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function validateTemperature(sourcePath, keyPath, value) {
|
|
280
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
281
|
+
throwConfigError(sourcePath, keyPath, "must be a non-negative finite number");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function validateTopP(sourcePath, keyPath, value) {
|
|
285
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
286
|
+
throwConfigError(sourcePath, keyPath, "must be a number from 0 to 1");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function validatePositiveInteger(sourcePath, keyPath, value) {
|
|
290
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
291
|
+
throwConfigError(sourcePath, keyPath, "must be a positive integer");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function validateBoolean(sourcePath, keyPath, value) {
|
|
295
|
+
if (typeof value !== "boolean") {
|
|
296
|
+
throwConfigError(sourcePath, keyPath, "must be a boolean");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function validateMode(sourcePath, keyPath, value) {
|
|
300
|
+
if (!isAgentMode(value)) {
|
|
301
|
+
throwConfigError(sourcePath, keyPath, "must be one of: subagent, primary, all");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function validateColor(sourcePath, keyPath, value) {
|
|
305
|
+
if (typeof value !== "string" || value !== value.trim() || !isOpenCodeColor(value)) {
|
|
306
|
+
throwConfigError(sourcePath, keyPath, "must be an OpenCode-compatible color string");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function isOpenCodeColor(value) {
|
|
310
|
+
if (value === "")
|
|
311
|
+
return false;
|
|
312
|
+
if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value))
|
|
313
|
+
return true;
|
|
314
|
+
return /^[a-zA-Z][a-zA-Z0-9-]*$/.test(value);
|
|
315
|
+
}
|
|
316
|
+
function validateSkills(sourcePath, keyPath, value, enabledSkills) {
|
|
317
|
+
if (!Array.isArray(value) || !value.every((skill) => typeof skill === "string" && skill !== "" && skill === skill.trim())) {
|
|
318
|
+
throwConfigError(sourcePath, keyPath, "must be an array of non-empty strings");
|
|
319
|
+
}
|
|
320
|
+
if (!enabledSkills)
|
|
321
|
+
return;
|
|
322
|
+
for (const skill of value) {
|
|
323
|
+
if (!enabledSkills.has(skill)) {
|
|
324
|
+
throwConfigError(sourcePath, keyPath, `unknown or disabled skill "${skill}"`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function validatePermission(sourcePath, keyPath, value) {
|
|
329
|
+
if (!isRecord(value)) {
|
|
330
|
+
throwConfigError(sourcePath, keyPath, "must be an object");
|
|
331
|
+
}
|
|
332
|
+
for (const [toolKey, rule] of Object.entries(value)) {
|
|
333
|
+
if (toolKey.trim() === "") {
|
|
334
|
+
throwConfigError(sourcePath, `${keyPath}.${toolKey}`, "must use a non-empty tool key");
|
|
335
|
+
}
|
|
336
|
+
validatePermissionRule(sourcePath, `${keyPath}.${toolKey}`, rule);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function validatePermissionRule(sourcePath, keyPath, value) {
|
|
340
|
+
if (isPermissionSetting(value))
|
|
341
|
+
return;
|
|
342
|
+
if (!isRecord(value)) {
|
|
343
|
+
throwConfigError(sourcePath, keyPath, "must be ask, allow, deny, or an object of pattern rules");
|
|
344
|
+
}
|
|
345
|
+
for (const [pattern, setting] of Object.entries(value)) {
|
|
346
|
+
if (pattern.trim() === "") {
|
|
347
|
+
throwConfigError(sourcePath, `${keyPath}.${pattern}`, "must use a non-empty permission pattern");
|
|
348
|
+
}
|
|
349
|
+
if (!isPermissionSetting(setting)) {
|
|
350
|
+
throwConfigError(sourcePath, `${keyPath}.${pattern}`, "must be ask, allow, or deny");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function readCategoryDirs(agentsDir) {
|
|
355
|
+
try {
|
|
356
|
+
return fs2.readdirSync(agentsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
357
|
+
} catch {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function readMarkdownFiles(categoryDir) {
|
|
362
|
+
try {
|
|
363
|
+
return fs2.readdirSync(categoryDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
364
|
+
} catch {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function validAgentKeys(inventory) {
|
|
369
|
+
return Object.keys(inventory.aliases).sort();
|
|
370
|
+
}
|
|
371
|
+
function throwConfigError(sourcePath, keyPath, message) {
|
|
372
|
+
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/lib/skill-loader.ts
|
|
376
|
+
import path3 from "path";
|
|
77
377
|
var SKILL_PREFIX = "systematic:";
|
|
78
378
|
var SKILL_DESCRIPTION_PREFIX = "(Systematic - Skill) ";
|
|
79
379
|
function formatSkillCommandName(name) {
|
|
@@ -90,7 +390,7 @@ function formatSkillDescription(description, fallbackName) {
|
|
|
90
390
|
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
91
391
|
}
|
|
92
392
|
function wrapSkillTemplate(skillPath, body) {
|
|
93
|
-
const skillDir =
|
|
393
|
+
const skillDir = path3.dirname(skillPath);
|
|
94
394
|
return `<skill-instruction>
|
|
95
395
|
Base directory for this skill: ${skillDir}/
|
|
96
396
|
File references (@path) in this skill are relative to this directory.
|
|
@@ -154,6 +454,7 @@ function loadAgentAsConfig(agentInfo) {
|
|
|
154
454
|
description,
|
|
155
455
|
prompt,
|
|
156
456
|
model,
|
|
457
|
+
variant,
|
|
157
458
|
temperature,
|
|
158
459
|
top_p,
|
|
159
460
|
tools,
|
|
@@ -170,6 +471,8 @@ function loadAgentAsConfig(agentInfo) {
|
|
|
170
471
|
};
|
|
171
472
|
if (model !== undefined)
|
|
172
473
|
config.model = model;
|
|
474
|
+
if (variant !== undefined)
|
|
475
|
+
config.variant = variant;
|
|
173
476
|
if (temperature !== undefined)
|
|
174
477
|
config.temperature = temperature;
|
|
175
478
|
if (top_p !== undefined)
|
|
@@ -237,19 +540,122 @@ function loadSkillAsCommand(loaded) {
|
|
|
237
540
|
config.subtask = loaded.subtask;
|
|
238
541
|
return config;
|
|
239
542
|
}
|
|
240
|
-
function collectAgents(dir, disabledAgents) {
|
|
543
|
+
function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
|
|
241
544
|
const agents = {};
|
|
242
545
|
const agentList = findAgentsInDir(dir);
|
|
546
|
+
const disabledSet = new Set(disabledAgents);
|
|
243
547
|
for (const agentInfo of agentList) {
|
|
244
|
-
|
|
548
|
+
const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
|
|
549
|
+
if (disabledSet.has(agentInfo.name) || disabledSet.has(id))
|
|
550
|
+
continue;
|
|
551
|
+
if (Object.hasOwn(nativeAgents, agentInfo.name))
|
|
552
|
+
continue;
|
|
553
|
+
const exactOverlay = overlays.agentsByTargetId.get(id);
|
|
554
|
+
if (exactOverlay?.value.disable === true)
|
|
245
555
|
continue;
|
|
246
556
|
const config = loadAgentAsConfig(agentInfo);
|
|
247
557
|
if (config) {
|
|
248
|
-
agents[agentInfo.name] = config;
|
|
558
|
+
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays);
|
|
249
559
|
}
|
|
250
560
|
}
|
|
251
561
|
return agents;
|
|
252
562
|
}
|
|
563
|
+
function applyAgentOverlays(config, agentInfo, overlays) {
|
|
564
|
+
const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
|
|
565
|
+
const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
|
|
566
|
+
const exactOverlay = overlays.agentsByTargetId.get(id);
|
|
567
|
+
const result = { ...config };
|
|
568
|
+
const permissionRules = createPermissionRuleAccumulator();
|
|
569
|
+
const hasPermissionOverlay = overlayControlsPermission(categoryOverlay?.value) || overlayControlsPermission(exactOverlay?.value);
|
|
570
|
+
if (hasPermissionOverlay && isRecord(config.permission)) {
|
|
571
|
+
addPermissionRules(permissionRules, config.permission);
|
|
572
|
+
}
|
|
573
|
+
result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
|
|
574
|
+
if (categoryOverlay) {
|
|
575
|
+
applyOverlayObject(result, categoryOverlay.value, permissionRules);
|
|
576
|
+
}
|
|
577
|
+
if (exactOverlay) {
|
|
578
|
+
applyOverlayObject(result, exactOverlay.value, permissionRules);
|
|
579
|
+
}
|
|
580
|
+
if (hasPermissionOverlay) {
|
|
581
|
+
const permission = permissionFromRules(permissionRules);
|
|
582
|
+
if (permission) {
|
|
583
|
+
result.permission = permission;
|
|
584
|
+
} else {
|
|
585
|
+
delete result.permission;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
function overlayControlsPermission(overlay) {
|
|
591
|
+
return overlay !== undefined && (Object.hasOwn(overlay, "permission") || Object.hasOwn(overlay, "skills"));
|
|
592
|
+
}
|
|
593
|
+
var OVERLAY_ASSIGN_FIELDS = [
|
|
594
|
+
"model",
|
|
595
|
+
"variant",
|
|
596
|
+
"temperature",
|
|
597
|
+
"top_p",
|
|
598
|
+
"mode",
|
|
599
|
+
"color",
|
|
600
|
+
"steps",
|
|
601
|
+
"hidden"
|
|
602
|
+
];
|
|
603
|
+
function applyOverlayObject(target, overlay, permissionRules) {
|
|
604
|
+
for (const field of OVERLAY_ASSIGN_FIELDS) {
|
|
605
|
+
if (Object.hasOwn(overlay, field)) {
|
|
606
|
+
target[field] = overlay[field];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (isRecord(overlay.permission)) {
|
|
610
|
+
addPermissionRules(permissionRules, overlay.permission);
|
|
611
|
+
}
|
|
612
|
+
if (Array.isArray(overlay.skills)) {
|
|
613
|
+
addManagedSkillRules(permissionRules, overlay.skills);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function createPermissionRuleAccumulator() {
|
|
617
|
+
return new Map;
|
|
618
|
+
}
|
|
619
|
+
function addPermissionRules(accumulator, permission) {
|
|
620
|
+
for (const [tool, rule] of Object.entries(permission)) {
|
|
621
|
+
if (isPermissionSettingValue(rule)) {
|
|
622
|
+
setPermissionRule(accumulator, tool, "*", rule);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (!isRecord(rule))
|
|
626
|
+
continue;
|
|
627
|
+
for (const [pattern, setting] of Object.entries(rule)) {
|
|
628
|
+
if (isPermissionSettingValue(setting)) {
|
|
629
|
+
setPermissionRule(accumulator, tool, pattern, setting);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function addManagedSkillRules(accumulator, skills) {
|
|
635
|
+
setPermissionRule(accumulator, "skill", "*", "deny");
|
|
636
|
+
for (const skill of skills) {
|
|
637
|
+
if (typeof skill === "string") {
|
|
638
|
+
setPermissionRule(accumulator, "skill", skill, "allow");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function permissionFromRules(accumulator) {
|
|
643
|
+
const permission = {};
|
|
644
|
+
for (const [tool, rules] of accumulator) {
|
|
645
|
+
permission[tool] = Object.fromEntries(rules);
|
|
646
|
+
}
|
|
647
|
+
return Object.keys(permission).length > 0 ? permission : undefined;
|
|
648
|
+
}
|
|
649
|
+
function setPermissionRule(accumulator, tool, pattern, setting) {
|
|
650
|
+
const rules = accumulator.get(tool) ?? new Map;
|
|
651
|
+
if (rules.has(pattern))
|
|
652
|
+
rules.delete(pattern);
|
|
653
|
+
rules.set(pattern, setting);
|
|
654
|
+
accumulator.set(tool, rules);
|
|
655
|
+
}
|
|
656
|
+
function isPermissionSettingValue(value) {
|
|
657
|
+
return value === "ask" || value === "allow" || value === "deny";
|
|
658
|
+
}
|
|
253
659
|
function collectCommands(dir, disabledCommands) {
|
|
254
660
|
const commands = {};
|
|
255
661
|
const commandList = findCommandsInDir(dir);
|
|
@@ -280,19 +686,32 @@ function collectSkillsAsCommands(dir, disabledSkills) {
|
|
|
280
686
|
}
|
|
281
687
|
return commands;
|
|
282
688
|
}
|
|
689
|
+
function collectEnabledSkillNames(dir, disabledSkills) {
|
|
690
|
+
const disabledSet = new Set(disabledSkills);
|
|
691
|
+
return findSkillsInDir(dir).filter((skillInfo) => !disabledSet.has(skillInfo.name)).map((skillInfo) => skillInfo.name);
|
|
692
|
+
}
|
|
283
693
|
function createConfigHandler(deps) {
|
|
284
694
|
const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
|
|
285
695
|
return async (config) => {
|
|
286
|
-
const systematicConfig =
|
|
287
|
-
const
|
|
288
|
-
const
|
|
696
|
+
const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
|
|
697
|
+
const existingAgents = { ...config.agent ?? {} };
|
|
698
|
+
const existingCommands = { ...config.command ?? {} };
|
|
289
699
|
const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, systematicConfig.disabled_skills);
|
|
290
|
-
const
|
|
700
|
+
const enabledSkillNames = collectEnabledSkillNames(bundledSkillsDir, systematicConfig.disabled_skills);
|
|
701
|
+
const inventory = buildBundledAgentInventory(bundledAgentsDir, systematicConfig.disabled_agents);
|
|
702
|
+
const validatedOverlays = validateAgentOverlays({
|
|
703
|
+
inventory,
|
|
704
|
+
overlays,
|
|
705
|
+
nativeAgents: existingAgents,
|
|
706
|
+
enabledSkills: enabledSkillNames
|
|
707
|
+
});
|
|
708
|
+
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
709
|
+
const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, existingAgents, resolvedOverlays);
|
|
710
|
+
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
291
711
|
config.agent = {
|
|
292
712
|
...bundledAgents,
|
|
293
713
|
...existingAgents
|
|
294
714
|
};
|
|
295
|
-
const existingCommands = config.command ?? {};
|
|
296
715
|
config.command = {
|
|
297
716
|
...bundledCommands,
|
|
298
717
|
...bundledSkills,
|
|
@@ -303,11 +722,12 @@ function createConfigHandler(deps) {
|
|
|
303
722
|
}
|
|
304
723
|
function registerSkillsPaths(config, skillsDir) {
|
|
305
724
|
const extended = config;
|
|
306
|
-
extended.skills
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
extended.skills
|
|
310
|
-
|
|
725
|
+
const paths = extended.skills?.paths ?? [];
|
|
726
|
+
const nextPaths = paths.includes(skillsDir) ? [...paths] : [...paths, skillsDir];
|
|
727
|
+
extended.skills = {
|
|
728
|
+
...extended.skills,
|
|
729
|
+
paths: nextPaths
|
|
730
|
+
};
|
|
311
731
|
}
|
|
312
732
|
|
|
313
733
|
// src/lib/plugin-singleton.ts
|
|
@@ -342,8 +762,8 @@ async function plugInOnce({
|
|
|
342
762
|
}
|
|
343
763
|
|
|
344
764
|
// src/lib/skill-tool.ts
|
|
345
|
-
import
|
|
346
|
-
import
|
|
765
|
+
import fs3 from "fs";
|
|
766
|
+
import path4 from "path";
|
|
347
767
|
import { pathToFileURL } from "url";
|
|
348
768
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
349
769
|
function formatSkillsXml(skills) {
|
|
@@ -369,17 +789,17 @@ function discoverSkillFiles(dir, limit = 10) {
|
|
|
369
789
|
function handleEntry(entry, currentDir) {
|
|
370
790
|
if (entry.isDirectory()) {
|
|
371
791
|
if (!shouldSkipDirectory(entry.name)) {
|
|
372
|
-
recurse(
|
|
792
|
+
recurse(path4.resolve(currentDir, entry.name));
|
|
373
793
|
}
|
|
374
794
|
} else if (shouldIncludeFile(entry.name)) {
|
|
375
|
-
files.push(
|
|
795
|
+
files.push(path4.resolve(currentDir, entry.name));
|
|
376
796
|
}
|
|
377
797
|
}
|
|
378
798
|
function recurse(currentDir) {
|
|
379
799
|
if (files.length >= limit)
|
|
380
800
|
return;
|
|
381
801
|
try {
|
|
382
|
-
const entries =
|
|
802
|
+
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
383
803
|
for (const entry of entries) {
|
|
384
804
|
if (files.length >= limit)
|
|
385
805
|
break;
|
|
@@ -460,7 +880,7 @@ function createSkillTool(options) {
|
|
|
460
880
|
throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
|
|
461
881
|
}
|
|
462
882
|
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
463
|
-
const dir =
|
|
883
|
+
const dir = path4.dirname(matchedSkill.skillFile);
|
|
464
884
|
const base = pathToFileURL(dir).href;
|
|
465
885
|
const files = discoverSkillFiles(dir);
|
|
466
886
|
await context.ask({
|
|
@@ -497,12 +917,12 @@ function createSkillTool(options) {
|
|
|
497
917
|
}
|
|
498
918
|
|
|
499
919
|
// src/index.ts
|
|
500
|
-
var __dirname2 =
|
|
501
|
-
var packageRoot =
|
|
502
|
-
var bundledSkillsDir =
|
|
503
|
-
var bundledAgentsDir =
|
|
504
|
-
var bundledCommandsDir =
|
|
505
|
-
var packageJsonPath =
|
|
920
|
+
var __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
|
|
921
|
+
var packageRoot = path5.resolve(__dirname2, "..");
|
|
922
|
+
var bundledSkillsDir = path5.join(packageRoot, "skills");
|
|
923
|
+
var bundledAgentsDir = path5.join(packageRoot, "agents");
|
|
924
|
+
var bundledCommandsDir = path5.join(packageRoot, "commands");
|
|
925
|
+
var packageJsonPath = path5.join(packageRoot, "package.json");
|
|
506
926
|
var hasLoggedInit = false;
|
|
507
927
|
var applyBootstrapContent = (output, content) => {
|
|
508
928
|
if (output.system.length > 0) {
|
|
@@ -515,9 +935,9 @@ ${content}`;
|
|
|
515
935
|
};
|
|
516
936
|
var getPackageVersion = () => {
|
|
517
937
|
try {
|
|
518
|
-
if (!
|
|
938
|
+
if (!fs4.existsSync(packageJsonPath))
|
|
519
939
|
return "unknown";
|
|
520
|
-
const content =
|
|
940
|
+
const content = fs4.readFileSync(packageJsonPath, "utf8");
|
|
521
941
|
const parsed = JSON.parse(content);
|
|
522
942
|
return parsed.version ?? "unknown";
|
|
523
943
|
} catch {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SourcedOverlayConfigMap } from './config.js';
|
|
2
|
+
export interface BundledAgentInventoryEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
key: string;
|
|
5
|
+
category: string;
|
|
6
|
+
file: string;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface BundledAgentInventory {
|
|
10
|
+
agentsByQualifiedId: Record<string, BundledAgentInventoryEntry>;
|
|
11
|
+
aliases: Record<string, string>;
|
|
12
|
+
categories: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ValidatedAgentOverlay {
|
|
15
|
+
key: string;
|
|
16
|
+
target: BundledAgentInventoryEntry;
|
|
17
|
+
value: Record<string, unknown>;
|
|
18
|
+
sourcePath: string;
|
|
19
|
+
keyPath: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ValidatedCategoryOverlay {
|
|
22
|
+
key: string;
|
|
23
|
+
value: Record<string, unknown>;
|
|
24
|
+
sourcePath: string;
|
|
25
|
+
keyPath: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ValidatedAgentOverlays {
|
|
28
|
+
agents: ValidatedAgentOverlay[];
|
|
29
|
+
categories: ValidatedCategoryOverlay[];
|
|
30
|
+
}
|
|
31
|
+
export interface ValidateAgentOverlaysOptions {
|
|
32
|
+
inventory: BundledAgentInventory;
|
|
33
|
+
overlays: SourcedOverlayConfigMap;
|
|
34
|
+
nativeAgents?: Record<string, unknown>;
|
|
35
|
+
enabledSkills?: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface ResolvedAgentOverlaySet {
|
|
38
|
+
agentsByTargetId: Map<string, ValidatedAgentOverlay>;
|
|
39
|
+
categoriesByKey: Map<string, ValidatedCategoryOverlay>;
|
|
40
|
+
}
|
|
41
|
+
export declare function buildBundledAgentInventory(agentsDir: string, disabledAgents: string[]): BundledAgentInventory;
|
|
42
|
+
export declare function validateAgentOverlays({ inventory, overlays, nativeAgents, enabledSkills, }: ValidateAgentOverlaysOptions): ValidatedAgentOverlays;
|
|
43
|
+
export declare function resolveAgentOverlaySet(overlays: ValidatedAgentOverlays): ResolvedAgentOverlaySet;
|
|
44
|
+
export declare function inferBuiltInTemperature(name: string, description?: string): number;
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -13,8 +13,9 @@ export declare function formatAgentDescription(name: string, description: string
|
|
|
13
13
|
* This follows the pattern used by oh-my-opencode to inject bundled agents,
|
|
14
14
|
* skills (as commands), and commands into OpenCode's configuration.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* Existing OpenCode
|
|
16
|
+
* Bundled content is loaded and then tuned with Systematic agent overlays.
|
|
17
|
+
* Existing native OpenCode agents with the same emitted key are preserved as
|
|
18
|
+
* replacements for bundled agents.
|
|
18
19
|
*/
|
|
19
20
|
export declare function createConfigHandler(deps: ConfigHandlerDeps): (config: Config) => Promise<void>;
|
|
20
21
|
/** Register a directory for OpenCode's native skill discovery (`skill` tool). */
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -2,14 +2,32 @@ export interface BootstrapConfig {
|
|
|
2
2
|
enabled: boolean;
|
|
3
3
|
file?: string;
|
|
4
4
|
}
|
|
5
|
+
export type OverlayConfig = Record<string, unknown>;
|
|
6
|
+
export type OverlayConfigMap = Record<string, OverlayConfig>;
|
|
7
|
+
export interface SourcedOverlayConfig {
|
|
8
|
+
value: OverlayConfig;
|
|
9
|
+
sourcePath: string;
|
|
10
|
+
keyPath: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SourcedOverlayConfigMap {
|
|
13
|
+
agents: Record<string, SourcedOverlayConfig>;
|
|
14
|
+
categories: Record<string, SourcedOverlayConfig>;
|
|
15
|
+
}
|
|
16
|
+
export interface SourceAwareConfigResult {
|
|
17
|
+
config: SystematicConfig;
|
|
18
|
+
overlays: SourcedOverlayConfigMap;
|
|
19
|
+
}
|
|
5
20
|
export interface SystematicConfig {
|
|
6
21
|
disabled_skills: string[];
|
|
7
22
|
disabled_agents: string[];
|
|
8
23
|
disabled_commands: string[];
|
|
9
24
|
bootstrap: BootstrapConfig;
|
|
25
|
+
agents?: OverlayConfigMap;
|
|
26
|
+
categories?: OverlayConfigMap;
|
|
10
27
|
}
|
|
11
28
|
export declare const DEFAULT_CONFIG: SystematicConfig;
|
|
12
29
|
export declare function loadConfig(projectDir: string): SystematicConfig;
|
|
30
|
+
export declare function loadConfigWithSources(projectDir: string): SourceAwareConfigResult;
|
|
13
31
|
export declare function getConfigPaths(projectDir: string): {
|
|
14
32
|
customConfig?: string | undefined;
|
|
15
33
|
customDir?: string | undefined;
|