@fro.bot/systematic 2.32.2 → 2.33.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/dist/cli.js +1 -1
- package/dist/{index-wjkgb2gb.js → index-vyzhzvap.js} +65 -47
- package/dist/index.js +218 -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 +70 -0
- package/dist/lib/skills.d.ts +6 -0
- package/dist/schemas/systematic-config.schema.json +16 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -485,53 +485,65 @@ 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
|
+
}
|
|
518
|
+
function extractFrontmatterFromContent(content) {
|
|
519
|
+
const { data, parseError } = parseFrontmatter(content);
|
|
520
|
+
if (parseError) {
|
|
521
|
+
return { name: "", description: "" };
|
|
522
|
+
}
|
|
523
|
+
const metadata = parseMetadata(data);
|
|
524
|
+
const deprecated = parseDeprecated(data);
|
|
525
|
+
const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
|
|
526
|
+
const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
|
|
527
|
+
return {
|
|
528
|
+
name: extractString(data, "name"),
|
|
529
|
+
description: extractString(data, "description"),
|
|
530
|
+
license: extractNonEmptyString(data, "license"),
|
|
531
|
+
compatibility: extractNonEmptyString(data, "compatibility"),
|
|
532
|
+
metadata,
|
|
533
|
+
deprecated,
|
|
534
|
+
disableModelInvocation: extractBoolean(data, "disable-model-invocation"),
|
|
535
|
+
userInvocable: extractBoolean(data, "user-invocable"),
|
|
536
|
+
subtask: data.context === "fork" ? true : extractBoolean(data, "subtask") ?? undefined,
|
|
537
|
+
agent: extractNonEmptyString(data, "agent"),
|
|
538
|
+
model: extractNonEmptyString(data, "model"),
|
|
539
|
+
argumentHint: argumentHint !== "" ? argumentHint : undefined,
|
|
540
|
+
allowedTools: extractNonEmptyString(data, "allowed-tools")
|
|
541
|
+
};
|
|
542
|
+
}
|
|
488
543
|
function extractFrontmatter(filePath) {
|
|
489
544
|
try {
|
|
490
545
|
const content = fs3.readFileSync(filePath, "utf8");
|
|
491
|
-
|
|
492
|
-
if (parseError) {
|
|
493
|
-
return { name: "", description: "" };
|
|
494
|
-
}
|
|
495
|
-
const metadataRaw = data.metadata;
|
|
496
|
-
let metadata;
|
|
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
|
-
}
|
|
518
|
-
const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
|
|
519
|
-
const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
|
|
520
|
-
return {
|
|
521
|
-
name: extractString(data, "name"),
|
|
522
|
-
description: extractString(data, "description"),
|
|
523
|
-
license: extractNonEmptyString(data, "license"),
|
|
524
|
-
compatibility: extractNonEmptyString(data, "compatibility"),
|
|
525
|
-
metadata,
|
|
526
|
-
deprecated,
|
|
527
|
-
disableModelInvocation: extractBoolean(data, "disable-model-invocation"),
|
|
528
|
-
userInvocable: extractBoolean(data, "user-invocable"),
|
|
529
|
-
subtask: data.context === "fork" ? true : extractBoolean(data, "subtask") ?? undefined,
|
|
530
|
-
agent: extractNonEmptyString(data, "agent"),
|
|
531
|
-
model: extractNonEmptyString(data, "model"),
|
|
532
|
-
argumentHint: argumentHint !== "" ? argumentHint : undefined,
|
|
533
|
-
allowedTools: extractNonEmptyString(data, "allowed-tools")
|
|
534
|
-
};
|
|
546
|
+
return extractFrontmatterFromContent(content);
|
|
535
547
|
} catch {
|
|
536
548
|
return { name: "", description: "" };
|
|
537
549
|
}
|
|
@@ -16040,6 +16052,10 @@ function createSystematicConfigSchema(opts) {
|
|
|
16040
16052
|
{ enabled: true },
|
|
16041
16053
|
{ enabled: false, file: ".opencode/custom-prompt.md" }
|
|
16042
16054
|
]
|
|
16055
|
+
}),
|
|
16056
|
+
skills_as_commands: exports_external.boolean().default(true).meta({
|
|
16057
|
+
description: "Register skills discovered from user/project skill directories (OpenCode config and other agent-harness-standard locations) as slash commands. Default true.",
|
|
16058
|
+
examples: [true, false]
|
|
16043
16059
|
})
|
|
16044
16060
|
}).strict().meta({
|
|
16045
16061
|
description: "Systematic user configuration file (systematic.json / systematic.jsonc)",
|
|
@@ -16071,7 +16087,8 @@ var DEFAULT_CONFIG = {
|
|
|
16071
16087
|
enabled: true
|
|
16072
16088
|
},
|
|
16073
16089
|
agents: {},
|
|
16074
|
-
categories: {}
|
|
16090
|
+
categories: {},
|
|
16091
|
+
skills_as_commands: true
|
|
16075
16092
|
};
|
|
16076
16093
|
var SECURITY_OVERLAY_FIELDS2 = new Set(SECURITY_OVERLAY_FIELDS);
|
|
16077
16094
|
var CURRENT_SKILL_NAMES_SET = new Set(BUNDLED_SKILL_NAMES);
|
|
@@ -16240,7 +16257,8 @@ function loadConfigWithSources(projectDir) {
|
|
|
16240
16257
|
...customConfig?.bootstrap
|
|
16241
16258
|
},
|
|
16242
16259
|
agents: overlayValues(overlays.agents),
|
|
16243
|
-
categories: overlayValues(overlays.categories)
|
|
16260
|
+
categories: overlayValues(overlays.categories),
|
|
16261
|
+
skills_as_commands: customConfig?.skills_as_commands ?? projectConfig?.skills_as_commands ?? userConfig?.skills_as_commands ?? DEFAULT_CONFIG.skills_as_commands
|
|
16244
16262
|
};
|
|
16245
16263
|
const warned = new Set;
|
|
16246
16264
|
const droppedSkills = computeDroppedNames(result.disabled_skills, CURRENT_SKILL_NAMES_SET);
|
|
@@ -16407,4 +16425,4 @@ function extractCommandFrontmatter(content) {
|
|
|
16407
16425
|
};
|
|
16408
16426
|
}
|
|
16409
16427
|
|
|
16410
|
-
export { parseFrontmatter, isRecord, convertContent, convertFileWithCache, findSkillsInDir, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter };
|
|
16428
|
+
export { parseFrontmatter, isRecord, convertContent, convertFileWithCache, walkDir, extractFrontmatterFromContent, 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
|
+
extractFrontmatterFromContent,
|
|
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-vyzhzvap.js";
|
|
17
19
|
|
|
18
20
|
// src/index.ts
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
+
import fs6 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,10 @@ ${skillUsage}${catalogSection}
|
|
|
237
239
|
</SYSTEMATIC_WORKFLOWS>`;
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
// src/lib/config-handler.ts
|
|
243
|
+
import os3 from "os";
|
|
244
|
+
import path7 from "path";
|
|
245
|
+
|
|
240
246
|
// src/lib/agent-overlays.ts
|
|
241
247
|
import fs2 from "fs";
|
|
242
248
|
import path4 from "path";
|
|
@@ -624,11 +630,136 @@ function throwConfigError(sourcePath, keyPath, message) {
|
|
|
624
630
|
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
|
|
625
631
|
}
|
|
626
632
|
|
|
633
|
+
// src/lib/discovered-skills.ts
|
|
634
|
+
import fs3 from "fs";
|
|
635
|
+
import path5 from "path";
|
|
636
|
+
var SKILL_NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
637
|
+
function isValidSkillName(name) {
|
|
638
|
+
return name.length >= 1 && name.length <= 64 && SKILL_NAME_REGEX.test(name);
|
|
639
|
+
}
|
|
640
|
+
function findGitWorktreeRoot(startDir) {
|
|
641
|
+
let current = path5.resolve(startDir);
|
|
642
|
+
while (true) {
|
|
643
|
+
try {
|
|
644
|
+
if (fs3.existsSync(path5.join(current, ".git"))) {
|
|
645
|
+
return current;
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const parent = path5.dirname(current);
|
|
651
|
+
if (parent === current)
|
|
652
|
+
return null;
|
|
653
|
+
current = parent;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function upWalk(targets, start, stop) {
|
|
657
|
+
const results = [];
|
|
658
|
+
let current = path5.resolve(start);
|
|
659
|
+
const resolvedStop = stop === undefined ? undefined : path5.resolve(stop);
|
|
660
|
+
while (true) {
|
|
661
|
+
for (const target of targets) {
|
|
662
|
+
const candidate = path5.join(current, target);
|
|
663
|
+
try {
|
|
664
|
+
if (fs3.existsSync(candidate)) {
|
|
665
|
+
results.push(candidate);
|
|
666
|
+
}
|
|
667
|
+
} catch {}
|
|
668
|
+
}
|
|
669
|
+
if (resolvedStop !== undefined && current === resolvedStop)
|
|
670
|
+
break;
|
|
671
|
+
const parent = path5.dirname(current);
|
|
672
|
+
if (parent === current)
|
|
673
|
+
break;
|
|
674
|
+
current = parent;
|
|
675
|
+
}
|
|
676
|
+
return results;
|
|
677
|
+
}
|
|
678
|
+
function uniqueStrings(values) {
|
|
679
|
+
return Array.from(new Set(values));
|
|
680
|
+
}
|
|
681
|
+
function buildOpencodeConfigDirs(startDir, homeDir, gitRoot, globalConfigDir, opencodeConfigDirOverride) {
|
|
682
|
+
const dirs = [globalConfigDir];
|
|
683
|
+
dirs.push(...upWalk([".opencode"], startDir, gitRoot ?? startDir));
|
|
684
|
+
dirs.push(...upWalk([".opencode"], homeDir, homeDir));
|
|
685
|
+
if (opencodeConfigDirOverride !== undefined) {
|
|
686
|
+
dirs.push(opencodeConfigDirOverride);
|
|
687
|
+
}
|
|
688
|
+
return uniqueStrings(dirs);
|
|
689
|
+
}
|
|
690
|
+
function globSkillFiles(rootDir, subdirNames) {
|
|
691
|
+
const results = [];
|
|
692
|
+
for (const subdirName of subdirNames) {
|
|
693
|
+
const scanRoot = path5.join(rootDir, subdirName);
|
|
694
|
+
try {
|
|
695
|
+
if (!fs3.existsSync(scanRoot))
|
|
696
|
+
continue;
|
|
697
|
+
const entries = walkDir(scanRoot, {
|
|
698
|
+
maxDepth: 10,
|
|
699
|
+
filter: (entry) => !entry.isDirectory && entry.name === "SKILL.md"
|
|
700
|
+
});
|
|
701
|
+
for (const entry of entries) {
|
|
702
|
+
results.push(entry.path);
|
|
703
|
+
}
|
|
704
|
+
} catch {}
|
|
705
|
+
}
|
|
706
|
+
return results;
|
|
707
|
+
}
|
|
708
|
+
function toDiscoveredSkill(skillPath, rootId) {
|
|
709
|
+
let content;
|
|
710
|
+
try {
|
|
711
|
+
content = fs3.readFileSync(skillPath, "utf8");
|
|
712
|
+
} catch {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const frontmatter = extractFrontmatterFromContent(content);
|
|
716
|
+
const name = frontmatter.name;
|
|
717
|
+
if (!name || !isValidSkillName(name))
|
|
718
|
+
return;
|
|
719
|
+
return {
|
|
720
|
+
name,
|
|
721
|
+
description: frontmatter.description,
|
|
722
|
+
frontmatter,
|
|
723
|
+
body: parseFrontmatter(content).body,
|
|
724
|
+
skillPath,
|
|
725
|
+
root: rootId
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function discoverSkills(options) {
|
|
729
|
+
const { startDir, homeDir, configDir, opencodeConfigDirOverride } = options;
|
|
730
|
+
const globalConfigDir = configDir ?? path5.join(homeDir, ".config/opencode");
|
|
731
|
+
const gitRoot = findGitWorktreeRoot(startDir);
|
|
732
|
+
const byName = new Map;
|
|
733
|
+
function upsertAll(skillPaths, rootId) {
|
|
734
|
+
for (const skillPath of skillPaths) {
|
|
735
|
+
const skill = toDiscoveredSkill(skillPath, rootId);
|
|
736
|
+
if (skill)
|
|
737
|
+
byName.set(skill.name, skill);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
upsertAll(globSkillFiles(homeDir, [".claude/skills"]), "global-claude");
|
|
741
|
+
upsertAll(globSkillFiles(homeDir, [".agents/skills"]), "global-agents");
|
|
742
|
+
const externalLevels = upWalk([".claude", ".agents"], startDir, gitRoot ?? startDir);
|
|
743
|
+
for (const levelDir of externalLevels) {
|
|
744
|
+
const isClaudeDir = path5.basename(levelDir) === ".claude";
|
|
745
|
+
const parentDir = path5.dirname(levelDir);
|
|
746
|
+
const subdirGlob = isClaudeDir ? ".claude/skills" : ".agents/skills";
|
|
747
|
+
const rootId = isClaudeDir ? "project-claude" : "project-agents";
|
|
748
|
+
upsertAll(globSkillFiles(parentDir, [subdirGlob]), rootId);
|
|
749
|
+
}
|
|
750
|
+
const configDirs = buildOpencodeConfigDirs(startDir, homeDir, gitRoot, globalConfigDir, opencodeConfigDirOverride);
|
|
751
|
+
for (const dir of configDirs) {
|
|
752
|
+
const rootId = dir === globalConfigDir ? "global-opencode-config" : "project-opencode";
|
|
753
|
+
upsertAll(globSkillFiles(dir, ["skill", "skills"]), rootId);
|
|
754
|
+
}
|
|
755
|
+
return Array.from(byName.values());
|
|
756
|
+
}
|
|
757
|
+
|
|
627
758
|
// src/lib/model-availability.ts
|
|
628
759
|
import { createHash } from "crypto";
|
|
629
|
-
import
|
|
760
|
+
import fs4 from "fs";
|
|
630
761
|
import os2 from "os";
|
|
631
|
-
import
|
|
762
|
+
import path6 from "path";
|
|
632
763
|
function emptyAvailability() {
|
|
633
764
|
return { status: "unknown", models: new Set };
|
|
634
765
|
}
|
|
@@ -638,8 +769,8 @@ var MODELS_JSON_FILENAME = "models.json";
|
|
|
638
769
|
var availabilityCache = new WeakMap;
|
|
639
770
|
function resolveCacheDir() {
|
|
640
771
|
const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
|
|
641
|
-
const cacheBase = xdgCacheHome &&
|
|
642
|
-
return
|
|
772
|
+
const cacheBase = xdgCacheHome && path6.isAbsolute(xdgCacheHome) ? xdgCacheHome : path6.join(os2.homedir(), ".cache");
|
|
773
|
+
return path6.join(cacheBase, "opencode");
|
|
643
774
|
}
|
|
644
775
|
function fastHash(input) {
|
|
645
776
|
return createHash("sha1").update(input).digest("hex");
|
|
@@ -658,7 +789,7 @@ function isProviderRecord(value) {
|
|
|
658
789
|
function readModelsFromCache(filePath) {
|
|
659
790
|
let fd;
|
|
660
791
|
try {
|
|
661
|
-
fd =
|
|
792
|
+
fd = fs4.openSync(filePath, "r");
|
|
662
793
|
} catch {
|
|
663
794
|
return null;
|
|
664
795
|
}
|
|
@@ -666,7 +797,7 @@ function readModelsFromCache(filePath) {
|
|
|
666
797
|
try {
|
|
667
798
|
let stat;
|
|
668
799
|
try {
|
|
669
|
-
stat =
|
|
800
|
+
stat = fs4.fstatSync(fd);
|
|
670
801
|
} catch {
|
|
671
802
|
return null;
|
|
672
803
|
}
|
|
@@ -681,7 +812,7 @@ function readModelsFromCache(filePath) {
|
|
|
681
812
|
const buffer = Buffer.alloc(stat.size);
|
|
682
813
|
let bytesRead;
|
|
683
814
|
try {
|
|
684
|
-
bytesRead =
|
|
815
|
+
bytesRead = fs4.readSync(fd, buffer, 0, stat.size, 0);
|
|
685
816
|
} catch {
|
|
686
817
|
return null;
|
|
687
818
|
}
|
|
@@ -690,7 +821,7 @@ function readModelsFromCache(filePath) {
|
|
|
690
821
|
raw = buffer.toString("utf8");
|
|
691
822
|
} finally {
|
|
692
823
|
try {
|
|
693
|
-
|
|
824
|
+
fs4.closeSync(fd);
|
|
694
825
|
} catch {}
|
|
695
826
|
}
|
|
696
827
|
if (raw.trim().length === 0)
|
|
@@ -717,14 +848,14 @@ function readFallbackCache() {
|
|
|
717
848
|
const cacheDir = resolveCacheDir();
|
|
718
849
|
const openCodeModelsUrl = process.env.OPENCODE_MODELS_URL?.trim();
|
|
719
850
|
if (openCodeModelsUrl) {
|
|
720
|
-
const urlDerivedPath =
|
|
851
|
+
const urlDerivedPath = path6.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
|
|
721
852
|
const urlResult = readModelsFromCache(urlDerivedPath);
|
|
722
853
|
if (urlResult !== null && urlResult.size > 0) {
|
|
723
854
|
return { status: "cache", models: urlResult };
|
|
724
855
|
}
|
|
725
856
|
return emptyAvailability();
|
|
726
857
|
}
|
|
727
|
-
const defaultPath =
|
|
858
|
+
const defaultPath = path6.join(cacheDir, MODELS_JSON_FILENAME);
|
|
728
859
|
const defaultResult = readModelsFromCache(defaultPath);
|
|
729
860
|
if (defaultResult !== null && defaultResult.size > 0) {
|
|
730
861
|
return { status: "cache", models: defaultResult };
|
|
@@ -1090,12 +1221,59 @@ function collectSkillsAsCommands(dir, disabledSkills) {
|
|
|
1090
1221
|
}
|
|
1091
1222
|
return commands;
|
|
1092
1223
|
}
|
|
1224
|
+
function loadDiscoveredSkillAsCommand(skill) {
|
|
1225
|
+
const description = formatSkillDescription(skill.description, skill.name);
|
|
1226
|
+
if (skill.frontmatter.disableModelInvocation === true) {
|
|
1227
|
+
return {
|
|
1228
|
+
template: wrapSkillTemplate(skill.skillPath, skill.body),
|
|
1229
|
+
description
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
template: buildDiscoveredSkillShimTemplate(skill.name),
|
|
1234
|
+
description
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function buildDiscoveredSkillShimTemplate(skillName) {
|
|
1238
|
+
return `Load the "${skillName}" skill using the skill tool, then follow its instructions to address this request:
|
|
1239
|
+
|
|
1240
|
+
<user-request>
|
|
1241
|
+
$ARGUMENTS
|
|
1242
|
+
</user-request>`;
|
|
1243
|
+
}
|
|
1244
|
+
function collectDiscoveredSkillsAsCommands(startDir, homeDir, configDir, opencodeConfigDirOverride, disabledCommands) {
|
|
1245
|
+
const commands = {};
|
|
1246
|
+
let discovered;
|
|
1247
|
+
try {
|
|
1248
|
+
discovered = discoverSkills({
|
|
1249
|
+
startDir,
|
|
1250
|
+
homeDir,
|
|
1251
|
+
configDir,
|
|
1252
|
+
opencodeConfigDirOverride
|
|
1253
|
+
});
|
|
1254
|
+
} catch {
|
|
1255
|
+
return commands;
|
|
1256
|
+
}
|
|
1257
|
+
for (const skill of discovered) {
|
|
1258
|
+
if (skill.frontmatter.userInvocable === false)
|
|
1259
|
+
continue;
|
|
1260
|
+
if (disabledCommands.includes(skill.name))
|
|
1261
|
+
continue;
|
|
1262
|
+
try {
|
|
1263
|
+
commands[skill.name] = loadDiscoveredSkillAsCommand(skill);
|
|
1264
|
+
} catch {}
|
|
1265
|
+
}
|
|
1266
|
+
return commands;
|
|
1267
|
+
}
|
|
1093
1268
|
function collectEnabledSkillNames(dir, disabledSkills) {
|
|
1094
1269
|
const disabledSet = new Set(disabledSkills);
|
|
1095
1270
|
return findSkillsInDir(dir).filter((skillInfo) => !disabledSet.has(skillInfo.name)).map((skillInfo) => skillInfo.name);
|
|
1096
1271
|
}
|
|
1097
1272
|
function createConfigHandler(deps) {
|
|
1098
1273
|
const { directory, bundledSkillsDir, bundledAgentsDir: bundledAgentsDir2, bundledCommandsDir } = deps;
|
|
1274
|
+
const homeDir = deps.homeDir ?? os3.homedir();
|
|
1275
|
+
const opencodeConfigDir = deps.opencodeConfigDir ?? path7.join(homeDir, ".config/opencode");
|
|
1276
|
+
const opencodeConfigDirOverride = process.env.OPENCODE_CONFIG_DIR?.trim() ? process.env.OPENCODE_CONFIG_DIR : undefined;
|
|
1099
1277
|
return async (config) => {
|
|
1100
1278
|
const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
|
|
1101
1279
|
const existingAgents = { ...config.agent ?? {} };
|
|
@@ -1116,11 +1294,15 @@ function createConfigHandler(deps) {
|
|
|
1116
1294
|
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
1117
1295
|
const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, nativeAgents, resolvedOverlays, availabilitySet);
|
|
1118
1296
|
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
1297
|
+
const discoveredSkillCommands = systematicConfig.skills_as_commands !== false ? collectDiscoveredSkillsAsCommands(directory, homeDir, opencodeConfigDir, opencodeConfigDirOverride, systematicConfig.disabled_commands) : {};
|
|
1119
1298
|
const bundledAgentKeys = new Set(Object.keys(bundledAgents));
|
|
1120
1299
|
config.agent = mergeSystematicEntries(existingAgents, bundledAgents, (key, agent) => bundledAgentKeys.has(key) && isSystematicAgentConfig(agent));
|
|
1121
|
-
const emittedCommands = {
|
|
1122
|
-
|
|
1123
|
-
|
|
1300
|
+
const emittedCommands = {
|
|
1301
|
+
...bundledCommands,
|
|
1302
|
+
...bundledSkills,
|
|
1303
|
+
...discoveredSkillCommands
|
|
1304
|
+
};
|
|
1305
|
+
config.command = mergeSystematicEntries(existingCommands, emittedCommands, (_key, command) => isSystematicCommandConfig(command));
|
|
1124
1306
|
registerSkillsPaths(config, bundledSkillsDir);
|
|
1125
1307
|
};
|
|
1126
1308
|
}
|
|
@@ -1136,22 +1318,19 @@ function registerSkillsPaths(config, skillsDir) {
|
|
|
1136
1318
|
};
|
|
1137
1319
|
}
|
|
1138
1320
|
function removeSystematicSkillPaths(paths) {
|
|
1139
|
-
return paths.filter((
|
|
1321
|
+
return paths.filter((path8) => !isSystematicSkillPath(path8));
|
|
1140
1322
|
}
|
|
1141
|
-
function isSystematicSkillPath(
|
|
1142
|
-
const normalizedPath = normalizePath(
|
|
1323
|
+
function isSystematicSkillPath(path8) {
|
|
1324
|
+
const normalizedPath = normalizePath(path8);
|
|
1143
1325
|
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
1326
|
}
|
|
1145
|
-
function normalizePath(
|
|
1146
|
-
return
|
|
1147
|
-
}
|
|
1148
|
-
function isSystematicOwnedCommandKey(key) {
|
|
1149
|
-
return key.startsWith("systematic:") || key.startsWith("ce:");
|
|
1327
|
+
function normalizePath(path8) {
|
|
1328
|
+
return path8.replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
1150
1329
|
}
|
|
1151
1330
|
|
|
1152
1331
|
// src/lib/skill-tool.ts
|
|
1153
|
-
import
|
|
1154
|
-
import
|
|
1332
|
+
import fs5 from "fs";
|
|
1333
|
+
import path8 from "path";
|
|
1155
1334
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1156
1335
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
1157
1336
|
function discoverSkillFiles(dir, limit = 10) {
|
|
@@ -1165,17 +1344,17 @@ function discoverSkillFiles(dir, limit = 10) {
|
|
|
1165
1344
|
function handleEntry(entry, currentDir) {
|
|
1166
1345
|
if (entry.isDirectory()) {
|
|
1167
1346
|
if (!shouldSkipDirectory(entry.name)) {
|
|
1168
|
-
recurse(
|
|
1347
|
+
recurse(path8.resolve(currentDir, entry.name));
|
|
1169
1348
|
}
|
|
1170
1349
|
} else if (shouldIncludeFile(entry.name)) {
|
|
1171
|
-
files.push(
|
|
1350
|
+
files.push(path8.resolve(currentDir, entry.name));
|
|
1172
1351
|
}
|
|
1173
1352
|
}
|
|
1174
1353
|
function recurse(currentDir) {
|
|
1175
1354
|
if (files.length >= limit)
|
|
1176
1355
|
return;
|
|
1177
1356
|
try {
|
|
1178
|
-
const entries =
|
|
1357
|
+
const entries = fs5.readdirSync(currentDir, { withFileTypes: true });
|
|
1179
1358
|
for (const entry of entries) {
|
|
1180
1359
|
if (files.length >= limit)
|
|
1181
1360
|
break;
|
|
@@ -1261,7 +1440,7 @@ ${catalog}`;
|
|
|
1261
1440
|
warnedSkills.add(matchedSkill.name);
|
|
1262
1441
|
}
|
|
1263
1442
|
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
1264
|
-
const dir =
|
|
1443
|
+
const dir = path8.dirname(matchedSkill.skillFile);
|
|
1265
1444
|
const base = pathToFileURL2(dir).href;
|
|
1266
1445
|
const files = discoverSkillFiles(dir);
|
|
1267
1446
|
await context.ask({
|
|
@@ -1298,17 +1477,17 @@ ${catalog}`;
|
|
|
1298
1477
|
}
|
|
1299
1478
|
|
|
1300
1479
|
// src/index.ts
|
|
1301
|
-
var __dirname3 =
|
|
1302
|
-
var packageRoot2 =
|
|
1303
|
-
var bundledSkillsDir =
|
|
1304
|
-
var bundledAgentsDir2 =
|
|
1305
|
-
var bundledCommandsDir =
|
|
1306
|
-
var packageJsonPath =
|
|
1480
|
+
var __dirname3 = path9.dirname(fileURLToPath2(import.meta.url));
|
|
1481
|
+
var packageRoot2 = path9.resolve(__dirname3, "..");
|
|
1482
|
+
var bundledSkillsDir = path9.join(packageRoot2, "skills");
|
|
1483
|
+
var bundledAgentsDir2 = path9.join(packageRoot2, "agents");
|
|
1484
|
+
var bundledCommandsDir = path9.join(packageRoot2, "commands");
|
|
1485
|
+
var packageJsonPath = path9.join(packageRoot2, "package.json");
|
|
1307
1486
|
var getPackageVersion = () => {
|
|
1308
1487
|
try {
|
|
1309
|
-
if (!
|
|
1488
|
+
if (!fs6.existsSync(packageJsonPath))
|
|
1310
1489
|
return "unknown";
|
|
1311
|
-
const content =
|
|
1490
|
+
const content = fs6.readFileSync(packageJsonPath, "utf8");
|
|
1312
1491
|
const parsed = JSON.parse(content);
|
|
1313
1492
|
return parsed.version ?? "unknown";
|
|
1314
1493
|
} 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,70 @@
|
|
|
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
|
+
/** SKILL.md body (frontmatter stripped), read once at discovery so callers
|
|
19
|
+
* that inline the body don't re-read the file. */
|
|
20
|
+
body: string;
|
|
21
|
+
skillPath: string;
|
|
22
|
+
root: DiscoveryRootId;
|
|
23
|
+
}
|
|
24
|
+
export interface DiscoverSkillsOptions {
|
|
25
|
+
/** Directory to start the upward worktree walk from (typically the project cwd). */
|
|
26
|
+
startDir: string;
|
|
27
|
+
/** Home directory, injected so tests can use a temp dir instead of the real one. */
|
|
28
|
+
homeDir: string;
|
|
29
|
+
/**
|
|
30
|
+
* Override for OpenCode's global config directory (mirrors
|
|
31
|
+
* `$XDG_CONFIG_HOME` resolution). Defaults to `<homeDir>/.config/opencode`
|
|
32
|
+
* when omitted.
|
|
33
|
+
*/
|
|
34
|
+
configDir?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Override mirroring upstream's `OPENCODE_CONFIG_DIR` env var: an extra
|
|
37
|
+
* config directory appended to the end of the OpenCode config-dir list
|
|
38
|
+
* (so it wins over every other root, including the default global config
|
|
39
|
+
* dir). Injected as a param rather than read from `process.env` to keep
|
|
40
|
+
* discovery pure and testable.
|
|
41
|
+
*/
|
|
42
|
+
opencodeConfigDirOverride?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Discover user/project skills, replicating OpenCode v1.17.6's real
|
|
46
|
+
* discovery algorithm (verified from source: `skill/index.ts`,
|
|
47
|
+
* `config/paths.ts`, `util/filesystem.ts`). Builds ONE map keyed by
|
|
48
|
+
* frontmatter skill `name`; entries are scanned and upserted in upstream's
|
|
49
|
+
* exact sequence, and later additions overwrite earlier ones:
|
|
50
|
+
*
|
|
51
|
+
* 1. Global external: `<home>/.claude/skills/**\/SKILL.md`, then
|
|
52
|
+
* `<home>/.agents/skills/**\/SKILL.md`.
|
|
53
|
+
* 2. Project external (multi-level up-walk): from `startDir` up to the
|
|
54
|
+
* git worktree root (inclusive), closest-first; at each level scan
|
|
55
|
+
* `.claude/skills/**\/SKILL.md` then `.agents/skills/**\/SKILL.md`.
|
|
56
|
+
* Because of last-write-wins, the worktree-root level wins over
|
|
57
|
+
* deeper subdirectories.
|
|
58
|
+
* 3. OpenCode config dirs (`ConfigPaths.directories`-equivalent: global
|
|
59
|
+
* config dir, then project `.opencode` up-walk, then `<home>/.opencode`,
|
|
60
|
+
* then an optional `OPENCODE_CONFIG_DIR`-style override), each scanned
|
|
61
|
+
* for `{skill,skills}/**\/SKILL.md`. Scanned last, so these beat
|
|
62
|
+
* everything above.
|
|
63
|
+
*
|
|
64
|
+
* The dedup key is the frontmatter `name` (not the containing directory
|
|
65
|
+
* name); entries with no name, or a name failing the charset/length regex,
|
|
66
|
+
* are skipped. Never throws: unreadable dirs/files, missing roots, or
|
|
67
|
+
* malformed frontmatter cause that entry to be skipped, not an abort.
|
|
68
|
+
*/
|
|
69
|
+
export declare function discoverSkills(options: DiscoverSkillsOptions): DiscoveredSkill[];
|
|
70
|
+
export {};
|
package/dist/lib/skills.d.ts
CHANGED
|
@@ -37,5 +37,11 @@ export interface SkillInfo {
|
|
|
37
37
|
allowedTools?: string;
|
|
38
38
|
}
|
|
39
39
|
export declare const SKILL_FRONTMATTER_FIELDS: readonly ["name", "description", "argument-hint", "disable-model-invocation", "allowed-tools", "license", "compatibility", "metadata", "deprecated", "user-invocable", "agent", "model", "context", "subtask"];
|
|
40
|
+
/**
|
|
41
|
+
* Parse skill frontmatter from already-read file content. Split out from
|
|
42
|
+
* `extractFrontmatter` so callers that also need the body (e.g. discovered-skill
|
|
43
|
+
* command emission) can read the file once and derive both. Never throws.
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractFrontmatterFromContent(content: string): SkillFrontmatter;
|
|
40
46
|
export declare function extractFrontmatter(filePath: string): SkillFrontmatter;
|
|
41
47
|
export declare function findSkillsInDir(dir: string, maxDepth?: number): SkillInfo[];
|
|
@@ -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
|
}
|