@fro.bot/systematic 3.6.1 → 3.7.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.
@@ -16355,6 +16355,13 @@ var SECURITY_OVERLAY_FIELDS = [
16355
16355
  ];
16356
16356
 
16357
16357
  // src/lib/config.ts
16358
+ var CONFIG_AUTHORITY_FIELD_PATHS = [
16359
+ "bootstrap.enabled",
16360
+ "bootstrap.file",
16361
+ "skills_as_commands",
16362
+ "workflow_guard.debug",
16363
+ "workflow_guard.mode"
16364
+ ];
16358
16365
  var DEFAULT_CONFIG = {
16359
16366
  disabled_skills: [],
16360
16367
  disabled_agents: [],
@@ -16371,7 +16378,20 @@ var DEFAULT_CONFIG = {
16371
16378
  pi_subagents: { categories: {}, agents: {} },
16372
16379
  skills_as_commands: true
16373
16380
  };
16374
- var SECURITY_OVERLAY_FIELDS2 = new Set(SECURITY_OVERLAY_FIELDS);
16381
+ var PROTECTED_OVERLAY_FIELD_PATHS = {
16382
+ agents: {
16383
+ model: "agents.*.model",
16384
+ permission: "agents.*.permission",
16385
+ skills: "agents.*.skills",
16386
+ variant: "agents.*.variant"
16387
+ },
16388
+ categories: {
16389
+ model: "categories.*.model",
16390
+ permission: "categories.*.permission",
16391
+ skills: "categories.*.skills",
16392
+ variant: "categories.*.variant"
16393
+ }
16394
+ };
16375
16395
  var PROJECT_PROTECTED_FIELDS = new Set(["workflow_guard"]);
16376
16396
  var CURRENT_SKILL_NAMES_SET = new Set(BUNDLED_SKILL_NAMES);
16377
16397
  var CURRENT_AGENT_NAMES_SET = new Set([
@@ -16383,14 +16403,14 @@ function computeDroppedNames(names, allowedSet) {
16383
16403
  return names.filter((n) => !allowedSet.has(n));
16384
16404
  }
16385
16405
  var MIGRATION_DOCS_URL = "https://fro.bot/systematic/guides/v3-migration/";
16386
- function warnDroppedNames(dropped, field, warned, removalVersion) {
16406
+ function warnDroppedNames(dropped, field, warned, removalVersion, warningSink = console.warn) {
16387
16407
  for (const name of dropped) {
16388
16408
  if (warned.has(name))
16389
16409
  continue;
16390
16410
  warned.add(name);
16391
16411
  const displayName = field === "categories" ? `${field}.${name}` : name;
16392
16412
  const removalNote = removalVersion ? ` It was removed in ${removalVersion}.` : "";
16393
- console.warn(`[systematic] "${displayName}" in \`${field}\` is no longer a bundled name and will be ignored.${removalNote} Remove it from your config to silence this warning. See ${MIGRATION_DOCS_URL} for migration guidance.`);
16413
+ warningSink(`[systematic] "${displayName}" in \`${field}\` is no longer a bundled name and will be ignored.${removalNote} Remove it from your config to silence this warning. See ${MIGRATION_DOCS_URL} for migration guidance.`);
16394
16414
  }
16395
16415
  }
16396
16416
  function resolveConfigPath(dir, basename) {
@@ -16496,16 +16516,91 @@ ${formatted.map((entry) => ` - ${entry}`).join(`
16496
16516
  issues
16497
16517
  });
16498
16518
  }
16499
- function loadConfigSource(filePath, trust) {
16500
- const rawConfig = loadJsoncFile(filePath);
16501
- if (!rawConfig)
16502
- return null;
16503
- const config2 = trust === "project" ? stripProjectProtectedFields(rawConfig) : rawConfig;
16504
- const result = SystematicConfigSchema.safeParse(config2);
16505
- if (!result.success) {
16506
- throwTopLevelConfigSchemaError(filePath, trust, result.error.issues, config2);
16519
+ function loadConfigSource(filePath, trust, invalidSource) {
16520
+ try {
16521
+ const rawConfig = loadJsoncFile(filePath);
16522
+ if (!rawConfig) {
16523
+ return {
16524
+ metadata: { kind: trust, presence: "absent" },
16525
+ source: null
16526
+ };
16527
+ }
16528
+ const protectedFields = collectProjectProtectedFields(rawConfig, trust);
16529
+ const config2 = trust === "project" ? stripProjectProtectedFields(rawConfig) : rawConfig;
16530
+ const result = SystematicConfigSchema.safeParse(config2);
16531
+ if (!result.success) {
16532
+ throwTopLevelConfigSchemaError(filePath, trust, result.error.issues, config2);
16533
+ }
16534
+ return {
16535
+ metadata: { kind: trust, presence: "present" },
16536
+ source: {
16537
+ canonicalPath: resolveConfigSourcePath(filePath),
16538
+ config: config2,
16539
+ path: filePath,
16540
+ protectedFields,
16541
+ trust
16542
+ }
16543
+ };
16544
+ } catch (error51) {
16545
+ if (invalidSource === "throw")
16546
+ throw error51;
16547
+ return {
16548
+ metadata: {
16549
+ errorCode: classifyConfigSourceError(error51),
16550
+ kind: trust,
16551
+ presence: "invalid"
16552
+ },
16553
+ source: null
16554
+ };
16507
16555
  }
16508
- return { path: filePath, config: config2, trust };
16556
+ }
16557
+ function classifyConfigSourceError(error51) {
16558
+ if (isConfigSchemaError(error51))
16559
+ return "schema-invalid";
16560
+ if (!(error51 instanceof Error))
16561
+ return "source-invalid";
16562
+ if (error51.message.includes("JSONC parse error"))
16563
+ return "parse-failed";
16564
+ if (error51.message.includes("unable to read file"))
16565
+ return "read-failed";
16566
+ return "source-invalid";
16567
+ }
16568
+ function isConfigSchemaError(error51) {
16569
+ return isRecord2(error51) && error51._tag === "ConfigSchemaError";
16570
+ }
16571
+ function resolveConfigSourcePath(filePath) {
16572
+ try {
16573
+ return fs3.realpathSync(filePath);
16574
+ } catch {
16575
+ return path3.resolve(filePath);
16576
+ }
16577
+ }
16578
+ function collectProjectProtectedFields(rawConfig, trust) {
16579
+ if (trust !== "project")
16580
+ return [];
16581
+ return [
16582
+ ...Object.hasOwn(rawConfig, "workflow_guard") ? [
16583
+ {
16584
+ fieldPath: "workflow_guard",
16585
+ outcome: "blocked",
16586
+ sourceKind: "project"
16587
+ }
16588
+ ] : [],
16589
+ ...collectOverlayProtectedFields(rawConfig.agents, "agents"),
16590
+ ...collectOverlayProtectedFields(rawConfig.categories, "categories")
16591
+ ];
16592
+ }
16593
+ function collectOverlayProtectedFields(overlayMap, mapKey) {
16594
+ if (!isRecord2(overlayMap))
16595
+ return [];
16596
+ return Object.values(overlayMap).flatMap((value) => isRecord2(value) ? collectProtectedOverlayValue(value, mapKey) : []);
16597
+ }
16598
+ function collectProtectedOverlayValue(value, mapKey) {
16599
+ return [...SECURITY_OVERLAY_FIELDS].filter((field) => Object.hasOwn(value, field)).map((field) => ({
16600
+ fieldPath: PROTECTED_OVERLAY_FIELD_PATHS[mapKey][field],
16601
+ outcome: "blocked",
16602
+ sourceKind: "project"
16603
+ }));
16509
16604
  }
16510
16605
  function stripProjectProtectedFields(rawConfig) {
16511
16606
  const config2 = { ...rawConfig };
@@ -16532,16 +16627,27 @@ function loadConfig(projectDir, options) {
16532
16627
  }
16533
16628
  function loadConfigWithSources(projectDir, options) {
16534
16629
  const includeProject = options?.includeProject ?? true;
16535
- const paths = getConfigPaths(projectDir);
16536
- const userSource = loadConfigSource(paths.userConfig, "user");
16537
- const projectSource = includeProject ? loadConfigSource(paths.projectConfig, "project") : null;
16538
- const customSource = paths.customConfig ? loadConfigSource(paths.customConfig, "custom") : null;
16630
+ const invalidSource = options?.invalidSource ?? "throw";
16631
+ const paths = getConfigPaths(projectDir, options);
16632
+ const warningSink = options?.warningSink ?? console.warn;
16633
+ const user = loadConfigSource(paths.userConfig, "user", invalidSource);
16634
+ const project = includeProject ? loadConfigSource(paths.projectConfig, "project", invalidSource) : {
16635
+ metadata: { kind: "project", presence: "absent" },
16636
+ source: null
16637
+ };
16638
+ const custom2 = paths.customConfig ? loadConfigSource(paths.customConfig, "custom", invalidSource) : {
16639
+ metadata: { kind: "custom", presence: "absent" },
16640
+ source: null
16641
+ };
16642
+ const userSource = user.source;
16643
+ const projectSource = project.source;
16644
+ const customSource = custom2.source;
16539
16645
  const sources = [userSource, projectSource, customSource].filter((source) => source !== null);
16540
16646
  const mergedOverlays = mergeOverlaySources(sources);
16541
16647
  const mergedPiSubagentsOverlays = mergePiSubagentsOverlaySources(sources);
16542
16648
  const droppedCategories = Object.keys(mergedOverlays.categories).filter((name) => REMOVED_AGENT_CATEGORIES_SET.has(name));
16543
16649
  const warned = new Set;
16544
- warnDroppedNames(droppedCategories, "categories", warned, "v3.0.0");
16650
+ warnDroppedNames(droppedCategories, "categories", warned, "v3.0.0", warningSink);
16545
16651
  const droppedCategorySet = new Set(droppedCategories);
16546
16652
  const overlays = droppedCategorySet.size === 0 ? mergedOverlays : {
16547
16653
  ...mergedOverlays,
@@ -16574,9 +16680,9 @@ function loadConfigWithSources(projectDir, options) {
16574
16680
  skills_as_commands: customConfig?.skills_as_commands ?? projectConfig?.skills_as_commands ?? userConfig?.skills_as_commands ?? DEFAULT_CONFIG.skills_as_commands
16575
16681
  };
16576
16682
  const droppedSkills = computeDroppedNames(result.disabled_skills, CURRENT_SKILL_NAMES_SET);
16577
- warnDroppedNames(droppedSkills, "disabled_skills", warned);
16683
+ warnDroppedNames(droppedSkills, "disabled_skills", warned, undefined, warningSink);
16578
16684
  const droppedAgents = computeDroppedNames(result.disabled_agents, CURRENT_AGENT_NAMES_SET);
16579
- warnDroppedNames(droppedAgents, "disabled_agents", warned);
16685
+ warnDroppedNames(droppedAgents, "disabled_agents", warned, undefined, warningSink);
16580
16686
  const droppedSkillSet = new Set(droppedSkills);
16581
16687
  const droppedAgentSet = new Set(droppedAgents);
16582
16688
  const effectiveConfig = droppedSkillSet.size === 0 && droppedAgentSet.size === 0 ? result : {
@@ -16584,7 +16690,76 @@ function loadConfigWithSources(projectDir, options) {
16584
16690
  disabled_skills: result.disabled_skills.filter((n) => !droppedSkillSet.has(n)),
16585
16691
  disabled_agents: result.disabled_agents.filter((n) => !droppedAgentSet.has(n))
16586
16692
  };
16587
- return { config: effectiveConfig, overlays };
16693
+ return {
16694
+ config: effectiveConfig,
16695
+ metadata: buildConfigObservationMetadata({
16696
+ custom: custom2.metadata,
16697
+ project: project.metadata,
16698
+ sources,
16699
+ user: user.metadata
16700
+ }),
16701
+ overlays
16702
+ };
16703
+ }
16704
+ function buildConfigObservationMetadata(summary) {
16705
+ const authorities = [];
16706
+ const sourceConfigs = new Map;
16707
+ const sourcePaths = new Map;
16708
+ for (const source of summary.sources) {
16709
+ sourceConfigs.set(source.trust, source.config);
16710
+ sourcePaths.set(source.trust, source.canonicalPath);
16711
+ }
16712
+ const firstDefinedSource = (fieldPath, candidates) => {
16713
+ for (const sourceKind of candidates) {
16714
+ const config2 = sourceConfigs.get(sourceKind);
16715
+ if (config2 && hasConfigField(config2, fieldPath)) {
16716
+ return { fieldPath, sourceKind };
16717
+ }
16718
+ }
16719
+ return;
16720
+ };
16721
+ const fieldCandidates = {
16722
+ "bootstrap.enabled": ["custom", "project", "user"],
16723
+ "bootstrap.file": ["custom", "project", "user"],
16724
+ skills_as_commands: ["custom", "project", "user"],
16725
+ "workflow_guard.debug": ["custom", "user"],
16726
+ "workflow_guard.mode": ["custom", "user"]
16727
+ };
16728
+ for (const fieldPath of CONFIG_AUTHORITY_FIELD_PATHS) {
16729
+ const authority = firstDefinedSource(fieldPath, fieldCandidates[fieldPath]);
16730
+ if (authority)
16731
+ authorities.push(authority);
16732
+ }
16733
+ const protectedFields = summary.sources.flatMap((source) => source.protectedFields);
16734
+ const sources = dedupeSourceMetadata([summary.custom, summary.project, summary.user], sourcePaths);
16735
+ return {
16736
+ authorities: sortAuthorities(authorities),
16737
+ protectedFields: sortProtectedFields(protectedFields),
16738
+ sources
16739
+ };
16740
+ }
16741
+ function dedupeSourceMetadata(metadata, sourcePaths) {
16742
+ const seen = new Set;
16743
+ return metadata.filter((source) => {
16744
+ const identity = sourcePaths.get(source.kind) ?? `missing:${source.kind}`;
16745
+ if (seen.has(identity))
16746
+ return false;
16747
+ seen.add(identity);
16748
+ return true;
16749
+ });
16750
+ }
16751
+ function hasConfigField(config2, fieldPath) {
16752
+ const [topLevel, nested] = fieldPath.split(".");
16753
+ if (nested === undefined)
16754
+ return config2[topLevel] !== undefined;
16755
+ const value = config2[topLevel];
16756
+ return isRecord2(value) && value[nested] !== undefined;
16757
+ }
16758
+ function sortAuthorities(authorities) {
16759
+ return [...authorities].sort((left, right) => left.fieldPath === right.fieldPath ? left.sourceKind.localeCompare(right.sourceKind) : left.fieldPath.localeCompare(right.fieldPath));
16760
+ }
16761
+ function sortProtectedFields(fields) {
16762
+ return [...fields].sort((left, right) => left.fieldPath === right.fieldPath ? left.sourceKind.localeCompare(right.sourceKind) : left.fieldPath.localeCompare(right.fieldPath));
16588
16763
  }
16589
16764
  function mergeOverlaySources(sources) {
16590
16765
  const result = {
@@ -16622,7 +16797,7 @@ function mergeOverlayMap(target, source, mapKey) {
16622
16797
  }
16623
16798
  }
16624
16799
  function rejectProjectSecurityOverlay(sourcePath, keyPath, value) {
16625
- for (const field of SECURITY_OVERLAY_FIELDS2) {
16800
+ for (const field of SECURITY_OVERLAY_FIELDS) {
16626
16801
  if (Object.hasOwn(value, field)) {
16627
16802
  throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath}.${field} is only valid in user config or OPENCODE_CONFIG_DIR config`);
16628
16803
  }
@@ -16630,7 +16805,7 @@ function rejectProjectSecurityOverlay(sourcePath, keyPath, value) {
16630
16805
  }
16631
16806
  function preserveSecurityFields(previous, next) {
16632
16807
  const result = { ...next };
16633
- for (const field of SECURITY_OVERLAY_FIELDS2) {
16808
+ for (const field of SECURITY_OVERLAY_FIELDS) {
16634
16809
  if (Object.hasOwn(previous, field)) {
16635
16810
  result[field] = previous[field];
16636
16811
  }
@@ -16698,11 +16873,12 @@ function overlayValues(overlays) {
16698
16873
  function throwInvalidOverlay(sourcePath, keyPath) {
16699
16874
  throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} must be an object`);
16700
16875
  }
16701
- function getConfigPaths(projectDir) {
16702
- const homeDir = os.homedir();
16703
- const customConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
16876
+ function getConfigPaths(projectDir, options) {
16877
+ const homeDir = options?.homeDir ?? os.homedir();
16878
+ const userConfigDir = options?.userConfigDir ?? path3.join(homeDir, ".config/opencode");
16879
+ const customConfigDir = options !== undefined && Object.hasOwn(options, "customConfigDir") ? options.customConfigDir?.trim() : process.env.OPENCODE_CONFIG_DIR?.trim();
16704
16880
  const result = {
16705
- userConfig: resolveConfigPath(path3.join(homeDir, ".config/opencode"), "systematic"),
16881
+ userConfig: resolveConfigPath(userConfigDir, "systematic"),
16706
16882
  projectConfig: resolveConfigPath(path3.join(projectDir, ".opencode"), "systematic"),
16707
16883
  userDir: path3.join(homeDir, ".config/opencode/systematic"),
16708
16884
  projectDir: path3.join(projectDir, ".opencode/systematic"),
@@ -16788,4 +16964,136 @@ function extractCommandFrontmatter(content) {
16788
16964
  };
16789
16965
  }
16790
16966
 
16791
- export { __require, parseFrontmatter, isRecord, walkDir, extractFrontmatterFromContent, findSkillsInDir, parse2 as parse, parseTree2 as parseTree, modify, applyEdits, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter };
16967
+ // src/lib/discovered-skills.ts
16968
+ import fs4 from "fs";
16969
+ import path4 from "path";
16970
+ var SKILL_NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
16971
+ function isValidSkillName(name) {
16972
+ return name.length >= 1 && name.length <= 64 && SKILL_NAME_REGEX.test(name);
16973
+ }
16974
+ function findGitWorktreeRoot(startDir) {
16975
+ let current = path4.resolve(startDir);
16976
+ while (true) {
16977
+ try {
16978
+ if (fs4.existsSync(path4.join(current, ".git"))) {
16979
+ return current;
16980
+ }
16981
+ } catch {
16982
+ return null;
16983
+ }
16984
+ const parent = path4.dirname(current);
16985
+ if (parent === current)
16986
+ return null;
16987
+ current = parent;
16988
+ }
16989
+ }
16990
+ function upWalk(targets, start, stop) {
16991
+ const results = [];
16992
+ let current = path4.resolve(start);
16993
+ const resolvedStop = stop === undefined ? undefined : path4.resolve(stop);
16994
+ while (true) {
16995
+ for (const target of targets) {
16996
+ const candidate = path4.join(current, target);
16997
+ try {
16998
+ if (fs4.existsSync(candidate)) {
16999
+ results.push(candidate);
17000
+ }
17001
+ } catch {}
17002
+ }
17003
+ if (resolvedStop !== undefined && current === resolvedStop)
17004
+ break;
17005
+ const parent = path4.dirname(current);
17006
+ if (parent === current)
17007
+ break;
17008
+ current = parent;
17009
+ }
17010
+ return results;
17011
+ }
17012
+ function uniqueStrings(values) {
17013
+ return Array.from(new Set(values));
17014
+ }
17015
+ function buildOpencodeConfigDirs(startDir, homeDir, gitRoot, globalConfigDir, opencodeConfigDirOverride) {
17016
+ const dirs = [globalConfigDir];
17017
+ dirs.push(...upWalk([".opencode"], startDir, gitRoot ?? startDir));
17018
+ dirs.push(...upWalk([".opencode"], homeDir, homeDir));
17019
+ if (opencodeConfigDirOverride !== undefined) {
17020
+ dirs.push(opencodeConfigDirOverride);
17021
+ }
17022
+ return uniqueStrings(dirs);
17023
+ }
17024
+ function globSkillFiles(rootDir, subdirNames) {
17025
+ const results = [];
17026
+ for (const subdirName of subdirNames) {
17027
+ const scanRoot = path4.join(rootDir, subdirName);
17028
+ try {
17029
+ if (!fs4.existsSync(scanRoot))
17030
+ continue;
17031
+ const entries = walkDir(scanRoot, {
17032
+ maxDepth: 10,
17033
+ filter: (entry) => entry.name === "SKILL.md"
17034
+ });
17035
+ for (const entry of entries) {
17036
+ results.push(entry.path);
17037
+ }
17038
+ } catch {}
17039
+ }
17040
+ return results;
17041
+ }
17042
+ function toDiscoveredSkill(skillPath, rootId, onIssue) {
17043
+ let content;
17044
+ try {
17045
+ content = fs4.readFileSync(skillPath, "utf8");
17046
+ } catch {
17047
+ onIssue?.("read-failed");
17048
+ return;
17049
+ }
17050
+ if (parseFrontmatter(content).parseError) {
17051
+ onIssue?.("source-invalid");
17052
+ return;
17053
+ }
17054
+ const frontmatter = extractFrontmatterFromContent(content);
17055
+ const name = frontmatter.name;
17056
+ if (!name || !isValidSkillName(name)) {
17057
+ onIssue?.("source-invalid");
17058
+ return;
17059
+ }
17060
+ return {
17061
+ name,
17062
+ description: frontmatter.description,
17063
+ frontmatter,
17064
+ body: parseFrontmatter(content).body,
17065
+ skillPath,
17066
+ root: rootId
17067
+ };
17068
+ }
17069
+ function discoverSkills(options) {
17070
+ const { startDir, homeDir, configDir, onIssue, opencodeConfigDirOverride } = options;
17071
+ const globalConfigDir = configDir ?? path4.join(homeDir, ".config/opencode");
17072
+ const gitRoot = findGitWorktreeRoot(startDir);
17073
+ const byName = new Map;
17074
+ function upsertAll(skillPaths, rootId) {
17075
+ for (const skillPath of skillPaths) {
17076
+ const skill = toDiscoveredSkill(skillPath, rootId, onIssue);
17077
+ if (skill)
17078
+ byName.set(skill.name, skill);
17079
+ }
17080
+ }
17081
+ upsertAll(globSkillFiles(homeDir, [".claude/skills"]), "global-claude");
17082
+ upsertAll(globSkillFiles(homeDir, [".agents/skills"]), "global-agents");
17083
+ const externalLevels = upWalk([".claude", ".agents"], startDir, gitRoot ?? startDir);
17084
+ for (const levelDir of externalLevels) {
17085
+ const isClaudeDir = path4.basename(levelDir) === ".claude";
17086
+ const parentDir = path4.dirname(levelDir);
17087
+ const subdirGlob = isClaudeDir ? ".claude/skills" : ".agents/skills";
17088
+ const rootId = isClaudeDir ? "project-claude" : "project-agents";
17089
+ upsertAll(globSkillFiles(parentDir, [subdirGlob]), rootId);
17090
+ }
17091
+ const configDirs = buildOpencodeConfigDirs(startDir, homeDir, gitRoot, globalConfigDir, opencodeConfigDirOverride);
17092
+ for (const dir of configDirs) {
17093
+ const rootId = dir === globalConfigDir ? "global-opencode-config" : "project-opencode";
17094
+ upsertAll(globSkillFiles(dir, ["skill", "skills"]), rootId);
17095
+ }
17096
+ return Array.from(byName.values());
17097
+ }
17098
+
17099
+ export { __require, parseFrontmatter, isRecord, extractString, findSkillsInDir, parse2 as parse, parseTree2 as parseTree, modify, applyEdits, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, discoverSkills };