@fro.bot/systematic 2.14.1 → 2.14.3
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/dist/index.js +82 -37
- package/dist/lib/model-availability.d.ts +14 -8
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -767,13 +767,39 @@ async function getAvailableModels(client, options = {}) {
|
|
|
767
767
|
if (response.error !== undefined || response.data === undefined) {
|
|
768
768
|
return readFallbackCache();
|
|
769
769
|
}
|
|
770
|
+
const models = buildSetFromProviders(response.data.providers);
|
|
771
|
+
if (models.size === 0) {
|
|
772
|
+
return emptyAvailability();
|
|
773
|
+
}
|
|
770
774
|
return {
|
|
771
775
|
status: "api",
|
|
772
|
-
models
|
|
776
|
+
models
|
|
773
777
|
};
|
|
774
778
|
}
|
|
775
779
|
|
|
776
780
|
// src/lib/config-handler.ts
|
|
781
|
+
function isSystematicAgentConfig(agent) {
|
|
782
|
+
const description = agent?.description;
|
|
783
|
+
return typeof description === "string" && /\(.* - Systematic\)$/.test(description);
|
|
784
|
+
}
|
|
785
|
+
function isSystematicCommandConfig(command) {
|
|
786
|
+
const description = command?.description;
|
|
787
|
+
return typeof description === "string" && (description.startsWith("(Systematic) ") || description.startsWith("(Systematic - Skill) "));
|
|
788
|
+
}
|
|
789
|
+
function mergeSystematicEntries(existing, emitted, shouldDropExisting) {
|
|
790
|
+
const merged = { ...existing ?? {} };
|
|
791
|
+
for (const [key, value] of Object.entries(existing ?? {})) {
|
|
792
|
+
if (shouldDropExisting(key, value)) {
|
|
793
|
+
delete merged[key];
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const [key, value] of Object.entries(emitted)) {
|
|
797
|
+
if (Object.hasOwn(merged, key))
|
|
798
|
+
continue;
|
|
799
|
+
merged[key] = value;
|
|
800
|
+
}
|
|
801
|
+
return merged;
|
|
802
|
+
}
|
|
777
803
|
function toTitleCase(name) {
|
|
778
804
|
return name.split("-").map((segment) => segment.length > 0 ? segment.charAt(0).toUpperCase() + segment.slice(1) : segment).join("-");
|
|
779
805
|
}
|
|
@@ -912,30 +938,37 @@ function applyAgentOverlays(config, agentInfo, overlays, availabilitySet) {
|
|
|
912
938
|
addPermissionRules(permissionRules, config.permission);
|
|
913
939
|
}
|
|
914
940
|
result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
941
|
+
applySourceModelDefault(result, agentInfo, availabilitySet);
|
|
942
|
+
applyAgentOverlay(result, categoryOverlay?.value, permissionRules);
|
|
943
|
+
applyAgentOverlay(result, exactOverlay?.value, permissionRules);
|
|
944
|
+
applyPermissionOverlay(result, permissionRules, hasPermissionOverlay);
|
|
945
|
+
return result;
|
|
946
|
+
}
|
|
947
|
+
function applySourceModelDefault(target, agentInfo, availabilitySet) {
|
|
948
|
+
if (!agentInfo.category || availabilitySet === undefined)
|
|
949
|
+
return;
|
|
950
|
+
const resolved = resolveSourceModel(agentInfo.category, availabilitySet);
|
|
951
|
+
target.model = `${resolved.provider}/${resolved.model}`;
|
|
952
|
+
if (resolved.variant !== undefined) {
|
|
953
|
+
target.variant = resolved.variant;
|
|
954
|
+
} else {
|
|
955
|
+
delete target.variant;
|
|
929
956
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
957
|
+
}
|
|
958
|
+
function applyAgentOverlay(target, overlay, permissionRules) {
|
|
959
|
+
if (overlay === undefined)
|
|
960
|
+
return;
|
|
961
|
+
applyOverlayObjectWithVariantClearing(target, overlay, permissionRules);
|
|
962
|
+
}
|
|
963
|
+
function applyPermissionOverlay(target, permissionRules, hasPermissionOverlay) {
|
|
964
|
+
if (!hasPermissionOverlay)
|
|
965
|
+
return;
|
|
966
|
+
const permission = permissionFromRules(permissionRules);
|
|
967
|
+
if (permission) {
|
|
968
|
+
target.permission = permission;
|
|
969
|
+
} else {
|
|
970
|
+
delete target.permission;
|
|
937
971
|
}
|
|
938
|
-
return result;
|
|
939
972
|
}
|
|
940
973
|
function overlayControlsPermission(overlay) {
|
|
941
974
|
return overlay !== undefined && (Object.hasOwn(overlay, "permission") || Object.hasOwn(overlay, "skills"));
|
|
@@ -1055,42 +1088,54 @@ function createConfigHandler(deps) {
|
|
|
1055
1088
|
const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
|
|
1056
1089
|
const existingAgents = { ...config.agent ?? {} };
|
|
1057
1090
|
const existingCommands = { ...config.command ?? {} };
|
|
1091
|
+
const nativeAgents = Object.fromEntries(Object.entries(existingAgents).filter(([, agent]) => !isSystematicAgentConfig(agent)));
|
|
1058
1092
|
const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, systematicConfig.disabled_skills);
|
|
1059
1093
|
const enabledSkillNames = collectEnabledSkillNames(bundledSkillsDir, systematicConfig.disabled_skills);
|
|
1060
1094
|
const inventory = buildBundledAgentInventory(bundledAgentsDir2, systematicConfig.disabled_agents);
|
|
1095
|
+
const availability = deps.client ? await getAvailableModels(deps.client) : undefined;
|
|
1096
|
+
const availabilitySet = availability && availability.status !== "unknown" ? availability.models : undefined;
|
|
1061
1097
|
assertSourceCategoryModelCoverage(inventory.categories);
|
|
1062
1098
|
const validatedOverlays = validateAgentOverlays({
|
|
1063
1099
|
inventory,
|
|
1064
1100
|
overlays,
|
|
1065
|
-
nativeAgents
|
|
1101
|
+
nativeAgents,
|
|
1066
1102
|
enabledSkills: enabledSkillNames
|
|
1067
1103
|
});
|
|
1068
1104
|
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
1069
|
-
const
|
|
1070
|
-
const availabilitySet = availability && availability.status !== "unknown" ? availability.models : undefined;
|
|
1071
|
-
const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, existingAgents, resolvedOverlays, availabilitySet);
|
|
1105
|
+
const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, nativeAgents, resolvedOverlays, availabilitySet);
|
|
1072
1106
|
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
config.command =
|
|
1078
|
-
...bundledCommands,
|
|
1079
|
-
...bundledSkills,
|
|
1080
|
-
...existingCommands
|
|
1081
|
-
};
|
|
1107
|
+
const bundledAgentKeys = new Set(Object.keys(bundledAgents));
|
|
1108
|
+
config.agent = mergeSystematicEntries(existingAgents, bundledAgents, (key, agent) => bundledAgentKeys.has(key) && isSystematicAgentConfig(agent));
|
|
1109
|
+
const emittedCommands = { ...bundledCommands, ...bundledSkills };
|
|
1110
|
+
const emittedCommandKeys = new Set(Object.keys(emittedCommands));
|
|
1111
|
+
config.command = mergeSystematicEntries(existingCommands, emittedCommands, (key, command) => isSystematicCommandConfig(command) && (emittedCommandKeys.has(key) || isSystematicOwnedCommandKey(key)));
|
|
1082
1112
|
registerSkillsPaths(config, bundledSkillsDir);
|
|
1083
1113
|
};
|
|
1084
1114
|
}
|
|
1085
1115
|
function registerSkillsPaths(config, skillsDir) {
|
|
1086
1116
|
const extended = config;
|
|
1087
1117
|
const paths = extended.skills?.paths ?? [];
|
|
1088
|
-
const nextPaths = paths
|
|
1118
|
+
const nextPaths = removeSystematicSkillPaths(paths);
|
|
1119
|
+
if (!nextPaths.includes(skillsDir))
|
|
1120
|
+
nextPaths.push(skillsDir);
|
|
1089
1121
|
extended.skills = {
|
|
1090
1122
|
...extended.skills,
|
|
1091
1123
|
paths: nextPaths
|
|
1092
1124
|
};
|
|
1093
1125
|
}
|
|
1126
|
+
function removeSystematicSkillPaths(paths) {
|
|
1127
|
+
return paths.filter((path6) => !isSystematicSkillPath(path6));
|
|
1128
|
+
}
|
|
1129
|
+
function isSystematicSkillPath(path6) {
|
|
1130
|
+
const normalizedPath = normalizePath(path6);
|
|
1131
|
+
return normalizedPath.endsWith("/.config/opencode/systematic/skills") || normalizedPath.endsWith("/.cache/opencode/systematic/skills") || normalizedPath.endsWith("/.local/share/opencode/systematic/skills") || normalizedPath.endsWith("/.opencode/systematic/skills") || /(?:^|\/)\.cache\/opencode\/packages\/@fro\.bot\/systematic@[^/]+\/node_modules\/@fro\.bot\/systematic\/skills(?:$|\/)/u.test(normalizedPath);
|
|
1132
|
+
}
|
|
1133
|
+
function normalizePath(path6) {
|
|
1134
|
+
return path6.replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
1135
|
+
}
|
|
1136
|
+
function isSystematicOwnedCommandKey(key) {
|
|
1137
|
+
return key.startsWith("systematic:") || key.startsWith("ce:");
|
|
1138
|
+
}
|
|
1094
1139
|
|
|
1095
1140
|
// src/lib/skill-tool.ts
|
|
1096
1141
|
import fs4 from "fs";
|
|
@@ -22,16 +22,22 @@ export interface OpencodeClientLike {
|
|
|
22
22
|
* Outcome of model availability discovery.
|
|
23
23
|
*
|
|
24
24
|
* - `api`: The OpenCode server's `/config/providers` endpoint responded with
|
|
25
|
-
* a connected-providers payload
|
|
26
|
-
*
|
|
25
|
+
* a connected-providers payload AND `models` is non-empty. An authoritatively
|
|
26
|
+
* empty response (`data.providers = []`, or providers present with zero
|
|
27
|
+
* models) collapses to `'unknown'` instead — see below — because the
|
|
28
|
+
* operational consequence is identical and downstream consumers should treat
|
|
29
|
+
* both cases the same way.
|
|
27
30
|
* - `cache`: The API call failed (error envelope, thrown, or timed out) and
|
|
28
31
|
* the local `models.json` cache was readable. `models` reflects whatever
|
|
29
|
-
* OpenCode last wrote to disk.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* OpenCode last wrote to disk. The cache may itself be empty; callers that
|
|
33
|
+
* need to distinguish "cached empty" from "cached with content" should
|
|
34
|
+
* inspect `models.size`.
|
|
35
|
+
* - `unknown`: Either both the API call and the cache fallback failed (cache
|
|
36
|
+
* missing, unreadable, corrupt, or schema-mismatched), OR the API call
|
|
37
|
+
* succeeded with zero usable models. Resolution should degrade gracefully —
|
|
38
|
+
* callers should treat `unknown` as a signal to skip source-default model
|
|
39
|
+
* pinning so users do not get agents pinned to inaccessible models.
|
|
40
|
+
* `models` is the empty set.
|
|
35
41
|
*/
|
|
36
42
|
export type DiscoveryStatus = 'api' | 'cache' | 'unknown';
|
|
37
43
|
export interface ModelAvailability {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.3",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://fro.bot/systematic",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@opencode-ai/sdk": "1.14.48",
|
|
69
69
|
"@types/bun": "latest",
|
|
70
70
|
"@types/js-yaml": "4.0.9",
|
|
71
|
-
"@types/node": "24.12.
|
|
71
|
+
"@types/node": "24.12.4",
|
|
72
72
|
"ajv": "8.20.0",
|
|
73
73
|
"ajv-formats": "3.0.1",
|
|
74
74
|
"conventional-changelog-conventionalcommits": "9.3.1",
|