onbuzz 4.8.0 → 4.8.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.
@@ -1,277 +1,282 @@
1
- /**
2
- * Skills Tool - Global skills library for agents
3
- *
4
- * Purpose:
5
- * - Allow agents to discover, browse, and read reusable skill instructions
6
- * - Support progressive disclosure: list → describe → read-section → read
7
- * - Support CRUD operations and importing skills from disk
8
- * - Skills are global (shared across agents) and persist across package updates
9
- *
10
- * Actions:
11
- * - list: List all skills with descriptions, section headings, and sizes
12
- * - describe: Get full metadata for a skill without loading content
13
- * - read: Read a skill's full content
14
- * - read-section: Read only a specific section of a skill
15
- * - read-file: Read a supporting file from a skill directory
16
- * - create: Create a new skill
17
- * - update: Update an existing skill
18
- * - delete: Remove a skill
19
- * - import: Import a skill from an external file or directory
20
- */
21
-
22
- import { BaseTool } from './baseTool.js';
23
- import { getSkillsService } from '../services/skillsService.js';
24
- import { SKILLS_ACTIONS } from '../utilities/toolConstants.js';
25
-
26
- class SkillsTool extends BaseTool {
27
- constructor(config = {}, logger = null) {
28
- super(config, logger);
29
-
30
- this.skillsService = null;
31
- this.requiresProject = false;
32
- this.isAsync = false;
33
- this.timeout = 30000;
34
- }
35
-
36
- async _ensureSkillsService() {
37
- if (!this.skillsService) {
38
- this.skillsService = getSkillsService(this.logger);
39
- await this.skillsService.initialize();
40
- }
41
- return this.skillsService;
42
- }
43
-
44
- getDescription() {
45
- return `Skills Tool: Browse and manage a global library of reusable skill instructions.
46
-
47
- Skills are structured knowledge packages containing instructions, checklists, templates, and reference files.
48
- Each skill is a directory with a skill.md file and optional supporting files.
49
- Always check for relevant skills when tackeling new tasks/changing task-focus.
50
-
51
- PROGRESSIVE DISCLOSURE Use this flow to minimize context usage:
52
- 1. "list" See all skills with section headings and sizes
53
- 2. "describe" Inspect a specific skill's structure in detail
54
- 3. "read-section" Load only the section you need
55
- 4. "read" → Load full content only when necessary
56
-
57
- ACTIONS:
58
-
59
- 1. LIST all skills:
60
- \`\`\`json
61
- { "toolId": "skills", "action": "list" }
62
- \`\`\`
63
- Returns: name, description, section headings, line count, file count for each skill.
64
-
65
- 2. DESCRIBE a skill (metadata only, no content):
66
- \`\`\`json
67
- { "toolId": "skills", "action": "describe", "name": "code-review" }
68
- \`\`\`
69
- Returns: description, sections with line ranges, file list, size.
70
-
71
- 3. READ full skill content:
72
- \`\`\`json
73
- { "toolId": "skills", "action": "read", "name": "code-review" }
74
- \`\`\`
75
- Returns: full skill.md content + list of files in the skill directory.
76
-
77
- 4. READ a specific SECTION:
78
- \`\`\`json
79
- { "toolId": "skills", "action": "read-section", "name": "code-review", "section": "Checklist" }
80
- \`\`\`
81
- Returns: only the content under the specified ## heading.
82
-
83
- 5. READ a supporting FILE:
84
- \`\`\`json
85
- { "toolId": "skills", "action": "read-file", "name": "email-templates", "file": "templates/welcome.html" }
86
- \`\`\`
87
- Returns: content of a specific file within the skill directory.
88
-
89
- 6. CREATE a new skill:
90
- \`\`\`json
91
- { "toolId": "skills", "action": "create", "name": "my-skill", "content": "# My Skill\\n\\nInstructions here...\\n\\n## Section One\\n..." }
92
- \`\`\`
93
- Optional: "files" array of { "path": "relative/path.ext", "content": "..." } for supporting files.
94
-
95
- 7. UPDATE an existing skill:
96
- \`\`\`json
97
- { "toolId": "skills", "action": "update", "name": "my-skill", "content": "# Updated content..." }
98
- \`\`\`
99
- Optional: "files" array to add/update supporting files.
100
-
101
- 8. DELETE a skill:
102
- \`\`\`json
103
- { "toolId": "skills", "action": "delete", "name": "my-skill" }
104
- \`\`\`
105
-
106
- 9. IMPORT a skill from disk:
107
- \`\`\`json
108
- { "toolId": "skills", "action": "import", "source": "/path/to/skill-dir-or-file" }
109
- \`\`\`
110
- Optional: "name" to override the derived skill name. If source is a directory, it must contain a skill.md file.
111
-
112
- SKILL NAMING: Names must be kebab-case (lowercase, hyphens). Example: "code-review", "email-templates".`;
113
- }
114
-
115
- parseParameters(content) {
116
- return content;
117
- }
118
-
119
- getRequiredParameters() {
120
- return ['action'];
121
- }
122
-
123
- getSupportedActions() {
124
- return Object.values(SKILLS_ACTIONS);
125
- }
126
-
127
- validateParameterTypes(params) {
128
- const errors = [];
129
- if (params.action && typeof params.action !== 'string') {
130
- errors.push('action must be a string');
131
- }
132
- if (params.name !== undefined && typeof params.name !== 'string') {
133
- errors.push('name must be a string');
134
- }
135
- if (params.content !== undefined && typeof params.content !== 'string') {
136
- errors.push('content must be a string');
137
- }
138
- if (params.section !== undefined && typeof params.section !== 'string') {
139
- errors.push('section must be a string');
140
- }
141
- if (params.file !== undefined && typeof params.file !== 'string') {
142
- errors.push('file must be a string');
143
- }
144
- if (params.source !== undefined && typeof params.source !== 'string') {
145
- errors.push('source must be a string');
146
- }
147
- return errors;
148
- }
149
-
150
- customValidateParameters(params) {
151
- const errors = [];
152
- const { action, name, content, section, file, source } = params;
153
-
154
- const validActions = this.getSupportedActions();
155
- if (!validActions.includes(action)) {
156
- errors.push(`Invalid action: "${action}". Valid actions: ${validActions.join(', ')}`);
157
- return errors;
158
- }
159
-
160
- // Action-specific required params
161
- const needsName = [SKILLS_ACTIONS.DESCRIBE, SKILLS_ACTIONS.READ, SKILLS_ACTIONS.READ_SECTION, SKILLS_ACTIONS.READ_FILE, SKILLS_ACTIONS.CREATE, SKILLS_ACTIONS.UPDATE, SKILLS_ACTIONS.DELETE];
162
- if (needsName.includes(action) && !name) {
163
- errors.push(`"name" is required for action "${action}"`);
164
- }
165
- if (action === SKILLS_ACTIONS.CREATE && !content) {
166
- errors.push('"content" is required for action "create"');
167
- }
168
- if (action === SKILLS_ACTIONS.READ_SECTION && !section) {
169
- errors.push('"section" is required for action "read-section"');
170
- }
171
- if (action === SKILLS_ACTIONS.READ_FILE && !file) {
172
- errors.push('"file" is required for action "read-file"');
173
- }
174
- if (action === SKILLS_ACTIONS.IMPORT && !source) {
175
- errors.push('"source" is required for action "import"');
176
- }
177
-
178
- return errors;
179
- }
180
-
181
- async execute(params, context = {}) {
182
- const service = await this._ensureSkillsService();
183
- const { action } = params;
184
-
185
- try {
186
- switch (action) {
187
- case SKILLS_ACTIONS.LIST:
188
- return this._formatResult(await service.listSkills(), 'Skills listed');
189
-
190
- case SKILLS_ACTIONS.DESCRIBE:
191
- return this._formatResult(await service.describeSkill(params.name), `Skill described: ${params.name}`);
192
-
193
- case SKILLS_ACTIONS.READ:
194
- return this._formatResult(await service.readSkill(params.name), `Skill read: ${params.name}`);
195
-
196
- case SKILLS_ACTIONS.READ_SECTION:
197
- return this._formatResult(await service.readSkillSection(params.name, params.section), `Section read: ${params.section}`);
198
-
199
- case SKILLS_ACTIONS.READ_FILE:
200
- return this._formatResult(await service.readSkillFile(params.name, params.file), `File read: ${params.file}`);
201
-
202
- case SKILLS_ACTIONS.CREATE:
203
- return this._formatResult(await service.createSkill(params.name, params.content, params.files || [], params.description || null), `Skill created: ${params.name}`);
204
-
205
- case SKILLS_ACTIONS.UPDATE:
206
- return this._formatResult(await service.updateSkill(params.name, params.content || null, params.files || [], params.description || null), `Skill updated: ${params.name}`);
207
-
208
- case SKILLS_ACTIONS.DELETE:
209
- await service.deleteSkill(params.name);
210
- return this._formatResult({ deleted: params.name }, `Skill deleted: ${params.name}`);
211
-
212
- case SKILLS_ACTIONS.IMPORT:
213
- return this._formatResult(await service.importSkill(params.source, params.name || null, params.description || null), `Skill imported: ${params.name || params.source}`);
214
-
215
- default:
216
- return { success: false, error: `Unknown action: ${action}` };
217
- }
218
- } catch (error) {
219
- return { success: false, error: error.message };
220
- }
221
- }
222
-
223
- _formatResult(data, message) {
224
- return {
225
- success: true,
226
- result: data,
227
- message
228
- };
229
- }
230
-
231
- getParameterSchema() {
232
- return {
233
- type: 'object',
234
- required: ['action'],
235
- properties: {
236
- action: {
237
- type: 'string',
238
- enum: Object.values(SKILLS_ACTIONS),
239
- description: 'The skill action to perform'
240
- },
241
- name: {
242
- type: 'string',
243
- description: 'Skill name (kebab-case)'
244
- },
245
- content: {
246
- type: 'string',
247
- description: 'Skill content (markdown)'
248
- },
249
- section: {
250
- type: 'string',
251
- description: 'Section heading to read (for read-section action)'
252
- },
253
- file: {
254
- type: 'string',
255
- description: 'Relative file path within skill directory (for read-file action)'
256
- },
257
- source: {
258
- type: 'string',
259
- description: 'Source file or directory path (for import action)'
260
- },
261
- files: {
262
- type: 'array',
263
- items: {
264
- type: 'object',
265
- properties: {
266
- path: { type: 'string' },
267
- content: { type: 'string' }
268
- }
269
- },
270
- description: 'Additional supporting files (for create/update actions)'
271
- }
272
- }
273
- };
274
- }
275
- }
276
-
277
- export default SkillsTool;
1
+ /**
2
+ * Skills Tool - Global skills library for agents
3
+ *
4
+ * Purpose:
5
+ * - Allow agents to discover, browse, and read reusable skill instructions
6
+ * - Support progressive disclosure: list → describe → read-section → read
7
+ * - Support CRUD operations and importing skills from disk
8
+ * - Skills are global (shared across agents) and persist across package updates
9
+ *
10
+ * Actions:
11
+ * - list: List all skills with descriptions, section headings, and sizes
12
+ * - describe: Get full metadata for a skill without loading content
13
+ * - read: Read a skill's full content
14
+ * - read-section: Read only a specific section of a skill
15
+ * - read-file: Read a supporting file from a skill directory
16
+ * - create: Create a new skill
17
+ * - update: Update an existing skill
18
+ * - delete: Remove a skill
19
+ * - import: Import a skill from an external file or directory
20
+ */
21
+
22
+ import { BaseTool } from './baseTool.js';
23
+ import { getSkillsService } from '../services/skillsService.js';
24
+ import { SKILLS_ACTIONS } from '../utilities/toolConstants.js';
25
+
26
+ class SkillsTool extends BaseTool {
27
+ constructor(config = {}, logger = null) {
28
+ super(config, logger);
29
+
30
+ this.skillsService = null;
31
+ this.requiresProject = false;
32
+ this.isAsync = false;
33
+ this.timeout = 30000;
34
+ }
35
+
36
+ async _ensureSkillsService() {
37
+ if (!this.skillsService) {
38
+ this.skillsService = getSkillsService(this.logger);
39
+ await this.skillsService.initialize();
40
+ }
41
+ return this.skillsService;
42
+ }
43
+
44
+ getDescription() {
45
+ return `Skills Tool: Browse and manage a global library of reusable skill instructions.
46
+
47
+ Skills are structured knowledge packages containing instructions, checklists, templates, and reference files.
48
+ Each skill is a directory with a skill.md file and optional supporting files.
49
+
50
+ ★ PROACTIVE USE — When a new task arrives or your focus shifts, your FIRST move should be:
51
+ \`{ "toolId": "skills", "action": "list" }\`
52
+ Skim the names + descriptions. If anything looks relevant, "describe" it and follow its
53
+ checklist instead of improvising. The team curates skills specifically so you don't have
54
+ to re-derive recurring playbooks from scratch.
55
+
56
+ PROGRESSIVE DISCLOSURE — Use this flow to minimize context usage:
57
+ 1. "list" → See all skills with section headings and sizes
58
+ 2. "describe" → Inspect a specific skill's structure in detail
59
+ 3. "read-section" Load only the section you need
60
+ 4. "read" → Load full content only when necessary
61
+
62
+ ACTIONS:
63
+
64
+ 1. LIST all skills:
65
+ \`\`\`json
66
+ { "toolId": "skills", "action": "list" }
67
+ \`\`\`
68
+ Returns: name, description, section headings, line count, file count for each skill.
69
+
70
+ 2. DESCRIBE a skill (metadata only, no content):
71
+ \`\`\`json
72
+ { "toolId": "skills", "action": "describe", "name": "code-review" }
73
+ \`\`\`
74
+ Returns: description, sections with line ranges, file list, size.
75
+
76
+ 3. READ full skill content:
77
+ \`\`\`json
78
+ { "toolId": "skills", "action": "read", "name": "code-review" }
79
+ \`\`\`
80
+ Returns: full skill.md content + list of files in the skill directory.
81
+
82
+ 4. READ a specific SECTION:
83
+ \`\`\`json
84
+ { "toolId": "skills", "action": "read-section", "name": "code-review", "section": "Checklist" }
85
+ \`\`\`
86
+ Returns: only the content under the specified ## heading.
87
+
88
+ 5. READ a supporting FILE:
89
+ \`\`\`json
90
+ { "toolId": "skills", "action": "read-file", "name": "email-templates", "file": "templates/welcome.html" }
91
+ \`\`\`
92
+ Returns: content of a specific file within the skill directory.
93
+
94
+ 6. CREATE a new skill:
95
+ \`\`\`json
96
+ { "toolId": "skills", "action": "create", "name": "my-skill", "content": "# My Skill\\n\\nInstructions here...\\n\\n## Section One\\n..." }
97
+ \`\`\`
98
+ Optional: "files" array of { "path": "relative/path.ext", "content": "..." } for supporting files.
99
+
100
+ 7. UPDATE an existing skill:
101
+ \`\`\`json
102
+ { "toolId": "skills", "action": "update", "name": "my-skill", "content": "# Updated content..." }
103
+ \`\`\`
104
+ Optional: "files" array to add/update supporting files.
105
+
106
+ 8. DELETE a skill:
107
+ \`\`\`json
108
+ { "toolId": "skills", "action": "delete", "name": "my-skill" }
109
+ \`\`\`
110
+
111
+ 9. IMPORT a skill from disk:
112
+ \`\`\`json
113
+ { "toolId": "skills", "action": "import", "source": "/path/to/skill-dir-or-file" }
114
+ \`\`\`
115
+ Optional: "name" to override the derived skill name. If source is a directory, it must contain a skill.md file.
116
+
117
+ SKILL NAMING: Names must be kebab-case (lowercase, hyphens). Example: "code-review", "email-templates".`;
118
+ }
119
+
120
+ parseParameters(content) {
121
+ return content;
122
+ }
123
+
124
+ getRequiredParameters() {
125
+ return ['action'];
126
+ }
127
+
128
+ getSupportedActions() {
129
+ return Object.values(SKILLS_ACTIONS);
130
+ }
131
+
132
+ validateParameterTypes(params) {
133
+ const errors = [];
134
+ if (params.action && typeof params.action !== 'string') {
135
+ errors.push('action must be a string');
136
+ }
137
+ if (params.name !== undefined && typeof params.name !== 'string') {
138
+ errors.push('name must be a string');
139
+ }
140
+ if (params.content !== undefined && typeof params.content !== 'string') {
141
+ errors.push('content must be a string');
142
+ }
143
+ if (params.section !== undefined && typeof params.section !== 'string') {
144
+ errors.push('section must be a string');
145
+ }
146
+ if (params.file !== undefined && typeof params.file !== 'string') {
147
+ errors.push('file must be a string');
148
+ }
149
+ if (params.source !== undefined && typeof params.source !== 'string') {
150
+ errors.push('source must be a string');
151
+ }
152
+ return errors;
153
+ }
154
+
155
+ customValidateParameters(params) {
156
+ const errors = [];
157
+ const { action, name, content, section, file, source } = params;
158
+
159
+ const validActions = this.getSupportedActions();
160
+ if (!validActions.includes(action)) {
161
+ errors.push(`Invalid action: "${action}". Valid actions: ${validActions.join(', ')}`);
162
+ return errors;
163
+ }
164
+
165
+ // Action-specific required params
166
+ const needsName = [SKILLS_ACTIONS.DESCRIBE, SKILLS_ACTIONS.READ, SKILLS_ACTIONS.READ_SECTION, SKILLS_ACTIONS.READ_FILE, SKILLS_ACTIONS.CREATE, SKILLS_ACTIONS.UPDATE, SKILLS_ACTIONS.DELETE];
167
+ if (needsName.includes(action) && !name) {
168
+ errors.push(`"name" is required for action "${action}"`);
169
+ }
170
+ if (action === SKILLS_ACTIONS.CREATE && !content) {
171
+ errors.push('"content" is required for action "create"');
172
+ }
173
+ if (action === SKILLS_ACTIONS.READ_SECTION && !section) {
174
+ errors.push('"section" is required for action "read-section"');
175
+ }
176
+ if (action === SKILLS_ACTIONS.READ_FILE && !file) {
177
+ errors.push('"file" is required for action "read-file"');
178
+ }
179
+ if (action === SKILLS_ACTIONS.IMPORT && !source) {
180
+ errors.push('"source" is required for action "import"');
181
+ }
182
+
183
+ return errors;
184
+ }
185
+
186
+ async execute(params, context = {}) {
187
+ const service = await this._ensureSkillsService();
188
+ const { action } = params;
189
+
190
+ try {
191
+ switch (action) {
192
+ case SKILLS_ACTIONS.LIST:
193
+ return this._formatResult(await service.listSkills(), 'Skills listed');
194
+
195
+ case SKILLS_ACTIONS.DESCRIBE:
196
+ return this._formatResult(await service.describeSkill(params.name), `Skill described: ${params.name}`);
197
+
198
+ case SKILLS_ACTIONS.READ:
199
+ return this._formatResult(await service.readSkill(params.name), `Skill read: ${params.name}`);
200
+
201
+ case SKILLS_ACTIONS.READ_SECTION:
202
+ return this._formatResult(await service.readSkillSection(params.name, params.section), `Section read: ${params.section}`);
203
+
204
+ case SKILLS_ACTIONS.READ_FILE:
205
+ return this._formatResult(await service.readSkillFile(params.name, params.file), `File read: ${params.file}`);
206
+
207
+ case SKILLS_ACTIONS.CREATE:
208
+ return this._formatResult(await service.createSkill(params.name, params.content, params.files || [], params.description || null), `Skill created: ${params.name}`);
209
+
210
+ case SKILLS_ACTIONS.UPDATE:
211
+ return this._formatResult(await service.updateSkill(params.name, params.content || null, params.files || [], params.description || null), `Skill updated: ${params.name}`);
212
+
213
+ case SKILLS_ACTIONS.DELETE:
214
+ await service.deleteSkill(params.name);
215
+ return this._formatResult({ deleted: params.name }, `Skill deleted: ${params.name}`);
216
+
217
+ case SKILLS_ACTIONS.IMPORT:
218
+ return this._formatResult(await service.importSkill(params.source, params.name || null, params.description || null), `Skill imported: ${params.name || params.source}`);
219
+
220
+ default:
221
+ return { success: false, error: `Unknown action: ${action}` };
222
+ }
223
+ } catch (error) {
224
+ return { success: false, error: error.message };
225
+ }
226
+ }
227
+
228
+ _formatResult(data, message) {
229
+ return {
230
+ success: true,
231
+ result: data,
232
+ message
233
+ };
234
+ }
235
+
236
+ getParameterSchema() {
237
+ return {
238
+ type: 'object',
239
+ required: ['action'],
240
+ properties: {
241
+ action: {
242
+ type: 'string',
243
+ enum: Object.values(SKILLS_ACTIONS),
244
+ description: 'The skill action to perform'
245
+ },
246
+ name: {
247
+ type: 'string',
248
+ description: 'Skill name (kebab-case)'
249
+ },
250
+ content: {
251
+ type: 'string',
252
+ description: 'Skill content (markdown)'
253
+ },
254
+ section: {
255
+ type: 'string',
256
+ description: 'Section heading to read (for read-section action)'
257
+ },
258
+ file: {
259
+ type: 'string',
260
+ description: 'Relative file path within skill directory (for read-file action)'
261
+ },
262
+ source: {
263
+ type: 'string',
264
+ description: 'Source file or directory path (for import action)'
265
+ },
266
+ files: {
267
+ type: 'array',
268
+ items: {
269
+ type: 'object',
270
+ properties: {
271
+ path: { type: 'string' },
272
+ content: { type: 'string' }
273
+ }
274
+ },
275
+ description: 'Additional supporting files (for create/update actions)'
276
+ }
277
+ }
278
+ };
279
+ }
280
+ }
281
+
282
+ export default SkillsTool;
@@ -26,8 +26,26 @@ const SYSTEM_DEFAULTS = {
26
26
  };
