assistme 0.2.8 → 0.3.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/chunk-TTEGHE2E.js +47 -0
- package/dist/chunk-UWE5WVQI.js +289 -0
- package/dist/config-PUIS2TQL.js +12 -0
- package/dist/index.js +818 -704
- package/dist/job-runner-N4XAAWLJ.js +7 -0
- package/package.json +1 -2
- package/src/agent/job-runner.ts +33 -71
- package/src/agent/mcp-servers.ts +111 -151
- package/src/agent/memory.test.ts +41 -65
- package/src/agent/memory.ts +33 -134
- package/src/agent/processor.ts +59 -17
- package/src/agent/scheduler.ts +47 -93
- package/src/agent/session.test.ts +8 -12
- package/src/agent/session.ts +10 -53
- package/src/agent/skill-evaluator.ts +258 -0
- package/src/agent/skills.ts +191 -494
- package/src/commands/job.ts +6 -6
- package/src/commands/status.ts +3 -10
- package/src/db/api-client.ts +68 -0
- package/src/db/supabase.test.ts +71 -184
- package/src/db/supabase.ts +140 -243
- package/dist/chunk-XY3LGAOY.js +0 -580
- package/dist/job-runner-XTGLMPZ3.js +0 -6
package/src/agent/skills.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import { log } from "../utils/logger.js";
|
|
3
|
-
import {
|
|
3
|
+
import { callMcpHandler } from "../db/api-client.js";
|
|
4
4
|
|
|
5
5
|
// ── Text Processing Helpers ─────────────────────────────────────────
|
|
6
6
|
|
|
@@ -25,12 +25,10 @@ const STOP_WORDS = new Set([
|
|
|
25
25
|
* Supports both English (space-delimited) and Chinese (character bigrams).
|
|
26
26
|
*/
|
|
27
27
|
function tokenize(text: string): string[] {
|
|
28
|
-
// English tokens: split on delimiters, filter stop words
|
|
29
28
|
const englishTokens = text
|
|
30
29
|
.split(/[\s\-_/.,;:!?()[\]{}'"]+/)
|
|
31
30
|
.filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
32
31
|
|
|
33
|
-
// Chinese character bigrams for CJK text
|
|
34
32
|
const chineseChars = text.replace(/[^\u4e00-\u9fff]/g, "");
|
|
35
33
|
const chineseBigrams: string[] = [];
|
|
36
34
|
for (let i = 0; i < chineseChars.length - 1; i++) {
|
|
@@ -40,9 +38,6 @@ function tokenize(text: string): string[] {
|
|
|
40
38
|
return [...englishTokens, ...chineseBigrams];
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
/**
|
|
44
|
-
* Generate bigrams (consecutive word pairs) from a token list.
|
|
45
|
-
*/
|
|
46
41
|
function bigrams(tokens: string[]): Set<string> {
|
|
47
42
|
const result = new Set<string>();
|
|
48
43
|
for (let i = 0; i < tokens.length - 1; i++) {
|
|
@@ -67,26 +62,6 @@ export interface SkillMetadata {
|
|
|
67
62
|
skillKey?: string;
|
|
68
63
|
}
|
|
69
64
|
|
|
70
|
-
/**
|
|
71
|
-
* A variable that a skill requires from the user.
|
|
72
|
-
* Variables are defined in the skill template (e.g. `{{github_repos}}`)
|
|
73
|
-
* and resolved with user-specific values stored in agent_skills.config.
|
|
74
|
-
*/
|
|
75
|
-
export interface SkillVariable {
|
|
76
|
-
/** Variable name used in templates, e.g. "github_repos" */
|
|
77
|
-
name: string;
|
|
78
|
-
/** Human-readable description, e.g. "GitHub repositories to monitor" */
|
|
79
|
-
description: string;
|
|
80
|
-
/** Data type of the variable */
|
|
81
|
-
type: "string" | "string[]" | "number" | "boolean";
|
|
82
|
-
/** Whether the skill cannot function without this variable */
|
|
83
|
-
required: boolean;
|
|
84
|
-
/** Default value if not configured by user */
|
|
85
|
-
default?: string;
|
|
86
|
-
/** Example value to guide the user, e.g. "octocat/hello-world, myorg/myrepo" */
|
|
87
|
-
example?: string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
65
|
export interface Skill {
|
|
91
66
|
name: string;
|
|
92
67
|
description: string;
|
|
@@ -98,20 +73,14 @@ export interface Skill {
|
|
|
98
73
|
argumentHint: string;
|
|
99
74
|
metadata: SkillMetadata;
|
|
100
75
|
homepage: string;
|
|
101
|
-
content: string;
|
|
102
|
-
filePath: string;
|
|
76
|
+
content: string;
|
|
77
|
+
filePath: string;
|
|
103
78
|
source: "bundled" | "manual" | "external" | "auto_extracted" | "auto_improved" | "job_generated";
|
|
104
|
-
dbId?: string;
|
|
105
|
-
sourceSkillId?: string;
|
|
106
|
-
|
|
107
|
-
variables?: SkillVariable[];
|
|
108
|
-
/** User-specific variable values (from agent_skills.config) */
|
|
109
|
-
config?: Record<string, unknown>;
|
|
79
|
+
dbId?: string;
|
|
80
|
+
sourceSkillId?: string;
|
|
81
|
+
invocationCount: number;
|
|
110
82
|
}
|
|
111
83
|
|
|
112
|
-
/**
|
|
113
|
-
* Parse metadata JSONB from DB row into SkillMetadata.
|
|
114
|
-
*/
|
|
115
84
|
function parseDbMetadata(raw: unknown): SkillMetadata {
|
|
116
85
|
if (!raw || typeof raw !== "object") return {};
|
|
117
86
|
const obj = raw as Record<string, unknown>;
|
|
@@ -127,63 +96,33 @@ function parseDbMetadata(raw: unknown): SkillMetadata {
|
|
|
127
96
|
}
|
|
128
97
|
|
|
129
98
|
// ── Skill Manager ───────────────────────────────────────────────────
|
|
130
|
-
// Architecture:
|
|
131
|
-
// skills table = repository of all skill definitions (like npm)
|
|
132
|
-
// agent_skills = user's active collection (approved/added skills)
|
|
133
|
-
// in-memory map = loaded from agent_skills for fast access
|
|
134
|
-
//
|
|
135
|
-
// Flow:
|
|
136
|
-
// create() → saves to skills table only (draft, is_public: false)
|
|
137
|
-
// addSkill() → copies from skills to agent_skills + in-memory map
|
|
138
|
-
// publish() → sets skills.is_public = true
|
|
139
99
|
|
|
140
100
|
export class SkillManager {
|
|
141
101
|
private skills: Map<string, Skill> = new Map();
|
|
142
102
|
private idfCache: Map<string, number> = new Map();
|
|
143
103
|
private userId: string | null = null;
|
|
144
104
|
|
|
145
|
-
/**
|
|
105
|
+
/** Cache for findRelevant() — keyed by prompt, invalidated on skill changes */
|
|
106
|
+
private relevanceCache: Map<string, { results: Skill[]; maxResults: number }> = new Map();
|
|
107
|
+
|
|
146
108
|
private readonly DESCRIPTION_BUDGET_CHARS = 16_000;
|
|
147
109
|
|
|
148
|
-
/**
|
|
149
|
-
* Set the user ID for DB operations.
|
|
150
|
-
* Called after authentication; enables DB-backed skill storage.
|
|
151
|
-
*/
|
|
152
110
|
setUserId(userId: string): void {
|
|
153
111
|
this.userId = userId;
|
|
154
112
|
}
|
|
155
113
|
|
|
156
|
-
/**
|
|
157
|
-
* Load skills from DB (user's agent_skills collection).
|
|
158
|
-
* This is the only loading mechanism — no file-based loading.
|
|
159
|
-
*/
|
|
160
114
|
async loadFromDb(): Promise<void> {
|
|
161
115
|
if (!this.userId) return;
|
|
162
116
|
|
|
163
117
|
try {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
// Load only the user's active skills from agent_skills
|
|
167
|
-
const { data, error } = await sb
|
|
168
|
-
.from("agent_skills")
|
|
169
|
-
.select("*")
|
|
170
|
-
.eq("user_id", this.userId)
|
|
171
|
-
.eq("is_active", true);
|
|
172
|
-
|
|
173
|
-
if (error) {
|
|
174
|
-
log.debug(`DB skill load failed: ${error.message}`);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
118
|
+
const data = await callMcpHandler<Record<string, unknown>[]>("skill.load");
|
|
177
119
|
|
|
178
120
|
this.skills.clear();
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
for (const row of dbRows) {
|
|
121
|
+
for (const row of data || []) {
|
|
182
122
|
const skill = this.rowToSkill(row);
|
|
183
123
|
this.skills.set(skill.name, skill);
|
|
184
124
|
}
|
|
185
125
|
|
|
186
|
-
// Build IDF cache for relevance matching
|
|
187
126
|
this.buildIdfCache();
|
|
188
127
|
|
|
189
128
|
if (this.skills.size > 0) {
|
|
@@ -194,19 +133,7 @@ export class SkillManager {
|
|
|
194
133
|
}
|
|
195
134
|
}
|
|
196
135
|
|
|
197
|
-
/**
|
|
198
|
-
* Convert a DB row to a Skill object.
|
|
199
|
-
*/
|
|
200
136
|
private rowToSkill(row: Record<string, unknown>): Skill {
|
|
201
|
-
// Parse variables from metadata.variables or top-level variables column
|
|
202
|
-
const meta = (row.metadata || {}) as Record<string, unknown>;
|
|
203
|
-
const rawVars = (row.variables || meta.variables) as SkillVariable[] | undefined;
|
|
204
|
-
const variables = Array.isArray(rawVars) ? rawVars : undefined;
|
|
205
|
-
|
|
206
|
-
// Parse user config (variable values) from config column
|
|
207
|
-
const rawConfig = row.config as Record<string, unknown> | undefined;
|
|
208
|
-
const config = rawConfig && typeof rawConfig === "object" ? rawConfig : undefined;
|
|
209
|
-
|
|
210
137
|
return {
|
|
211
138
|
name: row.name as string,
|
|
212
139
|
description: (row.description as string) || "",
|
|
@@ -223,15 +150,16 @@ export class SkillManager {
|
|
|
223
150
|
source: (row.source as Skill["source"]) || "manual",
|
|
224
151
|
dbId: row.id as string,
|
|
225
152
|
sourceSkillId: (row.source_skill_id as string) || undefined,
|
|
226
|
-
|
|
227
|
-
config,
|
|
153
|
+
invocationCount: (row.invocation_count as number) || 0,
|
|
228
154
|
};
|
|
229
155
|
}
|
|
230
156
|
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
157
|
+
/** Invalidate caches when skills change (create, add, update, remove). */
|
|
158
|
+
private invalidateCaches(): void {
|
|
159
|
+
this.relevanceCache.clear();
|
|
160
|
+
this.buildIdfCache();
|
|
161
|
+
}
|
|
162
|
+
|
|
235
163
|
private buildIdfCache(): void {
|
|
236
164
|
this.idfCache.clear();
|
|
237
165
|
const docFreq = new Map<string, number>();
|
|
@@ -248,102 +176,98 @@ export class SkillManager {
|
|
|
248
176
|
}
|
|
249
177
|
}
|
|
250
178
|
|
|
251
|
-
/**
|
|
252
|
-
* Get all loaded skills (from user's agent_skills collection).
|
|
253
|
-
*/
|
|
254
179
|
getAll(): Skill[] {
|
|
255
180
|
return Array.from(this.skills.values());
|
|
256
181
|
}
|
|
257
182
|
|
|
258
|
-
/**
|
|
259
|
-
* Get a skill by name.
|
|
260
|
-
*/
|
|
261
183
|
get(name: string): Skill | undefined {
|
|
262
184
|
return this.skills.get(name);
|
|
263
185
|
}
|
|
264
186
|
|
|
265
|
-
/**
|
|
266
|
-
* Find skills relevant to a given task prompt.
|
|
267
|
-
* Uses a TF-IDF-inspired scoring approach with pre-computed IDF cache.
|
|
268
|
-
*/
|
|
269
187
|
findRelevant(prompt: string, maxResults = 3): Skill[] {
|
|
270
|
-
|
|
188
|
+
// Check cache (hit if same prompt and at least as many results cached)
|
|
189
|
+
const cacheKey = prompt.toLowerCase();
|
|
190
|
+
const cached = this.relevanceCache.get(cacheKey);
|
|
191
|
+
if (cached && cached.maxResults >= maxResults) {
|
|
192
|
+
return cached.results.slice(0, maxResults);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const lower = cacheKey;
|
|
271
196
|
const promptTokens = tokenize(lower);
|
|
272
197
|
const promptTokenSet = new Set(promptTokens);
|
|
273
|
-
|
|
274
198
|
const idf = (word: string): number => this.idfCache.get(word) || 1;
|
|
275
|
-
|
|
276
199
|
const scored: Array<{ skill: Skill; score: number }> = [];
|
|
277
200
|
|
|
278
201
|
for (const skill of this.skills.values()) {
|
|
279
202
|
if (skill.disableModelInvocation) continue;
|
|
280
|
-
|
|
281
203
|
let score = 0;
|
|
282
|
-
|
|
283
|
-
// Name match (strongest signal)
|
|
284
204
|
if (lower.includes(skill.name.toLowerCase())) score += 10;
|
|
285
|
-
|
|
286
|
-
// Keywords match (explicit frontmatter keywords — high signal)
|
|
287
205
|
for (const kw of skill.keywords) {
|
|
288
|
-
if (lower.includes(kw.toLowerCase()))
|
|
289
|
-
score += 8;
|
|
290
|
-
}
|
|
206
|
+
if (lower.includes(kw.toLowerCase())) score += 8;
|
|
291
207
|
}
|
|
292
|
-
|
|
293
|
-
// Description word overlap with IDF weighting
|
|
294
208
|
const descTokens = tokenize(skill.description.toLowerCase());
|
|
295
209
|
for (const word of descTokens) {
|
|
296
|
-
if (promptTokenSet.has(word))
|
|
297
|
-
score += 3 * idf(word);
|
|
298
|
-
}
|
|
210
|
+
if (promptTokenSet.has(word)) score += 3 * idf(word);
|
|
299
211
|
}
|
|
300
|
-
|
|
301
|
-
// Content keyword matches with IDF weighting (lighter)
|
|
302
212
|
const contentTokens = tokenize(skill.content.toLowerCase());
|
|
303
213
|
for (const word of contentTokens) {
|
|
304
|
-
if (promptTokenSet.has(word))
|
|
305
|
-
score += 0.5 * idf(word);
|
|
306
|
-
}
|
|
214
|
+
if (promptTokenSet.has(word)) score += 0.5 * idf(word);
|
|
307
215
|
}
|
|
308
|
-
|
|
309
|
-
// Bigram matching: consecutive word pairs
|
|
310
216
|
const promptBigrams = bigrams(promptTokens);
|
|
311
217
|
const descBigrams = bigrams(descTokens);
|
|
312
218
|
for (const bg of descBigrams) {
|
|
313
219
|
if (promptBigrams.has(bg)) score += 5;
|
|
314
220
|
}
|
|
315
|
-
|
|
316
|
-
if (score > 0) {
|
|
317
|
-
scored.push({ skill, score });
|
|
318
|
-
}
|
|
221
|
+
if (score > 0) scored.push({ skill, score });
|
|
319
222
|
}
|
|
320
223
|
|
|
321
|
-
|
|
224
|
+
const results = scored
|
|
322
225
|
.sort((a, b) => b.score - a.score)
|
|
323
226
|
.slice(0, maxResults)
|
|
324
227
|
.map((s) => s.skill);
|
|
228
|
+
|
|
229
|
+
// Cache the result
|
|
230
|
+
this.relevanceCache.set(cacheKey, { results, maxResults });
|
|
231
|
+
return results;
|
|
325
232
|
}
|
|
326
233
|
|
|
327
234
|
/**
|
|
328
|
-
* Build
|
|
329
|
-
*
|
|
235
|
+
* Build lightweight skill descriptions for the system prompt.
|
|
236
|
+
* When a taskPrompt is provided, relevant skills are prioritized to the top;
|
|
237
|
+
* remaining skills are sorted by usage frequency (invocationCount).
|
|
330
238
|
*/
|
|
331
|
-
buildSkillDescriptions(): string {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
239
|
+
buildSkillDescriptions(taskPrompt?: string): string {
|
|
240
|
+
const all = this.getAll().filter((s) => !s.disableModelInvocation);
|
|
241
|
+
if (all.length === 0) return "";
|
|
242
|
+
|
|
243
|
+
// Split into "always" skills and the rest
|
|
244
|
+
const alwaysSkills = all.filter((s) => s.metadata.always);
|
|
245
|
+
const rest = all.filter((s) => !s.metadata.always);
|
|
246
|
+
|
|
247
|
+
// If a task prompt is provided, find relevant skills and prioritize them
|
|
248
|
+
let relevantNames: Set<string> | null = null;
|
|
249
|
+
if (taskPrompt) {
|
|
250
|
+
const relevant = this.findRelevant(taskPrompt, 10);
|
|
251
|
+
relevantNames = new Set(relevant.map((s) => s.name));
|
|
252
|
+
}
|
|
341
253
|
|
|
342
|
-
|
|
254
|
+
// Sort: relevant skills first (by match score, already sorted), then by invocation count
|
|
255
|
+
const sorted = rest.sort((a, b) => {
|
|
256
|
+
if (relevantNames) {
|
|
257
|
+
const aRelevant = relevantNames.has(a.name);
|
|
258
|
+
const bRelevant = relevantNames.has(b.name);
|
|
259
|
+
if (aRelevant && !bRelevant) return -1;
|
|
260
|
+
if (!aRelevant && bRelevant) return 1;
|
|
261
|
+
}
|
|
262
|
+
return (b.invocationCount || 0) - (a.invocationCount || 0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const skills = [...alwaysSkills, ...sorted];
|
|
343
266
|
|
|
344
267
|
let budget = this.DESCRIPTION_BUDGET_CHARS;
|
|
345
268
|
let prompt = "\n\n## Your Skills\n";
|
|
346
|
-
prompt += "These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n
|
|
269
|
+
prompt += "These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n";
|
|
270
|
+
prompt += "If no skill matches but the task is a reusable pattern, consider creating one with skill_create.\n\n";
|
|
347
271
|
let included = 0;
|
|
348
272
|
|
|
349
273
|
for (const skill of skills) {
|
|
@@ -357,22 +281,19 @@ export class SkillManager {
|
|
|
357
281
|
}
|
|
358
282
|
|
|
359
283
|
if (included < skills.length) {
|
|
360
|
-
prompt += `\n_(${skills.length - included} additional skills available — use
|
|
284
|
+
prompt += `\n_(${skills.length - included} additional skills available — use skill_search to find more)_\n`;
|
|
361
285
|
}
|
|
362
286
|
|
|
363
287
|
return prompt;
|
|
364
288
|
}
|
|
365
289
|
|
|
366
|
-
/**
|
|
367
|
-
* @deprecated Use buildSkillDescriptions() + skill_invoke tool instead.
|
|
368
|
-
*/
|
|
290
|
+
/** @deprecated Use buildSkillDescriptions() + skill_invoke tool instead. */
|
|
369
291
|
buildSkillPrompt(taskPrompt: string): string {
|
|
370
292
|
const relevant = this.findRelevant(taskPrompt);
|
|
371
293
|
if (relevant.length === 0) return "";
|
|
372
294
|
|
|
373
295
|
let prompt = "\n\n## Available Skills\n";
|
|
374
|
-
prompt +=
|
|
375
|
-
"The following skills provide detailed instructions for this type of task:\n\n";
|
|
296
|
+
prompt += "The following skills provide detailed instructions for this type of task:\n\n";
|
|
376
297
|
|
|
377
298
|
for (const skill of relevant) {
|
|
378
299
|
const emoji = skill.metadata.emoji || "";
|
|
@@ -384,48 +305,30 @@ export class SkillManager {
|
|
|
384
305
|
return prompt;
|
|
385
306
|
}
|
|
386
307
|
|
|
387
|
-
/**
|
|
388
|
-
* Create a new skill. Saves to `skills` table only (as private draft).
|
|
389
|
-
* The skill is NOT added to user's active collection (agent_skills) until
|
|
390
|
-
* the user approves it via addSkill().
|
|
391
|
-
*
|
|
392
|
-
* Returns { id, name } of the created skill, or null on failure.
|
|
393
|
-
*/
|
|
394
308
|
async create(
|
|
395
309
|
name: string,
|
|
396
310
|
description: string,
|
|
397
311
|
content: string,
|
|
398
|
-
options?: { source?: string; emoji?: string; keywords?: string[]
|
|
312
|
+
options?: { source?: string; emoji?: string; keywords?: string[] }
|
|
399
313
|
): Promise<{ id: string; name: string } | null> {
|
|
400
314
|
if (!this.userId) return null;
|
|
401
315
|
|
|
402
316
|
try {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
p_version: "1.0.0",
|
|
419
|
-
p_source: source,
|
|
420
|
-
p_emoji: options?.emoji || null,
|
|
421
|
-
p_keywords: options?.keywords || [],
|
|
422
|
-
p_metadata: metadata,
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
if (error) {
|
|
426
|
-
log.debug(`Skill create failed for "${name}": ${error.message}`);
|
|
427
|
-
return null;
|
|
428
|
-
}
|
|
317
|
+
const metadata = options?.emoji ? { openclaw: { emoji: options.emoji } } : {};
|
|
318
|
+
|
|
319
|
+
const data = await callMcpHandler<Record<string, unknown> | Record<string, unknown>[]>(
|
|
320
|
+
"skill.create",
|
|
321
|
+
{
|
|
322
|
+
name,
|
|
323
|
+
description,
|
|
324
|
+
content,
|
|
325
|
+
version: "1.0.0",
|
|
326
|
+
source: options?.source || "manual",
|
|
327
|
+
emoji: options?.emoji || null,
|
|
328
|
+
keywords: options?.keywords || [],
|
|
329
|
+
metadata,
|
|
330
|
+
},
|
|
331
|
+
);
|
|
429
332
|
|
|
430
333
|
const row = (Array.isArray(data) ? data[0] : data) as Record<string, unknown> | null;
|
|
431
334
|
if (!row) {
|
|
@@ -433,10 +336,29 @@ export class SkillManager {
|
|
|
433
336
|
return null;
|
|
434
337
|
}
|
|
435
338
|
|
|
436
|
-
// RPC returns out_id/out_name to avoid column ambiguity
|
|
437
339
|
const id = (row.out_id || row.id) as string;
|
|
438
340
|
const skillName = (row.out_name || row.name) as string;
|
|
439
341
|
|
|
342
|
+
// Add to in-memory map so the skill is immediately searchable
|
|
343
|
+
this.skills.set(skillName, {
|
|
344
|
+
name: skillName,
|
|
345
|
+
description,
|
|
346
|
+
version: "1.0.0",
|
|
347
|
+
userInvocable: true,
|
|
348
|
+
disableModelInvocation: false,
|
|
349
|
+
keywords: options?.keywords || [],
|
|
350
|
+
allowedTools: [],
|
|
351
|
+
argumentHint: "",
|
|
352
|
+
metadata: options?.emoji ? { emoji: options.emoji } : {},
|
|
353
|
+
homepage: "",
|
|
354
|
+
content,
|
|
355
|
+
filePath: "",
|
|
356
|
+
source: (options?.source as Skill["source"]) || "manual",
|
|
357
|
+
dbId: id,
|
|
358
|
+
invocationCount: 0,
|
|
359
|
+
});
|
|
360
|
+
this.invalidateCaches();
|
|
361
|
+
|
|
440
362
|
log.info(`Skill "${skillName}" created in skills table (pending approval)`);
|
|
441
363
|
return { id, name: skillName };
|
|
442
364
|
} catch (err) {
|
|
@@ -445,63 +367,19 @@ export class SkillManager {
|
|
|
445
367
|
}
|
|
446
368
|
}
|
|
447
369
|
|
|
448
|
-
/**
|
|
449
|
-
* Add a skill to the user's active collection (agent_skills).
|
|
450
|
-
* Copies content from the skills table.
|
|
451
|
-
* This is the "approval" step — after this, the skill is usable.
|
|
452
|
-
*/
|
|
453
370
|
async addSkill(skillId: string): Promise<Skill | null> {
|
|
454
371
|
if (!this.userId) return null;
|
|
455
372
|
|
|
456
373
|
try {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (fetchErr || !pub) {
|
|
468
|
-
log.debug(`Skill not found: ${skillId}`);
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const row = pub as Record<string, unknown>;
|
|
473
|
-
|
|
474
|
-
// Add to user's agent_skills via upsert
|
|
475
|
-
const { data, error } = await sb.rpc("upsert_agent_skill", {
|
|
476
|
-
p_user_id: this.userId,
|
|
477
|
-
p_name: row.name as string,
|
|
478
|
-
p_description: (row.description as string) || "",
|
|
479
|
-
p_content: row.content as string,
|
|
480
|
-
p_version: (row.version as string) || "1.0.0",
|
|
481
|
-
p_source: (row.source as string) || "manual",
|
|
482
|
-
p_emoji: (row.emoji as string) || null,
|
|
483
|
-
p_keywords: (row.keywords as string[]) || [],
|
|
484
|
-
p_source_skill_id: skillId,
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
if (error) {
|
|
488
|
-
log.debug(`addSkill failed: ${error.message}`);
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Update install count on skills table (non-critical)
|
|
493
|
-
try {
|
|
494
|
-
await sb.rpc("increment_install_count", { p_skill_id: skillId });
|
|
495
|
-
} catch {
|
|
496
|
-
try {
|
|
497
|
-
await sb.from("skills")
|
|
498
|
-
.update({ install_count: (row.install_count as number || 0) + 1 })
|
|
499
|
-
.eq("id", skillId);
|
|
500
|
-
} catch { /* ignore */ }
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Add to in-memory map
|
|
504
|
-
const agentSkillRow = (data && typeof data === "object" ? data : row) as Record<string, unknown>;
|
|
374
|
+
const result = await callMcpHandler<{ skill: Record<string, unknown>; agent_skill: unknown }>(
|
|
375
|
+
"skill.fetch_and_add",
|
|
376
|
+
{ skill_id: skillId },
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const row = result.skill;
|
|
380
|
+
const agentSkillRow = (result.agent_skill && typeof result.agent_skill === "object"
|
|
381
|
+
? result.agent_skill
|
|
382
|
+
: row) as Record<string, unknown>;
|
|
505
383
|
const skill = this.rowToSkill({
|
|
506
384
|
...agentSkillRow,
|
|
507
385
|
name: row.name,
|
|
@@ -510,7 +388,7 @@ export class SkillManager {
|
|
|
510
388
|
source_skill_id: skillId,
|
|
511
389
|
});
|
|
512
390
|
this.skills.set(skill.name, skill);
|
|
513
|
-
this.
|
|
391
|
+
this.invalidateCaches();
|
|
514
392
|
|
|
515
393
|
log.info(`Skill "${row.name}" added to user's collection`);
|
|
516
394
|
return skill;
|
|
@@ -520,28 +398,18 @@ export class SkillManager {
|
|
|
520
398
|
}
|
|
521
399
|
}
|
|
522
400
|
|
|
523
|
-
/**
|
|
524
|
-
* Remove a skill from user's collection (soft-delete in agent_skills).
|
|
525
|
-
*/
|
|
526
401
|
remove(name: string): boolean {
|
|
527
402
|
const skill = this.skills.get(name);
|
|
528
403
|
if (!skill) return false;
|
|
529
|
-
|
|
530
404
|
this.skills.delete(name);
|
|
531
|
-
|
|
532
|
-
// Mark inactive in DB (fire-and-forget)
|
|
405
|
+
this.invalidateCaches();
|
|
533
406
|
this.removeFromDb(name).catch(() => {});
|
|
534
407
|
return true;
|
|
535
408
|
}
|
|
536
409
|
|
|
537
|
-
/**
|
|
538
|
-
* List skills with summary info.
|
|
539
|
-
*/
|
|
540
410
|
listFormatted(): string {
|
|
541
411
|
const skills = this.getAll();
|
|
542
|
-
if (skills.length === 0)
|
|
543
|
-
return "No skills in your collection.";
|
|
544
|
-
}
|
|
412
|
+
if (skills.length === 0) return "No skills in your collection.";
|
|
545
413
|
|
|
546
414
|
return skills
|
|
547
415
|
.map((s) => {
|
|
@@ -551,29 +419,15 @@ export class SkillManager {
|
|
|
551
419
|
.join("\n\n");
|
|
552
420
|
}
|
|
553
421
|
|
|
554
|
-
/**
|
|
555
|
-
* Check if a skill with a similar name already exists in user's collection.
|
|
556
|
-
*/
|
|
557
422
|
findSimilar(name: string): Skill | null {
|
|
558
|
-
// Exact match
|
|
559
423
|
if (this.skills.has(name)) return this.skills.get(name)!;
|
|
560
424
|
|
|
561
|
-
// Fuzzy match: check if names overlap significantly
|
|
562
425
|
const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
563
426
|
for (const [existingName, skill] of this.skills) {
|
|
564
|
-
const normalizedExisting = existingName
|
|
565
|
-
|
|
566
|
-
.replace(/[^a-z0-9]/g, "");
|
|
567
|
-
|
|
568
|
-
// One contains the other
|
|
569
|
-
if (
|
|
570
|
-
normalizedName.includes(normalizedExisting) ||
|
|
571
|
-
normalizedExisting.includes(normalizedName)
|
|
572
|
-
) {
|
|
427
|
+
const normalizedExisting = existingName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
428
|
+
if (normalizedName.includes(normalizedExisting) || normalizedExisting.includes(normalizedName)) {
|
|
573
429
|
return skill;
|
|
574
430
|
}
|
|
575
|
-
|
|
576
|
-
// High word overlap
|
|
577
431
|
const nameWords = new Set(name.split("-"));
|
|
578
432
|
const existingWords = new Set(existingName.split("-"));
|
|
579
433
|
let overlap = 0;
|
|
@@ -584,45 +438,34 @@ export class SkillManager {
|
|
|
584
438
|
return skill;
|
|
585
439
|
}
|
|
586
440
|
}
|
|
587
|
-
|
|
588
441
|
return null;
|
|
589
442
|
}
|
|
590
443
|
|
|
591
|
-
/**
|
|
592
|
-
* Update an existing skill in user's collection.
|
|
593
|
-
* Bumps the patch version automatically. Also updates skills table if user is author.
|
|
594
|
-
*/
|
|
595
444
|
update(name: string, newContent: string, description?: string): boolean {
|
|
596
445
|
const skill = this.skills.get(name);
|
|
597
446
|
if (!skill) return false;
|
|
598
447
|
|
|
599
|
-
// Bump patch version
|
|
600
448
|
const versionParts = skill.version.split(".").map(Number);
|
|
601
449
|
versionParts[2] = (versionParts[2] || 0) + 1;
|
|
602
450
|
const newVersion = versionParts.join(".");
|
|
603
451
|
const newDescription = description || skill.description;
|
|
604
452
|
|
|
605
|
-
// Update in-memory
|
|
606
453
|
skill.content = newContent;
|
|
607
454
|
skill.description = newDescription;
|
|
608
455
|
skill.version = newVersion;
|
|
456
|
+
this.invalidateCaches();
|
|
609
457
|
|
|
610
|
-
// Persist to agent_skills (fire-and-forget)
|
|
611
458
|
this.syncToAgentSkills(name, newDescription, newContent, newVersion, {
|
|
612
459
|
source: "auto_improved",
|
|
613
460
|
}).catch(() => {});
|
|
614
461
|
|
|
615
|
-
// Also update skills table if this skill has a source (fire-and-forget)
|
|
616
462
|
if (skill.sourceSkillId && this.userId) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
.eq("author_id", this.userId!);
|
|
624
|
-
} catch { /* non-critical */ }
|
|
625
|
-
})();
|
|
463
|
+
callMcpHandler("skill.update_source", {
|
|
464
|
+
source_skill_id: skill.sourceSkillId,
|
|
465
|
+
content: newContent,
|
|
466
|
+
description: newDescription,
|
|
467
|
+
version: newVersion,
|
|
468
|
+
}).catch(() => {});
|
|
626
469
|
}
|
|
627
470
|
|
|
628
471
|
log.info(`Skill "${name}" updated to v${newVersion}`);
|
|
@@ -631,10 +474,6 @@ export class SkillManager {
|
|
|
631
474
|
|
|
632
475
|
// ── DB Integration ─────────────────────────────────────────────────
|
|
633
476
|
|
|
634
|
-
/**
|
|
635
|
-
* Persist a skill to agent_skills via upsert_agent_skill().
|
|
636
|
-
* Fire-and-forget: failures are logged but don't block.
|
|
637
|
-
*/
|
|
638
477
|
async syncToAgentSkills(
|
|
639
478
|
name: string,
|
|
640
479
|
description: string,
|
|
@@ -646,46 +485,26 @@ export class SkillManager {
|
|
|
646
485
|
keywords?: string[];
|
|
647
486
|
changeSummary?: string;
|
|
648
487
|
sourceSkillId?: string;
|
|
649
|
-
variables?: SkillVariable[];
|
|
650
488
|
}
|
|
651
489
|
): Promise<void> {
|
|
652
490
|
if (!this.userId) return;
|
|
653
491
|
|
|
654
492
|
try {
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
p_name: name,
|
|
666
|
-
p_description: description,
|
|
667
|
-
p_content: content,
|
|
668
|
-
p_version: version,
|
|
669
|
-
p_source: options?.source || "manual",
|
|
670
|
-
p_emoji: options?.emoji || null,
|
|
671
|
-
p_keywords: options?.keywords || [],
|
|
672
|
-
p_change_summary: options?.changeSummary || null,
|
|
673
|
-
p_source_skill_id: options?.sourceSkillId || null,
|
|
674
|
-
p_metadata: Object.keys(metadata).length > 0 ? metadata : null,
|
|
493
|
+
const data = await callMcpHandler<Record<string, unknown> | null>("skill.upsert", {
|
|
494
|
+
name,
|
|
495
|
+
description,
|
|
496
|
+
content,
|
|
497
|
+
version,
|
|
498
|
+
source: options?.source || "manual",
|
|
499
|
+
emoji: options?.emoji || null,
|
|
500
|
+
keywords: options?.keywords || [],
|
|
501
|
+
change_summary: options?.changeSummary || null,
|
|
502
|
+
source_skill_id: options?.sourceSkillId || null,
|
|
675
503
|
});
|
|
676
504
|
|
|
677
|
-
if (error) {
|
|
678
|
-
log.debug(`DB skill sync failed for "${name}": ${error.message}`);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Update in-memory skill with dbId and variables
|
|
683
505
|
const skill = this.skills.get(name);
|
|
684
|
-
if (skill && data && typeof data === "object" && "id" in
|
|
685
|
-
skill.dbId =
|
|
686
|
-
}
|
|
687
|
-
if (skill && options?.variables) {
|
|
688
|
-
skill.variables = options.variables;
|
|
506
|
+
if (skill && data && typeof data === "object" && "id" in data) {
|
|
507
|
+
skill.dbId = data.id as string;
|
|
689
508
|
}
|
|
690
509
|
|
|
691
510
|
log.debug(`Skill "${name}" synced to agent_skills`);
|
|
@@ -694,10 +513,6 @@ export class SkillManager {
|
|
|
694
513
|
}
|
|
695
514
|
}
|
|
696
515
|
|
|
697
|
-
/**
|
|
698
|
-
* Log a skill invocation to agent_skill_invocations.
|
|
699
|
-
* Fire-and-forget: failures don't block task execution.
|
|
700
|
-
*/
|
|
701
516
|
async logInvocation(
|
|
702
517
|
skillName: string,
|
|
703
518
|
options?: {
|
|
@@ -718,29 +533,19 @@ export class SkillManager {
|
|
|
718
533
|
}
|
|
719
534
|
|
|
720
535
|
try {
|
|
721
|
-
|
|
722
|
-
const { error } = await sb.from("agent_skill_invocations").insert({
|
|
536
|
+
await callMcpHandler("skill.log_invocation", {
|
|
723
537
|
skill_id: skillDbId,
|
|
724
|
-
user_id: this.userId,
|
|
725
538
|
message_id: options?.messageId || null,
|
|
726
539
|
session_id: options?.sessionId || null,
|
|
727
540
|
task_prompt: options?.taskPrompt?.slice(0, 500) || null,
|
|
728
541
|
arguments: options?.arguments || null,
|
|
729
542
|
success: options?.success ?? null,
|
|
730
543
|
});
|
|
731
|
-
|
|
732
|
-
if (error) {
|
|
733
|
-
log.debug(`Invocation log failed: ${error.message}`);
|
|
734
|
-
}
|
|
735
544
|
} catch (err) {
|
|
736
545
|
log.debug(`Invocation log error: ${err}`);
|
|
737
546
|
}
|
|
738
547
|
}
|
|
739
548
|
|
|
740
|
-
/**
|
|
741
|
-
* Search user's skills in DB using full-text search.
|
|
742
|
-
* Falls back to in-memory findRelevant() if DB is unavailable.
|
|
743
|
-
*/
|
|
744
549
|
async searchDb(query: string, limit = 10): Promise<Array<{
|
|
745
550
|
name: string;
|
|
746
551
|
description: string;
|
|
@@ -748,19 +553,11 @@ export class SkillManager {
|
|
|
748
553
|
source: string;
|
|
749
554
|
invocationCount: number;
|
|
750
555
|
}>> {
|
|
751
|
-
// Try DB search first
|
|
752
556
|
if (this.userId) {
|
|
753
557
|
try {
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
p_query: query,
|
|
758
|
-
p_limit: limit,
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
if (!error && data) {
|
|
762
|
-
const rows = data as Record<string, unknown>[];
|
|
763
|
-
return rows.map((row) => ({
|
|
558
|
+
const data = await callMcpHandler<Record<string, unknown>[]>("skill.search", { query, limit });
|
|
559
|
+
if (data) {
|
|
560
|
+
return data.map((row) => ({
|
|
764
561
|
name: row.name as string,
|
|
765
562
|
description: (row.description as string) || "",
|
|
766
563
|
emoji: (row.emoji as string) || "",
|
|
@@ -768,12 +565,9 @@ export class SkillManager {
|
|
|
768
565
|
invocationCount: (row.invocation_count as number) || 0,
|
|
769
566
|
}));
|
|
770
567
|
}
|
|
771
|
-
} catch {
|
|
772
|
-
// Fall through to in-memory search
|
|
773
|
-
}
|
|
568
|
+
} catch { /* Fall through to in-memory search */ }
|
|
774
569
|
}
|
|
775
570
|
|
|
776
|
-
// Fallback: in-memory search
|
|
777
571
|
const results = this.findRelevant(query, limit);
|
|
778
572
|
return results.map((s) => ({
|
|
779
573
|
name: s.name,
|
|
@@ -784,88 +578,52 @@ export class SkillManager {
|
|
|
784
578
|
}));
|
|
785
579
|
}
|
|
786
580
|
|
|
787
|
-
/**
|
|
788
|
-
* Remove a skill from DB (soft-delete).
|
|
789
|
-
*/
|
|
790
581
|
async removeFromDb(name: string): Promise<void> {
|
|
791
582
|
if (!this.userId) return;
|
|
792
|
-
|
|
793
583
|
try {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
.from("agent_skills")
|
|
797
|
-
.update({ is_active: false })
|
|
798
|
-
.eq("user_id", this.userId)
|
|
799
|
-
.eq("name", name);
|
|
800
|
-
} catch {
|
|
801
|
-
// Non-critical
|
|
802
|
-
}
|
|
584
|
+
await callMcpHandler("skill.remove", { name });
|
|
585
|
+
} catch { /* Non-critical */ }
|
|
803
586
|
}
|
|
804
587
|
|
|
805
588
|
// ── Marketplace ────────────────────────────────────────────────────
|
|
806
589
|
|
|
807
|
-
/**
|
|
808
|
-
* Publish a skill to the marketplace (sets is_public = true in skills table).
|
|
809
|
-
*/
|
|
810
590
|
async publish(
|
|
811
591
|
name: string,
|
|
812
592
|
options?: { category?: string; authorName?: string }
|
|
813
593
|
): Promise<{ id: string } | null> {
|
|
814
594
|
if (!this.userId) return null;
|
|
815
|
-
|
|
816
595
|
const skill = this.skills.get(name);
|
|
817
596
|
if (!skill) return null;
|
|
818
|
-
|
|
819
597
|
if (skill.source === "external") {
|
|
820
598
|
log.debug(`Cannot publish external skill "${name}"`);
|
|
821
599
|
return null;
|
|
822
600
|
}
|
|
823
601
|
|
|
824
602
|
try {
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
.
|
|
830
|
-
.
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
author_id: this.userId,
|
|
841
|
-
author_name: options?.authorName || null,
|
|
842
|
-
metadata: skill.metadata,
|
|
843
|
-
homepage: skill.homepage || null,
|
|
844
|
-
category: options?.category || null,
|
|
845
|
-
source: skill.source,
|
|
846
|
-
is_public: true,
|
|
847
|
-
},
|
|
848
|
-
{ onConflict: "author_id,name" }
|
|
849
|
-
)
|
|
850
|
-
.select("id")
|
|
851
|
-
.single();
|
|
852
|
-
|
|
853
|
-
if (error) {
|
|
854
|
-
log.debug(`Publish failed for "${name}": ${error.message}`);
|
|
855
|
-
return null;
|
|
856
|
-
}
|
|
603
|
+
const data = await callMcpHandler<{ id: string }>("skill.publish", {
|
|
604
|
+
name: skill.name,
|
|
605
|
+
description: skill.description,
|
|
606
|
+
version: skill.version,
|
|
607
|
+
emoji: skill.metadata.emoji || null,
|
|
608
|
+
content: skill.content,
|
|
609
|
+
argument_hint: skill.argumentHint || null,
|
|
610
|
+
keywords: skill.keywords,
|
|
611
|
+
allowed_tools: skill.allowedTools,
|
|
612
|
+
author_name: options?.authorName || null,
|
|
613
|
+
metadata: skill.metadata,
|
|
614
|
+
homepage: skill.homepage || null,
|
|
615
|
+
category: options?.category || null,
|
|
616
|
+
source: skill.source,
|
|
617
|
+
});
|
|
857
618
|
|
|
858
619
|
log.info(`Skill "${name}" published to marketplace`);
|
|
859
|
-
return data
|
|
620
|
+
return data;
|
|
860
621
|
} catch (err) {
|
|
861
622
|
log.debug(`Publish error: ${err}`);
|
|
862
623
|
return null;
|
|
863
624
|
}
|
|
864
625
|
}
|
|
865
626
|
|
|
866
|
-
/**
|
|
867
|
-
* Browse marketplace skills (public + active).
|
|
868
|
-
*/
|
|
869
627
|
async browse(options?: {
|
|
870
628
|
query?: string;
|
|
871
629
|
category?: string;
|
|
@@ -885,18 +643,15 @@ export class SkillManager {
|
|
|
885
643
|
ratingCount: number;
|
|
886
644
|
}>> {
|
|
887
645
|
try {
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
p_offset: options?.offset || 0,
|
|
646
|
+
const data = await callMcpHandler<Record<string, unknown>[]>("skill.browse", {
|
|
647
|
+
query: options?.query || null,
|
|
648
|
+
category: options?.category || null,
|
|
649
|
+
sort: options?.sort || "popular",
|
|
650
|
+
limit: options?.limit || 20,
|
|
651
|
+
offset: options?.offset || 0,
|
|
895
652
|
});
|
|
896
653
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
return (data as Record<string, unknown>[]).map((r) => ({
|
|
654
|
+
return (data || []).map((r) => ({
|
|
900
655
|
id: r.id as string,
|
|
901
656
|
name: r.name as string,
|
|
902
657
|
description: (r.description as string) || "",
|
|
@@ -912,93 +667,39 @@ export class SkillManager {
|
|
|
912
667
|
return [];
|
|
913
668
|
}
|
|
914
669
|
}
|
|
915
|
-
|
|
916
|
-
// ── Variable Configuration ──────────────────────────────────────────
|
|
917
|
-
|
|
918
|
-
/**
|
|
919
|
-
* Update user-specific variable values (config) for a skill.
|
|
920
|
-
* Persists to agent_skills.config in DB and updates in-memory.
|
|
921
|
-
*/
|
|
922
|
-
async updateConfig(
|
|
923
|
-
skillName: string,
|
|
924
|
-
config: Record<string, unknown>
|
|
925
|
-
): Promise<boolean> {
|
|
926
|
-
const skill = this.skills.get(skillName);
|
|
927
|
-
if (!skill) return false;
|
|
928
|
-
|
|
929
|
-
// Merge with existing config
|
|
930
|
-
const merged = { ...(skill.config || {}), ...config };
|
|
931
|
-
skill.config = merged;
|
|
932
|
-
|
|
933
|
-
// Persist to DB
|
|
934
|
-
if (this.userId && skill.dbId) {
|
|
935
|
-
try {
|
|
936
|
-
const sb = getSupabase();
|
|
937
|
-
await sb
|
|
938
|
-
.from("agent_skills")
|
|
939
|
-
.update({ config: merged })
|
|
940
|
-
.eq("id", skill.dbId)
|
|
941
|
-
.eq("user_id", this.userId);
|
|
942
|
-
log.debug(`Config updated for skill "${skillName}"`);
|
|
943
|
-
} catch (err) {
|
|
944
|
-
log.debug(`Config update failed for "${skillName}": ${err}`);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
return true;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Get the unresolved (unconfigured) variables for a skill.
|
|
953
|
-
* Returns variables that are required but have no value in config.
|
|
954
|
-
*/
|
|
955
|
-
getUnconfiguredVariables(skillName: string): SkillVariable[] {
|
|
956
|
-
const skill = this.skills.get(skillName);
|
|
957
|
-
if (!skill?.variables) return [];
|
|
958
|
-
|
|
959
|
-
return skill.variables.filter((v) => {
|
|
960
|
-
if (!v.required) return false;
|
|
961
|
-
const value = skill.config?.[v.name];
|
|
962
|
-
return value === undefined || value === null || value === "";
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
670
|
}
|
|
966
671
|
|
|
967
672
|
// ── Exported Utility Functions ─────────────────────────────────────
|
|
968
673
|
|
|
969
674
|
/**
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
675
|
+
* Validate a skill name against the Agent Skills spec:
|
|
676
|
+
* 1-64 chars, lowercase kebab-case (a-z, 0-9, hyphens),
|
|
677
|
+
* no leading/trailing/consecutive hyphens.
|
|
678
|
+
* Returns null if valid, or an error message if invalid.
|
|
974
679
|
*/
|
|
975
|
-
export function
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
):
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
return content.replace(/\{\{(\w+)\}\}/g, (match, varName: string) => {
|
|
983
|
-
const value = config?.[varName];
|
|
984
|
-
|
|
985
|
-
if (value !== undefined && value !== null) {
|
|
986
|
-
if (Array.isArray(value)) return value.join(", ");
|
|
987
|
-
return String(value);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
// Check for default value in variable definitions
|
|
991
|
-
const varDef = variables?.find((v) => v.name === varName);
|
|
992
|
-
if (varDef?.default !== undefined) return varDef.default;
|
|
993
|
-
|
|
994
|
-
// Leave as-is with warning for unresolved
|
|
995
|
-
return `{{${varName}: [NOT CONFIGURED]}}`;
|
|
996
|
-
});
|
|
680
|
+
export function validateSkillName(name: string): string | null {
|
|
681
|
+
if (!name || name.length === 0) return "name is empty";
|
|
682
|
+
if (name.length > 64) return `name too long (${name.length}/64 chars)`;
|
|
683
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
684
|
+
return `name must be lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens. Got: "${name}"`;
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
997
687
|
}
|
|
998
688
|
|
|
999
689
|
/**
|
|
1000
|
-
*
|
|
690
|
+
* Normalize a skill name to valid kebab-case.
|
|
691
|
+
* Converts to lowercase, replaces non-alphanumeric with hyphens,
|
|
692
|
+
* collapses consecutive hyphens, trims, and truncates to 64 chars.
|
|
1001
693
|
*/
|
|
694
|
+
export function normalizeSkillName(name: string): string {
|
|
695
|
+
return name
|
|
696
|
+
.toLowerCase()
|
|
697
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
698
|
+
.replace(/^-+|-+$/g, "")
|
|
699
|
+
.replace(/-{2,}/g, "-")
|
|
700
|
+
.slice(0, 64);
|
|
701
|
+
}
|
|
702
|
+
|
|
1002
703
|
export function substituteArguments(content: string, args: string): string {
|
|
1003
704
|
const parts = args.split(/\s+/);
|
|
1004
705
|
content = content.replace(/\$ARGUMENTS/g, args);
|
|
@@ -1007,10 +708,6 @@ export function substituteArguments(content: string, args: string): string {
|
|
|
1007
708
|
return content;
|
|
1008
709
|
}
|
|
1009
710
|
|
|
1010
|
-
/**
|
|
1011
|
-
* Preprocess dynamic context in skill content.
|
|
1012
|
-
* Replaces !`command` patterns with the command's stdout.
|
|
1013
|
-
*/
|
|
1014
711
|
export function preprocessDynamicContext(content: string, cwd?: string): string {
|
|
1015
712
|
return content.replace(/!`([^`]+)`/g, (_, cmd) => {
|
|
1016
713
|
try {
|