@fro.bot/systematic 2.32.1 → 2.33.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/dist/cli.js +1 -1
- package/dist/{index-wjkgb2gb.js → index-bwrrk4a8.js} +41 -26
- package/dist/index.js +220 -39
- package/dist/lib/config-handler.d.ts +4 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/discovered-skills.d.ts +67 -0
- package/dist/schemas/systematic-config.schema.json +16 -0
- package/package.json +4 -4
- package/skills/gemini-imagegen/requirements.txt +1 -1
package/dist/cli.js
CHANGED
|
@@ -485,6 +485,36 @@ function walkDir(rootDir, options = {}) {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
// src/lib/skills.ts
|
|
488
|
+
function parseMetadata(data) {
|
|
489
|
+
const metadataRaw = data.metadata;
|
|
490
|
+
if (!isRecord(metadataRaw)) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const entries = Object.entries(metadataRaw);
|
|
494
|
+
if (!entries.every(([, v]) => typeof v === "string")) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
return Object.fromEntries(entries);
|
|
498
|
+
}
|
|
499
|
+
function parseDeprecated(data) {
|
|
500
|
+
const deprecatedRaw = data.deprecated;
|
|
501
|
+
if (!isRecord(deprecatedRaw)) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const since = typeof deprecatedRaw.since === "string" && deprecatedRaw.since !== "" ? deprecatedRaw.since : undefined;
|
|
505
|
+
const removal = typeof deprecatedRaw.removal === "string" && deprecatedRaw.removal !== "" ? deprecatedRaw.removal : undefined;
|
|
506
|
+
if (since === undefined || removal === undefined) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const deprecated = { since, removal };
|
|
510
|
+
if (typeof deprecatedRaw.replacement === "string") {
|
|
511
|
+
deprecated.replacement = deprecatedRaw.replacement;
|
|
512
|
+
}
|
|
513
|
+
if (typeof deprecatedRaw.reason === "string") {
|
|
514
|
+
deprecated.reason = deprecatedRaw.reason;
|
|
515
|
+
}
|
|
516
|
+
return deprecated;
|
|
517
|
+
}
|
|
488
518
|
function extractFrontmatter(filePath) {
|
|
489
519
|
try {
|
|
490
520
|
const content = fs3.readFileSync(filePath, "utf8");
|
|
@@ -492,29 +522,8 @@ function extractFrontmatter(filePath) {
|
|
|
492
522
|
if (parseError) {
|
|
493
523
|
return { name: "", description: "" };
|
|
494
524
|
}
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
if (isRecord(metadataRaw)) {
|
|
498
|
-
const entries = Object.entries(metadataRaw);
|
|
499
|
-
if (entries.every(([, v]) => typeof v === "string")) {
|
|
500
|
-
metadata = Object.fromEntries(entries);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
const deprecatedRaw = data.deprecated;
|
|
504
|
-
let deprecated;
|
|
505
|
-
if (isRecord(deprecatedRaw)) {
|
|
506
|
-
const since = typeof deprecatedRaw.since === "string" && deprecatedRaw.since !== "" ? deprecatedRaw.since : undefined;
|
|
507
|
-
const removal = typeof deprecatedRaw.removal === "string" && deprecatedRaw.removal !== "" ? deprecatedRaw.removal : undefined;
|
|
508
|
-
if (since !== undefined && removal !== undefined) {
|
|
509
|
-
deprecated = { since, removal };
|
|
510
|
-
if (typeof deprecatedRaw.replacement === "string") {
|
|
511
|
-
deprecated.replacement = deprecatedRaw.replacement;
|
|
512
|
-
}
|
|
513
|
-
if (typeof deprecatedRaw.reason === "string") {
|
|
514
|
-
deprecated.reason = deprecatedRaw.reason;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
525
|
+
const metadata = parseMetadata(data);
|
|
526
|
+
const deprecated = parseDeprecated(data);
|
|
518
527
|
const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
|
|
519
528
|
const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
|
|
520
529
|
return {
|
|
@@ -16040,6 +16049,10 @@ function createSystematicConfigSchema(opts) {
|
|
|
16040
16049
|
{ enabled: true },
|
|
16041
16050
|
{ enabled: false, file: ".opencode/custom-prompt.md" }
|
|
16042
16051
|
]
|
|
16052
|
+
}),
|
|
16053
|
+
skills_as_commands: exports_external.boolean().default(true).meta({
|
|
16054
|
+
description: "Register skills discovered from user/project skill directories (OpenCode config and other agent-harness-standard locations) as slash commands. Default true.",
|
|
16055
|
+
examples: [true, false]
|
|
16043
16056
|
})
|
|
16044
16057
|
}).strict().meta({
|
|
16045
16058
|
description: "Systematic user configuration file (systematic.json / systematic.jsonc)",
|
|
@@ -16071,7 +16084,8 @@ var DEFAULT_CONFIG = {
|
|
|
16071
16084
|
enabled: true
|
|
16072
16085
|
},
|
|
16073
16086
|
agents: {},
|
|
16074
|
-
categories: {}
|
|
16087
|
+
categories: {},
|
|
16088
|
+
skills_as_commands: true
|
|
16075
16089
|
};
|
|
16076
16090
|
var SECURITY_OVERLAY_FIELDS2 = new Set(SECURITY_OVERLAY_FIELDS);
|
|
16077
16091
|
var CURRENT_SKILL_NAMES_SET = new Set(BUNDLED_SKILL_NAMES);
|
|
@@ -16240,7 +16254,8 @@ function loadConfigWithSources(projectDir) {
|
|
|
16240
16254
|
...customConfig?.bootstrap
|
|
16241
16255
|
},
|
|
16242
16256
|
agents: overlayValues(overlays.agents),
|
|
16243
|
-
categories: overlayValues(overlays.categories)
|
|
16257
|
+
categories: overlayValues(overlays.categories),
|
|
16258
|
+
skills_as_commands: customConfig?.skills_as_commands ?? projectConfig?.skills_as_commands ?? userConfig?.skills_as_commands ?? DEFAULT_CONFIG.skills_as_commands
|
|
16244
16259
|
};
|
|
16245
16260
|
const warned = new Set;
|
|
16246
16261
|
const droppedSkills = computeDroppedNames(result.disabled_skills, CURRENT_SKILL_NAMES_SET);
|
|
@@ -16407,4 +16422,4 @@ function extractCommandFrontmatter(content) {
|
|
|
16407
16422
|
};
|
|
16408
16423
|
}
|
|
16409
16424
|
|
|
16410
|
-
export { parseFrontmatter, isRecord, convertContent, convertFileWithCache, findSkillsInDir, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter };
|
|
16425
|
+
export { parseFrontmatter, isRecord, convertContent, convertFileWithCache, walkDir, extractFrontmatter, findSkillsInDir, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter };
|
package/dist/index.js
CHANGED
|
@@ -6,18 +6,20 @@ import {
|
|
|
6
6
|
exports_external,
|
|
7
7
|
extractAgentFrontmatter,
|
|
8
8
|
extractCommandFrontmatter,
|
|
9
|
+
extractFrontmatter,
|
|
9
10
|
findAgentsInDir,
|
|
10
11
|
findCommandsInDir,
|
|
11
12
|
findSkillsInDir,
|
|
12
13
|
isRecord,
|
|
13
14
|
loadConfig,
|
|
14
15
|
loadConfigWithSources,
|
|
15
|
-
parseFrontmatter
|
|
16
|
-
|
|
16
|
+
parseFrontmatter,
|
|
17
|
+
walkDir
|
|
18
|
+
} from "./index-bwrrk4a8.js";
|
|
17
19
|
|
|
18
20
|
// src/index.ts
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
+
import fs7 from "fs";
|
|
22
|
+
import path9 from "path";
|
|
21
23
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
22
24
|
|
|
23
25
|
// src/lib/bootstrap.ts
|
|
@@ -237,6 +239,11 @@ ${skillUsage}${catalogSection}
|
|
|
237
239
|
</SYSTEMATIC_WORKFLOWS>`;
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
// src/lib/config-handler.ts
|
|
243
|
+
import fs5 from "fs";
|
|
244
|
+
import os3 from "os";
|
|
245
|
+
import path7 from "path";
|
|
246
|
+
|
|
240
247
|
// src/lib/agent-overlays.ts
|
|
241
248
|
import fs2 from "fs";
|
|
242
249
|
import path4 from "path";
|
|
@@ -624,11 +631,137 @@ function throwConfigError(sourcePath, keyPath, message) {
|
|
|
624
631
|
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
|
|
625
632
|
}
|
|
626
633
|
|
|
634
|
+
// src/lib/discovered-skills.ts
|
|
635
|
+
import fs3 from "fs";
|
|
636
|
+
import path5 from "path";
|
|
637
|
+
var SKILL_NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
638
|
+
function isValidSkillName(name) {
|
|
639
|
+
return name.length >= 1 && name.length <= 64 && SKILL_NAME_REGEX.test(name);
|
|
640
|
+
}
|
|
641
|
+
function findGitWorktreeRoot(startDir) {
|
|
642
|
+
let current = path5.resolve(startDir);
|
|
643
|
+
while (true) {
|
|
644
|
+
try {
|
|
645
|
+
if (fs3.existsSync(path5.join(current, ".git"))) {
|
|
646
|
+
return current;
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
const parent = path5.dirname(current);
|
|
652
|
+
if (parent === current)
|
|
653
|
+
return null;
|
|
654
|
+
current = parent;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function upWalk(targets, start, stop) {
|
|
658
|
+
const results = [];
|
|
659
|
+
let current = path5.resolve(start);
|
|
660
|
+
const resolvedStop = stop === undefined ? undefined : path5.resolve(stop);
|
|
661
|
+
while (true) {
|
|
662
|
+
for (const target of targets) {
|
|
663
|
+
const candidate = path5.join(current, target);
|
|
664
|
+
try {
|
|
665
|
+
if (fs3.existsSync(candidate)) {
|
|
666
|
+
results.push(candidate);
|
|
667
|
+
}
|
|
668
|
+
} catch {}
|
|
669
|
+
}
|
|
670
|
+
if (resolvedStop !== undefined && current === resolvedStop)
|
|
671
|
+
break;
|
|
672
|
+
const parent = path5.dirname(current);
|
|
673
|
+
if (parent === current)
|
|
674
|
+
break;
|
|
675
|
+
current = parent;
|
|
676
|
+
}
|
|
677
|
+
return results;
|
|
678
|
+
}
|
|
679
|
+
function uniqueStrings(values) {
|
|
680
|
+
return Array.from(new Set(values));
|
|
681
|
+
}
|
|
682
|
+
function buildOpencodeConfigDirs(startDir, homeDir, gitRoot, globalConfigDir, opencodeConfigDirOverride) {
|
|
683
|
+
const dirs = [globalConfigDir];
|
|
684
|
+
dirs.push(...upWalk([".opencode"], startDir, gitRoot ?? startDir));
|
|
685
|
+
dirs.push(...upWalk([".opencode"], homeDir, homeDir));
|
|
686
|
+
if (opencodeConfigDirOverride !== undefined) {
|
|
687
|
+
dirs.push(opencodeConfigDirOverride);
|
|
688
|
+
}
|
|
689
|
+
return uniqueStrings(dirs);
|
|
690
|
+
}
|
|
691
|
+
function globSkillFiles(rootDir, subdirNames) {
|
|
692
|
+
const results = [];
|
|
693
|
+
for (const subdirName of subdirNames) {
|
|
694
|
+
const scanRoot = path5.join(rootDir, subdirName);
|
|
695
|
+
try {
|
|
696
|
+
if (!fs3.existsSync(scanRoot))
|
|
697
|
+
continue;
|
|
698
|
+
const entries = walkDir(scanRoot, {
|
|
699
|
+
maxDepth: 10,
|
|
700
|
+
filter: (entry) => !entry.isDirectory && entry.name === "SKILL.md"
|
|
701
|
+
});
|
|
702
|
+
for (const entry of entries) {
|
|
703
|
+
results.push(entry.path);
|
|
704
|
+
}
|
|
705
|
+
} catch {}
|
|
706
|
+
}
|
|
707
|
+
return results;
|
|
708
|
+
}
|
|
709
|
+
function toDiscoveredSkill(skillPath, rootId) {
|
|
710
|
+
let stat;
|
|
711
|
+
try {
|
|
712
|
+
stat = fs3.statSync(skillPath);
|
|
713
|
+
} catch {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (!stat.isFile())
|
|
717
|
+
return;
|
|
718
|
+
const frontmatter = extractFrontmatter(skillPath);
|
|
719
|
+
const name = frontmatter.name;
|
|
720
|
+
if (!name || !isValidSkillName(name))
|
|
721
|
+
return;
|
|
722
|
+
return {
|
|
723
|
+
name,
|
|
724
|
+
description: frontmatter.description,
|
|
725
|
+
frontmatter,
|
|
726
|
+
skillPath,
|
|
727
|
+
root: rootId
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function discoverSkills(options) {
|
|
731
|
+
const { startDir, homeDir, configDir, opencodeConfigDirOverride } = options;
|
|
732
|
+
const globalConfigDir = configDir ?? path5.join(homeDir, ".config/opencode");
|
|
733
|
+
const gitRoot = findGitWorktreeRoot(startDir);
|
|
734
|
+
const byName = new Map;
|
|
735
|
+
function upsertAll(skillPaths, rootId) {
|
|
736
|
+
for (const skillPath of skillPaths) {
|
|
737
|
+
const skill = toDiscoveredSkill(skillPath, rootId);
|
|
738
|
+
if (skill)
|
|
739
|
+
byName.set(skill.name, skill);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
upsertAll(globSkillFiles(homeDir, [".claude/skills"]), "global-claude");
|
|
743
|
+
upsertAll(globSkillFiles(homeDir, [".agents/skills"]), "global-agents");
|
|
744
|
+
const externalLevels = upWalk([".claude", ".agents"], startDir, gitRoot ?? startDir);
|
|
745
|
+
for (const levelDir of externalLevels) {
|
|
746
|
+
const isClaudeDir = path5.basename(levelDir) === ".claude";
|
|
747
|
+
const parentDir = path5.dirname(levelDir);
|
|
748
|
+
const subdirGlob = isClaudeDir ? ".claude/skills" : ".agents/skills";
|
|
749
|
+
const rootId = isClaudeDir ? "project-claude" : "project-agents";
|
|
750
|
+
upsertAll(globSkillFiles(parentDir, [subdirGlob]), rootId);
|
|
751
|
+
}
|
|
752
|
+
const configDirs = buildOpencodeConfigDirs(startDir, homeDir, gitRoot, globalConfigDir, opencodeConfigDirOverride);
|
|
753
|
+
for (const dir of configDirs) {
|
|
754
|
+
const rootId = dir === globalConfigDir ? "global-opencode-config" : "project-opencode";
|
|
755
|
+
upsertAll(globSkillFiles(dir, ["skill", "skills"]), rootId);
|
|
756
|
+
}
|
|
757
|
+
return Array.from(byName.values());
|
|
758
|
+
}
|
|
759
|
+
|
|
627
760
|
// src/lib/model-availability.ts
|
|
628
761
|
import { createHash } from "crypto";
|
|
629
|
-
import
|
|
762
|
+
import fs4 from "fs";
|
|
630
763
|
import os2 from "os";
|
|
631
|
-
import
|
|
764
|
+
import path6 from "path";
|
|
632
765
|
function emptyAvailability() {
|
|
633
766
|
return { status: "unknown", models: new Set };
|
|
634
767
|
}
|
|
@@ -638,8 +771,8 @@ var MODELS_JSON_FILENAME = "models.json";
|
|
|
638
771
|
var availabilityCache = new WeakMap;
|
|
639
772
|
function resolveCacheDir() {
|
|
640
773
|
const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
|
|
641
|
-
const cacheBase = xdgCacheHome &&
|
|
642
|
-
return
|
|
774
|
+
const cacheBase = xdgCacheHome && path6.isAbsolute(xdgCacheHome) ? xdgCacheHome : path6.join(os2.homedir(), ".cache");
|
|
775
|
+
return path6.join(cacheBase, "opencode");
|
|
643
776
|
}
|
|
644
777
|
function fastHash(input) {
|
|
645
778
|
return createHash("sha1").update(input).digest("hex");
|
|
@@ -658,7 +791,7 @@ function isProviderRecord(value) {
|
|
|
658
791
|
function readModelsFromCache(filePath) {
|
|
659
792
|
let fd;
|
|
660
793
|
try {
|
|
661
|
-
fd =
|
|
794
|
+
fd = fs4.openSync(filePath, "r");
|
|
662
795
|
} catch {
|
|
663
796
|
return null;
|
|
664
797
|
}
|
|
@@ -666,7 +799,7 @@ function readModelsFromCache(filePath) {
|
|
|
666
799
|
try {
|
|
667
800
|
let stat;
|
|
668
801
|
try {
|
|
669
|
-
stat =
|
|
802
|
+
stat = fs4.fstatSync(fd);
|
|
670
803
|
} catch {
|
|
671
804
|
return null;
|
|
672
805
|
}
|
|
@@ -681,7 +814,7 @@ function readModelsFromCache(filePath) {
|
|
|
681
814
|
const buffer = Buffer.alloc(stat.size);
|
|
682
815
|
let bytesRead;
|
|
683
816
|
try {
|
|
684
|
-
bytesRead =
|
|
817
|
+
bytesRead = fs4.readSync(fd, buffer, 0, stat.size, 0);
|
|
685
818
|
} catch {
|
|
686
819
|
return null;
|
|
687
820
|
}
|
|
@@ -690,7 +823,7 @@ function readModelsFromCache(filePath) {
|
|
|
690
823
|
raw = buffer.toString("utf8");
|
|
691
824
|
} finally {
|
|
692
825
|
try {
|
|
693
|
-
|
|
826
|
+
fs4.closeSync(fd);
|
|
694
827
|
} catch {}
|
|
695
828
|
}
|
|
696
829
|
if (raw.trim().length === 0)
|
|
@@ -717,14 +850,14 @@ function readFallbackCache() {
|
|
|
717
850
|
const cacheDir = resolveCacheDir();
|
|
718
851
|
const openCodeModelsUrl = process.env.OPENCODE_MODELS_URL?.trim();
|
|
719
852
|
if (openCodeModelsUrl) {
|
|
720
|
-
const urlDerivedPath =
|
|
853
|
+
const urlDerivedPath = path6.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
|
|
721
854
|
const urlResult = readModelsFromCache(urlDerivedPath);
|
|
722
855
|
if (urlResult !== null && urlResult.size > 0) {
|
|
723
856
|
return { status: "cache", models: urlResult };
|
|
724
857
|
}
|
|
725
858
|
return emptyAvailability();
|
|
726
859
|
}
|
|
727
|
-
const defaultPath =
|
|
860
|
+
const defaultPath = path6.join(cacheDir, MODELS_JSON_FILENAME);
|
|
728
861
|
const defaultResult = readModelsFromCache(defaultPath);
|
|
729
862
|
if (defaultResult !== null && defaultResult.size > 0) {
|
|
730
863
|
return { status: "cache", models: defaultResult };
|
|
@@ -1090,12 +1223,59 @@ function collectSkillsAsCommands(dir, disabledSkills) {
|
|
|
1090
1223
|
}
|
|
1091
1224
|
return commands;
|
|
1092
1225
|
}
|
|
1226
|
+
function loadDiscoveredSkillAsCommand(skill) {
|
|
1227
|
+
const description = formatSkillDescription(skill.description, skill.name);
|
|
1228
|
+
if (skill.frontmatter.disableModelInvocation === true) {
|
|
1229
|
+
const content = fs5.readFileSync(skill.skillPath, "utf8");
|
|
1230
|
+
const { body } = parseFrontmatter(content);
|
|
1231
|
+
return {
|
|
1232
|
+
template: wrapSkillTemplate(skill.skillPath, body),
|
|
1233
|
+
description
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
return {
|
|
1237
|
+
template: buildDiscoveredSkillShimTemplate(skill.name),
|
|
1238
|
+
description
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
function buildDiscoveredSkillShimTemplate(skillName) {
|
|
1242
|
+
return `Load the "${skillName}" skill using the skill tool, then follow its instructions to address this request:
|
|
1243
|
+
|
|
1244
|
+
<user-request>
|
|
1245
|
+
$ARGUMENTS
|
|
1246
|
+
</user-request>`;
|
|
1247
|
+
}
|
|
1248
|
+
function collectDiscoveredSkillsAsCommands(startDir, homeDir, configDir, opencodeConfigDirOverride) {
|
|
1249
|
+
const commands = {};
|
|
1250
|
+
let discovered;
|
|
1251
|
+
try {
|
|
1252
|
+
discovered = discoverSkills({
|
|
1253
|
+
startDir,
|
|
1254
|
+
homeDir,
|
|
1255
|
+
configDir,
|
|
1256
|
+
opencodeConfigDirOverride
|
|
1257
|
+
});
|
|
1258
|
+
} catch {
|
|
1259
|
+
return commands;
|
|
1260
|
+
}
|
|
1261
|
+
for (const skill of discovered) {
|
|
1262
|
+
if (skill.frontmatter.userInvocable === false)
|
|
1263
|
+
continue;
|
|
1264
|
+
try {
|
|
1265
|
+
commands[skill.name] = loadDiscoveredSkillAsCommand(skill);
|
|
1266
|
+
} catch {}
|
|
1267
|
+
}
|
|
1268
|
+
return commands;
|
|
1269
|
+
}
|
|
1093
1270
|
function collectEnabledSkillNames(dir, disabledSkills) {
|
|
1094
1271
|
const disabledSet = new Set(disabledSkills);
|
|
1095
1272
|
return findSkillsInDir(dir).filter((skillInfo) => !disabledSet.has(skillInfo.name)).map((skillInfo) => skillInfo.name);
|
|
1096
1273
|
}
|
|
1097
1274
|
function createConfigHandler(deps) {
|
|
1098
1275
|
const { directory, bundledSkillsDir, bundledAgentsDir: bundledAgentsDir2, bundledCommandsDir } = deps;
|
|
1276
|
+
const homeDir = deps.homeDir ?? os3.homedir();
|
|
1277
|
+
const opencodeConfigDir = deps.opencodeConfigDir ?? path7.join(homeDir, ".config/opencode");
|
|
1278
|
+
const opencodeConfigDirOverride = process.env.OPENCODE_CONFIG_DIR?.trim() ? process.env.OPENCODE_CONFIG_DIR : undefined;
|
|
1099
1279
|
return async (config) => {
|
|
1100
1280
|
const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
|
|
1101
1281
|
const existingAgents = { ...config.agent ?? {} };
|
|
@@ -1116,11 +1296,15 @@ function createConfigHandler(deps) {
|
|
|
1116
1296
|
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
1117
1297
|
const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, nativeAgents, resolvedOverlays, availabilitySet);
|
|
1118
1298
|
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
1299
|
+
const discoveredSkillCommands = systematicConfig.skills_as_commands !== false ? collectDiscoveredSkillsAsCommands(directory, homeDir, opencodeConfigDir, opencodeConfigDirOverride) : {};
|
|
1119
1300
|
const bundledAgentKeys = new Set(Object.keys(bundledAgents));
|
|
1120
1301
|
config.agent = mergeSystematicEntries(existingAgents, bundledAgents, (key, agent) => bundledAgentKeys.has(key) && isSystematicAgentConfig(agent));
|
|
1121
|
-
const emittedCommands = {
|
|
1122
|
-
|
|
1123
|
-
|
|
1302
|
+
const emittedCommands = {
|
|
1303
|
+
...bundledCommands,
|
|
1304
|
+
...bundledSkills,
|
|
1305
|
+
...discoveredSkillCommands
|
|
1306
|
+
};
|
|
1307
|
+
config.command = mergeSystematicEntries(existingCommands, emittedCommands, (_key, command) => isSystematicCommandConfig(command));
|
|
1124
1308
|
registerSkillsPaths(config, bundledSkillsDir);
|
|
1125
1309
|
};
|
|
1126
1310
|
}
|
|
@@ -1136,22 +1320,19 @@ function registerSkillsPaths(config, skillsDir) {
|
|
|
1136
1320
|
};
|
|
1137
1321
|
}
|
|
1138
1322
|
function removeSystematicSkillPaths(paths) {
|
|
1139
|
-
return paths.filter((
|
|
1323
|
+
return paths.filter((path8) => !isSystematicSkillPath(path8));
|
|
1140
1324
|
}
|
|
1141
|
-
function isSystematicSkillPath(
|
|
1142
|
-
const normalizedPath = normalizePath(
|
|
1325
|
+
function isSystematicSkillPath(path8) {
|
|
1326
|
+
const normalizedPath = normalizePath(path8);
|
|
1143
1327
|
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);
|
|
1144
1328
|
}
|
|
1145
|
-
function normalizePath(
|
|
1146
|
-
return
|
|
1147
|
-
}
|
|
1148
|
-
function isSystematicOwnedCommandKey(key) {
|
|
1149
|
-
return key.startsWith("systematic:") || key.startsWith("ce:");
|
|
1329
|
+
function normalizePath(path8) {
|
|
1330
|
+
return path8.replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
1150
1331
|
}
|
|
1151
1332
|
|
|
1152
1333
|
// src/lib/skill-tool.ts
|
|
1153
|
-
import
|
|
1154
|
-
import
|
|
1334
|
+
import fs6 from "fs";
|
|
1335
|
+
import path8 from "path";
|
|
1155
1336
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1156
1337
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
1157
1338
|
function discoverSkillFiles(dir, limit = 10) {
|
|
@@ -1165,17 +1346,17 @@ function discoverSkillFiles(dir, limit = 10) {
|
|
|
1165
1346
|
function handleEntry(entry, currentDir) {
|
|
1166
1347
|
if (entry.isDirectory()) {
|
|
1167
1348
|
if (!shouldSkipDirectory(entry.name)) {
|
|
1168
|
-
recurse(
|
|
1349
|
+
recurse(path8.resolve(currentDir, entry.name));
|
|
1169
1350
|
}
|
|
1170
1351
|
} else if (shouldIncludeFile(entry.name)) {
|
|
1171
|
-
files.push(
|
|
1352
|
+
files.push(path8.resolve(currentDir, entry.name));
|
|
1172
1353
|
}
|
|
1173
1354
|
}
|
|
1174
1355
|
function recurse(currentDir) {
|
|
1175
1356
|
if (files.length >= limit)
|
|
1176
1357
|
return;
|
|
1177
1358
|
try {
|
|
1178
|
-
const entries =
|
|
1359
|
+
const entries = fs6.readdirSync(currentDir, { withFileTypes: true });
|
|
1179
1360
|
for (const entry of entries) {
|
|
1180
1361
|
if (files.length >= limit)
|
|
1181
1362
|
break;
|
|
@@ -1261,7 +1442,7 @@ ${catalog}`;
|
|
|
1261
1442
|
warnedSkills.add(matchedSkill.name);
|
|
1262
1443
|
}
|
|
1263
1444
|
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
1264
|
-
const dir =
|
|
1445
|
+
const dir = path8.dirname(matchedSkill.skillFile);
|
|
1265
1446
|
const base = pathToFileURL2(dir).href;
|
|
1266
1447
|
const files = discoverSkillFiles(dir);
|
|
1267
1448
|
await context.ask({
|
|
@@ -1298,17 +1479,17 @@ ${catalog}`;
|
|
|
1298
1479
|
}
|
|
1299
1480
|
|
|
1300
1481
|
// src/index.ts
|
|
1301
|
-
var __dirname3 =
|
|
1302
|
-
var packageRoot2 =
|
|
1303
|
-
var bundledSkillsDir =
|
|
1304
|
-
var bundledAgentsDir2 =
|
|
1305
|
-
var bundledCommandsDir =
|
|
1306
|
-
var packageJsonPath =
|
|
1482
|
+
var __dirname3 = path9.dirname(fileURLToPath2(import.meta.url));
|
|
1483
|
+
var packageRoot2 = path9.resolve(__dirname3, "..");
|
|
1484
|
+
var bundledSkillsDir = path9.join(packageRoot2, "skills");
|
|
1485
|
+
var bundledAgentsDir2 = path9.join(packageRoot2, "agents");
|
|
1486
|
+
var bundledCommandsDir = path9.join(packageRoot2, "commands");
|
|
1487
|
+
var packageJsonPath = path9.join(packageRoot2, "package.json");
|
|
1307
1488
|
var getPackageVersion = () => {
|
|
1308
1489
|
try {
|
|
1309
|
-
if (!
|
|
1490
|
+
if (!fs7.existsSync(packageJsonPath))
|
|
1310
1491
|
return "unknown";
|
|
1311
|
-
const content =
|
|
1492
|
+
const content = fs7.readFileSync(packageJsonPath, "utf8");
|
|
1312
1493
|
const parsed = JSON.parse(content);
|
|
1313
1494
|
return parsed.version ?? "unknown";
|
|
1314
1495
|
} catch {
|
|
@@ -7,6 +7,10 @@ export interface ConfigHandlerDeps {
|
|
|
7
7
|
bundledCommandsDir: string;
|
|
8
8
|
/** OpenCode client for availability lookup. When omitted, availability falls back to empty set (last-resort resolution). */
|
|
9
9
|
client?: OpencodeClientLike;
|
|
10
|
+
/** Home directory for discovered-skill lookups. Defaults to `os.homedir()`; inject a temp dir in tests. */
|
|
11
|
+
homeDir?: string;
|
|
12
|
+
/** OpenCode global config directory override for discovered-skill lookups. Defaults to `<homeDir>/.config/opencode`. */
|
|
13
|
+
opencodeConfigDir?: string;
|
|
10
14
|
}
|
|
11
15
|
export declare function toTitleCase(name: string): string;
|
|
12
16
|
export declare function formatAgentDescription(name: string, description: string | undefined): string;
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type SkillFrontmatter } from './skills.js';
|
|
2
|
+
/**
|
|
3
|
+
* Provenance ids for discovered skills. When the same skill `name` is found
|
|
4
|
+
* in multiple roots, the winner is whichever root is discovered LAST in
|
|
5
|
+
* upstream's sequence (mirrors upstream `skill/index.ts`'s last-write-wins
|
|
6
|
+
* map keyed by frontmatter name). Discovery order (earliest to latest):
|
|
7
|
+
* global-claude, global-agents, project-claude/project-agents (walked from
|
|
8
|
+
* startDir up to the worktree root, closest-first), then all
|
|
9
|
+
* `.opencode`-style config directories (global-opencode-config wins over
|
|
10
|
+
* everything above it). Do not reorder without re-verifying against
|
|
11
|
+
* upstream.
|
|
12
|
+
*/
|
|
13
|
+
type DiscoveryRootId = 'global-claude' | 'global-agents' | 'project-claude' | 'project-agents' | 'project-opencode' | 'global-opencode-config';
|
|
14
|
+
export interface DiscoveredSkill {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
frontmatter: SkillFrontmatter;
|
|
18
|
+
skillPath: string;
|
|
19
|
+
root: DiscoveryRootId;
|
|
20
|
+
}
|
|
21
|
+
export interface DiscoverSkillsOptions {
|
|
22
|
+
/** Directory to start the upward worktree walk from (typically the project cwd). */
|
|
23
|
+
startDir: string;
|
|
24
|
+
/** Home directory, injected so tests can use a temp dir instead of the real one. */
|
|
25
|
+
homeDir: string;
|
|
26
|
+
/**
|
|
27
|
+
* Override for OpenCode's global config directory (mirrors
|
|
28
|
+
* `$XDG_CONFIG_HOME` resolution). Defaults to `<homeDir>/.config/opencode`
|
|
29
|
+
* when omitted.
|
|
30
|
+
*/
|
|
31
|
+
configDir?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Override mirroring upstream's `OPENCODE_CONFIG_DIR` env var: an extra
|
|
34
|
+
* config directory appended to the end of the OpenCode config-dir list
|
|
35
|
+
* (so it wins over every other root, including the default global config
|
|
36
|
+
* dir). Injected as a param rather than read from `process.env` to keep
|
|
37
|
+
* discovery pure and testable.
|
|
38
|
+
*/
|
|
39
|
+
opencodeConfigDirOverride?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Discover user/project skills, replicating OpenCode v1.17.6's real
|
|
43
|
+
* discovery algorithm (verified from source: `skill/index.ts`,
|
|
44
|
+
* `config/paths.ts`, `util/filesystem.ts`). Builds ONE map keyed by
|
|
45
|
+
* frontmatter skill `name`; entries are scanned and upserted in upstream's
|
|
46
|
+
* exact sequence, and later additions overwrite earlier ones:
|
|
47
|
+
*
|
|
48
|
+
* 1. Global external: `<home>/.claude/skills/**\/SKILL.md`, then
|
|
49
|
+
* `<home>/.agents/skills/**\/SKILL.md`.
|
|
50
|
+
* 2. Project external (multi-level up-walk): from `startDir` up to the
|
|
51
|
+
* git worktree root (inclusive), closest-first; at each level scan
|
|
52
|
+
* `.claude/skills/**\/SKILL.md` then `.agents/skills/**\/SKILL.md`.
|
|
53
|
+
* Because of last-write-wins, the worktree-root level wins over
|
|
54
|
+
* deeper subdirectories.
|
|
55
|
+
* 3. OpenCode config dirs (`ConfigPaths.directories`-equivalent: global
|
|
56
|
+
* config dir, then project `.opencode` up-walk, then `<home>/.opencode`,
|
|
57
|
+
* then an optional `OPENCODE_CONFIG_DIR`-style override), each scanned
|
|
58
|
+
* for `{skill,skills}/**\/SKILL.md`. Scanned last, so these beat
|
|
59
|
+
* everything above.
|
|
60
|
+
*
|
|
61
|
+
* The dedup key is the frontmatter `name` (not the containing directory
|
|
62
|
+
* name); entries with no name, or a name failing the charset/length regex,
|
|
63
|
+
* are skipped. Never throws: unreadable dirs/files, missing roots, or
|
|
64
|
+
* malformed frontmatter cause that entry to be skipped, not an abort.
|
|
65
|
+
*/
|
|
66
|
+
export declare function discoverSkills(options: DiscoverSkillsOptions): DiscoveredSkill[];
|
|
67
|
+
export {};
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"bootstrap": {
|
|
24
24
|
"$ref": "#/definitions/__schema51"
|
|
25
|
+
},
|
|
26
|
+
"skills_as_commands": {
|
|
27
|
+
"$ref": "#/definitions/__schema57"
|
|
25
28
|
}
|
|
26
29
|
},
|
|
27
30
|
"additionalProperties": false,
|
|
@@ -1504,6 +1507,19 @@
|
|
|
1504
1507
|
},
|
|
1505
1508
|
"__schema56": {
|
|
1506
1509
|
"type": "string"
|
|
1510
|
+
},
|
|
1511
|
+
"__schema57": {
|
|
1512
|
+
"default": true,
|
|
1513
|
+
"description": "Register skills discovered from user/project skill directories (OpenCode config and other agent-harness-standard locations) as slash commands. Default true.",
|
|
1514
|
+
"examples": [true, false],
|
|
1515
|
+
"allOf": [
|
|
1516
|
+
{
|
|
1517
|
+
"$ref": "#/definitions/__schema58"
|
|
1518
|
+
}
|
|
1519
|
+
]
|
|
1520
|
+
},
|
|
1521
|
+
"__schema58": {
|
|
1522
|
+
"type": "boolean"
|
|
1507
1523
|
}
|
|
1508
1524
|
}
|
|
1509
1525
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.33.0",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://fro.bot/systematic",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"@opencode-ai/plugin": "^1.1.30"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@biomejs/biome": "2.
|
|
69
|
-
"@opencode-ai/plugin": "1.17.
|
|
70
|
-
"@opencode-ai/sdk": "1.17.
|
|
68
|
+
"@biomejs/biome": "2.5.2",
|
|
69
|
+
"@opencode-ai/plugin": "1.17.13",
|
|
70
|
+
"@opencode-ai/sdk": "1.17.13",
|
|
71
71
|
"@semantic-release/exec": "7.1.0",
|
|
72
72
|
"@types/bun": "latest",
|
|
73
73
|
"@types/js-yaml": "4.0.9",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
google-genai>=1.0.0
|
|
2
|
-
Pillow>=
|
|
2
|
+
Pillow>=12.2.0
|