opencode-idea 0.0.17 → 0.0.19

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.
@@ -27,6 +27,13 @@
27
27
  // and deliberately kept OUT of the registered definition — the caller filters on
28
28
  // it instead (see `readSkills`), because a skill whose tools are absent must not
29
29
  // be registered at all.
30
+ //
31
+ // The other key is OpenCode's own, written in the portable frontmatter spelling
32
+ // OpenCode documents: a nested `metadata:` block holding
33
+ // `opencode/autoinvoke: false`. It is translated to the definition's
34
+ // `autoinvoke` — the name the registration API uses — which is what makes a
35
+ // skill user-invoked only: OpenCode keeps it registered and loadable on request,
36
+ // but leaves it out of the model's available list.
30
37
 
31
38
  import { readFileSync, readdirSync } from 'node:fs';
32
39
  import { fileURLToPath } from 'node:url';
@@ -87,11 +94,37 @@ function splitList(value) {
87
94
  .filter((entry) => entry !== '');
88
95
  }
89
96
 
97
+ /**
98
+ * Read `opencode/autoinvoke` out of the frontmatter, or `undefined` when absent.
99
+ *
100
+ * The spelling is OpenCode's portable one — a nested `metadata:` block holding
101
+ * `opencode/autoinvoke: false`. That shape is exactly what the flat reader above
102
+ * already produces: the indented line trims to `opencode/autoinvoke: false` and
103
+ * lands under that namespaced key, while the bare `metadata:` line comes out
104
+ * empty and is ignored. One nesting level is therefore supported on purpose, not
105
+ * by accident.
106
+ *
107
+ * The value must be `true` or `false`. Anything else throws: a typo that
108
+ * silently left the skill model-invocable is the failure this whole mechanism
109
+ * exists to prevent, so it must not pass quietly.
110
+ *
111
+ * @param {Record<string, string>} fields
112
+ * @param {string} source Used in error messages.
113
+ * @returns {boolean | undefined}
114
+ */
115
+ export function readAutoinvoke(fields, source) {
116
+ const raw = fields['opencode/autoinvoke'];
117
+ if (raw === undefined) return undefined;
118
+ if (raw === 'true') return true;
119
+ if (raw === 'false') return false;
120
+ throw new Error(`${source}: "opencode/autoinvoke" must be true or false, got "${raw}"`);
121
+ }
122
+
90
123
  /**
91
124
  * Load one skill directory into a `Skill.Info`-shaped definition.
92
125
  *
93
126
  * @param {string} id Directory name, used as the skill id.
94
- * @returns {{ id: string, name: string, description: string, path: string, content: string }}
127
+ * @returns {{ id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string }}
95
128
  */
96
129
  export function readSkill(id) {
97
130
  return readSkillPackage(id).skill;
@@ -104,8 +137,13 @@ export function readSkill(id) {
104
137
  * while `requires` is this package's own metadata and must NOT be registered —
105
138
  * it is what the caller filters on.
106
139
  *
140
+ * `autoinvoke` is OpenCode's, so it goes the other way: it ships in the
141
+ * frontmatter as `opencode/autoinvoke` and is translated onto the definition,
142
+ * where it picks user-invoked-only over model-invocable. It is only set when the
143
+ * frontmatter asks for it, so a skill that says nothing keeps the plain shape.
144
+ *
107
145
  * @param {string} id Directory name, used as the skill id.
108
- * @returns {{ skill: { id: string, name: string, description: string, path: string, content: string }, requires: string[] }}
146
+ * @returns {{ skill: { id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string }, requires: string[] }}
109
147
  */
110
148
  export function readSkillPackage(id) {
111
149
  const file = new URL(`${id}/${SKILL_FILE_NAME}`, SKILLS_DIR);
@@ -116,11 +154,13 @@ export function readSkillPackage(id) {
116
154
  if (!description) {
117
155
  throw new Error(`${source}: frontmatter needs a "description", otherwise the skill is never offered`);
118
156
  }
157
+ const autoinvoke = readAutoinvoke(fields, source);
119
158
  return {
120
159
  skill: {
121
160
  id,
122
161
  name: fields.name || id,
123
162
  description,
163
+ ...(autoinvoke === undefined ? {} : { autoinvoke }),
124
164
  path: fileURLToPath(file),
125
165
  content: text.replace(FRONTMATTER, '').replace(/^\s+/, '').trimEnd(),
126
166
  },
@@ -131,7 +171,7 @@ export function readSkillPackage(id) {
131
171
  /**
132
172
  * Every shipped skill, sorted by id, each with the tools it requires.
133
173
  *
134
- * @returns {Array<{ skill: { id: string, name: string, description: string, path: string, content: string }, requires: string[] }>}
174
+ * @returns {Array<{ skill: { id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string }, requires: string[] }>}
135
175
  */
136
176
  export function readSkills() {
137
177
  return readdirSync(SKILLS_DIR, { withFileTypes: true })