27
27
 
28
28
  // Model Router Configuration
29
+ //
30
+ // ROUTER_MODEL is the model the Dynamic Model Routing feature calls
31
+ // (via a cheap chat-completion request) to decide which "real" model
32
+ // should handle each turn. Resolution order:
33
+ // 1. env LOXIA_ROUTER_MODEL — operator override, no rebuild needed
34
+ // 2. 'gpt-4.1-nano' — current live default. The platform's
35
+ // autopilot-model-router deployment uses gpt-4.1-nano as its
36
+ // underlying model, and the model-catalog keys entries by the
37
+ // underlying model name (NOT the Azure deployment name), so the
38
+ // CLI must ask for 'gpt-4.1-nano' to be matched. Cheaper than the
39
+ // retired OpenAI 'model-router' product, same job.
40
+ //
41
+ // Historical note: this used to be the literal string 'model-router',
42
+ // matching an OpenAI product name. That product is no longer in our
43
+ // Azure catalog (no deployment keyed under that name), which caused
44
+ // every routing call to fail with HTTP 400 "Unsupported model:
45
+ // model-router" until the circuit breaker tripped. The fix migrates
46
+ // the default to the underlying model name that IS in the catalog.
29
47
  const MODEL_ROUTER_CONFIG = {
30
- ROUTER_MODEL: 'model-router', // Autopilot model router deployment
48
+ ROUTER_MODEL: process.env.LOXIA_ROUTER_MODEL || 'gpt-4.1-nano',
31
49
  CONTEXT_MESSAGES_COUNT: 5, // Number of recent messages to include
32
50
  BENCHMARK_REFRESH_INTERVAL: 3600000, // 1 hour in milliseconds
33
51
  FALLBACK_ON_ERROR: true, // Continue with previous model on router error