assistme 0.2.8 → 0.2.9
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 +434 -672
- 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 +26 -149
- package/src/agent/memory.test.ts +41 -65
- package/src/agent/memory.ts +33 -134
- package/src/agent/processor.ts +0 -3
- 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/skills.ts +89 -488
- 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,13 @@ 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
|
-
/** Variable definitions declared by the skill template */
|
|
107
|
-
variables?: SkillVariable[];
|
|
108
|
-
/** User-specific variable values (from agent_skills.config) */
|
|
109
|
-
config?: Record<string, unknown>;
|
|
79
|
+
dbId?: string;
|
|
80
|
+
sourceSkillId?: string;
|
|
110
81
|
}
|
|
111
82
|
|
|
112
|
-
/**
|
|
113
|
-
* Parse metadata JSONB from DB row into SkillMetadata.
|
|
114
|
-
*/
|
|
115
83
|
function parseDbMetadata(raw: unknown): SkillMetadata {
|
|
116
84
|
if (!raw || typeof raw !== "object") return {};
|
|
117
85
|
const obj = raw as Record<string, unknown>;
|
|
@@ -127,63 +95,30 @@ function parseDbMetadata(raw: unknown): SkillMetadata {
|
|
|
127
95
|
}
|
|
128
96
|
|
|
129
97
|
// ── 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
98
|
|
|
140
99
|
export class SkillManager {
|
|
141
100
|
private skills: Map<string, Skill> = new Map();
|
|
142
101
|
private idfCache: Map<string, number> = new Map();
|
|
143
102
|
private userId: string | null = null;
|
|
144
103
|
|
|
145
|
-
/** Budget for skill descriptions in system prompt (characters). */
|
|
146
104
|
private readonly DESCRIPTION_BUDGET_CHARS = 16_000;
|
|
147
105
|
|
|
148
|
-
/**
|
|
149
|
-
* Set the user ID for DB operations.
|
|
150
|
-
* Called after authentication; enables DB-backed skill storage.
|
|
151
|
-
*/
|
|
152
106
|
setUserId(userId: string): void {
|
|
153
107
|
this.userId = userId;
|
|
154
108
|
}
|
|
155
109
|
|
|
156
|
-
/**
|
|
157
|
-
* Load skills from DB (user's agent_skills collection).
|
|
158
|
-
* This is the only loading mechanism — no file-based loading.
|
|
159
|
-
*/
|
|
160
110
|
async loadFromDb(): Promise<void> {
|
|
161
111
|
if (!this.userId) return;
|
|
162
112
|
|
|
163
113
|
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
|
-
}
|
|
114
|
+
const data = await callMcpHandler<Record<string, unknown>[]>("skill.load");
|
|
177
115
|
|
|
178
116
|
this.skills.clear();
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
for (const row of dbRows) {
|
|
117
|
+
for (const row of data || []) {
|
|
182
118
|
const skill = this.rowToSkill(row);
|
|
183
119
|
this.skills.set(skill.name, skill);
|
|
184
120
|
}
|
|
185
121
|
|
|
186
|
-
// Build IDF cache for relevance matching
|
|
187
122
|
this.buildIdfCache();
|
|
188
123
|
|
|
189
124
|
if (this.skills.size > 0) {
|
|
@@ -194,19 +129,7 @@ export class SkillManager {
|
|
|
194
129
|
}
|
|
195
130
|
}
|
|
196
131
|
|
|
197
|
-
/**
|
|
198
|
-
* Convert a DB row to a Skill object.
|
|
199
|
-
*/
|
|
200
132
|
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
133
|
return {
|
|
211
134
|
name: row.name as string,
|
|
212
135
|
description: (row.description as string) || "",
|
|
@@ -223,15 +146,9 @@ export class SkillManager {
|
|
|
223
146
|
source: (row.source as Skill["source"]) || "manual",
|
|
224
147
|
dbId: row.id as string,
|
|
225
148
|
sourceSkillId: (row.source_skill_id as string) || undefined,
|
|
226
|
-
variables,
|
|
227
|
-
config,
|
|
228
149
|
};
|
|
229
150
|
}
|
|
230
151
|
|
|
231
|
-
/**
|
|
232
|
-
* Pre-compute IDF (Inverse Document Frequency) for all tokens across skills.
|
|
233
|
-
* Called once after loadFromDb(), avoids recomputing on every findRelevant() call.
|
|
234
|
-
*/
|
|
235
152
|
private buildIdfCache(): void {
|
|
236
153
|
this.idfCache.clear();
|
|
237
154
|
const docFreq = new Map<string, number>();
|
|
@@ -248,74 +165,42 @@ export class SkillManager {
|
|
|
248
165
|
}
|
|
249
166
|
}
|
|
250
167
|
|
|
251
|
-
/**
|
|
252
|
-
* Get all loaded skills (from user's agent_skills collection).
|
|
253
|
-
*/
|
|
254
168
|
getAll(): Skill[] {
|
|
255
169
|
return Array.from(this.skills.values());
|
|
256
170
|
}
|
|
257
171
|
|
|
258
|
-
/**
|
|
259
|
-
* Get a skill by name.
|
|
260
|
-
*/
|
|
261
172
|
get(name: string): Skill | undefined {
|
|
262
173
|
return this.skills.get(name);
|
|
263
174
|
}
|
|
264
175
|
|
|
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
176
|
findRelevant(prompt: string, maxResults = 3): Skill[] {
|
|
270
177
|
const lower = prompt.toLowerCase();
|
|
271
178
|
const promptTokens = tokenize(lower);
|
|
272
179
|
const promptTokenSet = new Set(promptTokens);
|
|
273
|
-
|
|
274
180
|
const idf = (word: string): number => this.idfCache.get(word) || 1;
|
|
275
|
-
|
|
276
181
|
const scored: Array<{ skill: Skill; score: number }> = [];
|
|
277
182
|
|
|
278
183
|
for (const skill of this.skills.values()) {
|
|
279
184
|
if (skill.disableModelInvocation) continue;
|
|
280
|
-
|
|
281
185
|
let score = 0;
|
|
282
|
-
|
|
283
|
-
// Name match (strongest signal)
|
|
284
186
|
if (lower.includes(skill.name.toLowerCase())) score += 10;
|
|
285
|
-
|
|
286
|
-
// Keywords match (explicit frontmatter keywords — high signal)
|
|
287
187
|
for (const kw of skill.keywords) {
|
|
288
|
-
if (lower.includes(kw.toLowerCase()))
|
|
289
|
-
score += 8;
|
|
290
|
-
}
|
|
188
|
+
if (lower.includes(kw.toLowerCase())) score += 8;
|
|
291
189
|
}
|
|
292
|
-
|
|
293
|
-
// Description word overlap with IDF weighting
|
|
294
190
|
const descTokens = tokenize(skill.description.toLowerCase());
|
|
295
191
|
for (const word of descTokens) {
|
|
296
|
-
if (promptTokenSet.has(word))
|
|
297
|
-
score += 3 * idf(word);
|
|
298
|
-
}
|
|
192
|
+
if (promptTokenSet.has(word)) score += 3 * idf(word);
|
|
299
193
|
}
|
|
300
|
-
|
|
301
|
-
// Content keyword matches with IDF weighting (lighter)
|
|
302
194
|
const contentTokens = tokenize(skill.content.toLowerCase());
|
|
303
195
|
for (const word of contentTokens) {
|
|
304
|
-
if (promptTokenSet.has(word))
|
|
305
|
-
score += 0.5 * idf(word);
|
|
306
|
-
}
|
|
196
|
+
if (promptTokenSet.has(word)) score += 0.5 * idf(word);
|
|
307
197
|
}
|
|
308
|
-
|
|
309
|
-
// Bigram matching: consecutive word pairs
|
|
310
198
|
const promptBigrams = bigrams(promptTokens);
|
|
311
199
|
const descBigrams = bigrams(descTokens);
|
|
312
200
|
for (const bg of descBigrams) {
|
|
313
201
|
if (promptBigrams.has(bg)) score += 5;
|
|
314
202
|
}
|
|
315
|
-
|
|
316
|
-
if (score > 0) {
|
|
317
|
-
scored.push({ skill, score });
|
|
318
|
-
}
|
|
203
|
+
if (score > 0) scored.push({ skill, score });
|
|
319
204
|
}
|
|
320
205
|
|
|
321
206
|
return scored
|
|
@@ -324,18 +209,12 @@ export class SkillManager {
|
|
|
324
209
|
.map((s) => s.skill);
|
|
325
210
|
}
|
|
326
211
|
|
|
327
|
-
/**
|
|
328
|
-
* Build a lightweight prompt section with skill descriptions only.
|
|
329
|
-
* Full skill content is loaded on-demand via the skill_invoke tool.
|
|
330
|
-
*/
|
|
331
212
|
buildSkillDescriptions(): string {
|
|
332
213
|
const skills = this.getAll()
|
|
333
214
|
.filter((s) => !s.disableModelInvocation)
|
|
334
215
|
.sort((a, b) => {
|
|
335
|
-
// always=true skills first
|
|
336
216
|
if (a.metadata.always && !b.metadata.always) return -1;
|
|
337
217
|
if (!a.metadata.always && b.metadata.always) return 1;
|
|
338
|
-
// Then shorter descriptions first (fit more skills in budget)
|
|
339
218
|
return a.description.length - b.description.length;
|
|
340
219
|
});
|
|
341
220
|
|
|
@@ -363,16 +242,13 @@ export class SkillManager {
|
|
|
363
242
|
return prompt;
|
|
364
243
|
}
|
|
365
244
|
|
|
366
|
-
/**
|
|
367
|
-
* @deprecated Use buildSkillDescriptions() + skill_invoke tool instead.
|
|
368
|
-
*/
|
|
245
|
+
/** @deprecated Use buildSkillDescriptions() + skill_invoke tool instead. */
|
|
369
246
|
buildSkillPrompt(taskPrompt: string): string {
|
|
370
247
|
const relevant = this.findRelevant(taskPrompt);
|
|
371
248
|
if (relevant.length === 0) return "";
|
|
372
249
|
|
|
373
250
|
let prompt = "\n\n## Available Skills\n";
|
|
374
|
-
prompt +=
|
|
375
|
-
"The following skills provide detailed instructions for this type of task:\n\n";
|
|
251
|
+
prompt += "The following skills provide detailed instructions for this type of task:\n\n";
|
|
376
252
|
|
|
377
253
|
for (const skill of relevant) {
|
|
378
254
|
const emoji = skill.metadata.emoji || "";
|
|
@@ -384,48 +260,30 @@ export class SkillManager {
|
|
|
384
260
|
return prompt;
|
|
385
261
|
}
|
|
386
262
|
|
|
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
263
|
async create(
|
|
395
264
|
name: string,
|
|
396
265
|
description: string,
|
|
397
266
|
content: string,
|
|
398
|
-
options?: { source?: string; emoji?: string; keywords?: string[]
|
|
267
|
+
options?: { source?: string; emoji?: string; keywords?: string[] }
|
|
399
268
|
): Promise<{ id: string; name: string } | null> {
|
|
400
269
|
if (!this.userId) return null;
|
|
401
270
|
|
|
402
271
|
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
|
-
}
|
|
272
|
+
const metadata = options?.emoji ? { openclaw: { emoji: options.emoji } } : {};
|
|
273
|
+
|
|
274
|
+
const data = await callMcpHandler<Record<string, unknown> | Record<string, unknown>[]>(
|
|
275
|
+
"skill.create",
|
|
276
|
+
{
|
|
277
|
+
name,
|
|
278
|
+
description,
|
|
279
|
+
content,
|
|
280
|
+
version: "1.0.0",
|
|
281
|
+
source: options?.source || "manual",
|
|
282
|
+
emoji: options?.emoji || null,
|
|
283
|
+
keywords: options?.keywords || [],
|
|
284
|
+
metadata,
|
|
285
|
+
},
|
|
286
|
+
);
|
|
429
287
|
|
|
430
288
|
const row = (Array.isArray(data) ? data[0] : data) as Record<string, unknown> | null;
|
|
431
289
|
if (!row) {
|
|
@@ -433,7 +291,6 @@ export class SkillManager {
|
|
|
433
291
|
return null;
|
|
434
292
|
}
|
|
435
293
|
|
|
436
|
-
// RPC returns out_id/out_name to avoid column ambiguity
|
|
437
294
|
const id = (row.out_id || row.id) as string;
|
|
438
295
|
const skillName = (row.out_name || row.name) as string;
|
|
439
296
|
|
|
@@ -445,63 +302,19 @@ export class SkillManager {
|
|
|
445
302
|
}
|
|
446
303
|
}
|
|
447
304
|
|
|
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
305
|
async addSkill(skillId: string): Promise<Skill | null> {
|
|
454
306
|
if (!this.userId) return null;
|
|
455
307
|
|
|
456
308
|
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>;
|
|
309
|
+
const result = await callMcpHandler<{ skill: Record<string, unknown>; agent_skill: unknown }>(
|
|
310
|
+
"skill.fetch_and_add",
|
|
311
|
+
{ skill_id: skillId },
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const row = result.skill;
|
|
315
|
+
const agentSkillRow = (result.agent_skill && typeof result.agent_skill === "object"
|
|
316
|
+
? result.agent_skill
|
|
317
|
+
: row) as Record<string, unknown>;
|
|
505
318
|
const skill = this.rowToSkill({
|
|
506
319
|
...agentSkillRow,
|
|
507
320
|
name: row.name,
|
|
@@ -520,28 +333,17 @@ export class SkillManager {
|
|
|
520
333
|
}
|
|
521
334
|
}
|
|
522
335
|
|
|
523
|
-
/**
|
|
524
|
-
* Remove a skill from user's collection (soft-delete in agent_skills).
|
|
525
|
-
*/
|
|
526
336
|
remove(name: string): boolean {
|
|
527
337
|
const skill = this.skills.get(name);
|
|
528
338
|
if (!skill) return false;
|
|
529
|
-
|
|
530
339
|
this.skills.delete(name);
|
|
531
|
-
|
|
532
|
-
// Mark inactive in DB (fire-and-forget)
|
|
533
340
|
this.removeFromDb(name).catch(() => {});
|
|
534
341
|
return true;
|
|
535
342
|
}
|
|
536
343
|
|
|
537
|
-
/**
|
|
538
|
-
* List skills with summary info.
|
|
539
|
-
*/
|
|
540
344
|
listFormatted(): string {
|
|
541
345
|
const skills = this.getAll();
|
|
542
|
-
if (skills.length === 0)
|
|
543
|
-
return "No skills in your collection.";
|
|
544
|
-
}
|
|
346
|
+
if (skills.length === 0) return "No skills in your collection.";
|
|
545
347
|
|
|
546
348
|
return skills
|
|
547
349
|
.map((s) => {
|
|
@@ -551,29 +353,15 @@ export class SkillManager {
|
|
|
551
353
|
.join("\n\n");
|
|
552
354
|
}
|
|
553
355
|
|
|
554
|
-
/**
|
|
555
|
-
* Check if a skill with a similar name already exists in user's collection.
|
|
556
|
-
*/
|
|
557
356
|
findSimilar(name: string): Skill | null {
|
|
558
|
-
// Exact match
|
|
559
357
|
if (this.skills.has(name)) return this.skills.get(name)!;
|
|
560
358
|
|
|
561
|
-
// Fuzzy match: check if names overlap significantly
|
|
562
359
|
const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
563
360
|
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
|
-
) {
|
|
361
|
+
const normalizedExisting = existingName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
362
|
+
if (normalizedName.includes(normalizedExisting) || normalizedExisting.includes(normalizedName)) {
|
|
573
363
|
return skill;
|
|
574
364
|
}
|
|
575
|
-
|
|
576
|
-
// High word overlap
|
|
577
365
|
const nameWords = new Set(name.split("-"));
|
|
578
366
|
const existingWords = new Set(existingName.split("-"));
|
|
579
367
|
let overlap = 0;
|
|
@@ -584,45 +372,33 @@ export class SkillManager {
|
|
|
584
372
|
return skill;
|
|
585
373
|
}
|
|
586
374
|
}
|
|
587
|
-
|
|
588
375
|
return null;
|
|
589
376
|
}
|
|
590
377
|
|
|
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
378
|
update(name: string, newContent: string, description?: string): boolean {
|
|
596
379
|
const skill = this.skills.get(name);
|
|
597
380
|
if (!skill) return false;
|
|
598
381
|
|
|
599
|
-
// Bump patch version
|
|
600
382
|
const versionParts = skill.version.split(".").map(Number);
|
|
601
383
|
versionParts[2] = (versionParts[2] || 0) + 1;
|
|
602
384
|
const newVersion = versionParts.join(".");
|
|
603
385
|
const newDescription = description || skill.description;
|
|
604
386
|
|
|
605
|
-
// Update in-memory
|
|
606
387
|
skill.content = newContent;
|
|
607
388
|
skill.description = newDescription;
|
|
608
389
|
skill.version = newVersion;
|
|
609
390
|
|
|
610
|
-
// Persist to agent_skills (fire-and-forget)
|
|
611
391
|
this.syncToAgentSkills(name, newDescription, newContent, newVersion, {
|
|
612
392
|
source: "auto_improved",
|
|
613
393
|
}).catch(() => {});
|
|
614
394
|
|
|
615
|
-
// Also update skills table if this skill has a source (fire-and-forget)
|
|
616
395
|
if (skill.sourceSkillId && this.userId) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
.eq("author_id", this.userId!);
|
|
624
|
-
} catch { /* non-critical */ }
|
|
625
|
-
})();
|
|
396
|
+
callMcpHandler("skill.update_source", {
|
|
397
|
+
source_skill_id: skill.sourceSkillId,
|
|
398
|
+
content: newContent,
|
|
399
|
+
description: newDescription,
|
|
400
|
+
version: newVersion,
|
|
401
|
+
}).catch(() => {});
|
|
626
402
|
}
|
|
627
403
|
|
|
628
404
|
log.info(`Skill "${name}" updated to v${newVersion}`);
|
|
@@ -631,10 +407,6 @@ export class SkillManager {
|
|
|
631
407
|
|
|
632
408
|
// ── DB Integration ─────────────────────────────────────────────────
|
|
633
409
|
|
|
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
410
|
async syncToAgentSkills(
|
|
639
411
|
name: string,
|
|
640
412
|
description: string,
|
|
@@ -646,46 +418,26 @@ export class SkillManager {
|
|
|
646
418
|
keywords?: string[];
|
|
647
419
|
changeSummary?: string;
|
|
648
420
|
sourceSkillId?: string;
|
|
649
|
-
variables?: SkillVariable[];
|
|
650
421
|
}
|
|
651
422
|
): Promise<void> {
|
|
652
423
|
if (!this.userId) return;
|
|
653
424
|
|
|
654
425
|
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,
|
|
426
|
+
const data = await callMcpHandler<Record<string, unknown> | null>("skill.upsert", {
|
|
427
|
+
name,
|
|
428
|
+
description,
|
|
429
|
+
content,
|
|
430
|
+
version,
|
|
431
|
+
source: options?.source || "manual",
|
|
432
|
+
emoji: options?.emoji || null,
|
|
433
|
+
keywords: options?.keywords || [],
|
|
434
|
+
change_summary: options?.changeSummary || null,
|
|
435
|
+
source_skill_id: options?.sourceSkillId || null,
|
|
675
436
|
});
|
|
676
437
|
|
|
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
438
|
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;
|
|
439
|
+
if (skill && data && typeof data === "object" && "id" in data) {
|
|
440
|
+
skill.dbId = data.id as string;
|
|
689
441
|
}
|
|
690
442
|
|
|
691
443
|
log.debug(`Skill "${name}" synced to agent_skills`);
|
|
@@ -694,10 +446,6 @@ export class SkillManager {
|
|
|
694
446
|
}
|
|
695
447
|
}
|
|
696
448
|
|
|
697
|
-
/**
|
|
698
|
-
* Log a skill invocation to agent_skill_invocations.
|
|
699
|
-
* Fire-and-forget: failures don't block task execution.
|
|
700
|
-
*/
|
|
701
449
|
async logInvocation(
|
|
702
450
|
skillName: string,
|
|
703
451
|
options?: {
|
|
@@ -718,29 +466,19 @@ export class SkillManager {
|
|
|
718
466
|
}
|
|
719
467
|
|
|
720
468
|
try {
|
|
721
|
-
|
|
722
|
-
const { error } = await sb.from("agent_skill_invocations").insert({
|
|
469
|
+
await callMcpHandler("skill.log_invocation", {
|
|
723
470
|
skill_id: skillDbId,
|
|
724
|
-
user_id: this.userId,
|
|
725
471
|
message_id: options?.messageId || null,
|
|
726
472
|
session_id: options?.sessionId || null,
|
|
727
473
|
task_prompt: options?.taskPrompt?.slice(0, 500) || null,
|
|
728
474
|
arguments: options?.arguments || null,
|
|
729
475
|
success: options?.success ?? null,
|
|
730
476
|
});
|
|
731
|
-
|
|
732
|
-
if (error) {
|
|
733
|
-
log.debug(`Invocation log failed: ${error.message}`);
|
|
734
|
-
}
|
|
735
477
|
} catch (err) {
|
|
736
478
|
log.debug(`Invocation log error: ${err}`);
|
|
737
479
|
}
|
|
738
480
|
}
|
|
739
481
|
|
|
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
482
|
async searchDb(query: string, limit = 10): Promise<Array<{
|
|
745
483
|
name: string;
|
|
746
484
|
description: string;
|
|
@@ -748,19 +486,11 @@ export class SkillManager {
|
|
|
748
486
|
source: string;
|
|
749
487
|
invocationCount: number;
|
|
750
488
|
}>> {
|
|
751
|
-
// Try DB search first
|
|
752
489
|
if (this.userId) {
|
|
753
490
|
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) => ({
|
|
491
|
+
const data = await callMcpHandler<Record<string, unknown>[]>("skill.search", { query, limit });
|
|
492
|
+
if (data) {
|
|
493
|
+
return data.map((row) => ({
|
|
764
494
|
name: row.name as string,
|
|
765
495
|
description: (row.description as string) || "",
|
|
766
496
|
emoji: (row.emoji as string) || "",
|
|
@@ -768,12 +498,9 @@ export class SkillManager {
|
|
|
768
498
|
invocationCount: (row.invocation_count as number) || 0,
|
|
769
499
|
}));
|
|
770
500
|
}
|
|
771
|
-
} catch {
|
|
772
|
-
// Fall through to in-memory search
|
|
773
|
-
}
|
|
501
|
+
} catch { /* Fall through to in-memory search */ }
|
|
774
502
|
}
|
|
775
503
|
|
|
776
|
-
// Fallback: in-memory search
|
|
777
504
|
const results = this.findRelevant(query, limit);
|
|
778
505
|
return results.map((s) => ({
|
|
779
506
|
name: s.name,
|
|
@@ -784,88 +511,52 @@ export class SkillManager {
|
|
|
784
511
|
}));
|
|
785
512
|
}
|
|
786
513
|
|
|
787
|
-
/**
|
|
788
|
-
* Remove a skill from DB (soft-delete).
|
|
789
|
-
*/
|
|
790
514
|
async removeFromDb(name: string): Promise<void> {
|
|
791
515
|
if (!this.userId) return;
|
|
792
|
-
|
|
793
516
|
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
|
-
}
|
|
517
|
+
await callMcpHandler("skill.remove", { name });
|
|
518
|
+
} catch { /* Non-critical */ }
|
|
803
519
|
}
|
|
804
520
|
|
|
805
521
|
// ── Marketplace ────────────────────────────────────────────────────
|
|
806
522
|
|
|
807
|
-
/**
|
|
808
|
-
* Publish a skill to the marketplace (sets is_public = true in skills table).
|
|
809
|
-
*/
|
|
810
523
|
async publish(
|
|
811
524
|
name: string,
|
|
812
525
|
options?: { category?: string; authorName?: string }
|
|
813
526
|
): Promise<{ id: string } | null> {
|
|
814
527
|
if (!this.userId) return null;
|
|
815
|
-
|
|
816
528
|
const skill = this.skills.get(name);
|
|
817
529
|
if (!skill) return null;
|
|
818
|
-
|
|
819
530
|
if (skill.source === "external") {
|
|
820
531
|
log.debug(`Cannot publish external skill "${name}"`);
|
|
821
532
|
return null;
|
|
822
533
|
}
|
|
823
534
|
|
|
824
535
|
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
|
-
}
|
|
536
|
+
const data = await callMcpHandler<{ id: string }>("skill.publish", {
|
|
537
|
+
name: skill.name,
|
|
538
|
+
description: skill.description,
|
|
539
|
+
version: skill.version,
|
|
540
|
+
emoji: skill.metadata.emoji || null,
|
|
541
|
+
content: skill.content,
|
|
542
|
+
argument_hint: skill.argumentHint || null,
|
|
543
|
+
keywords: skill.keywords,
|
|
544
|
+
allowed_tools: skill.allowedTools,
|
|
545
|
+
author_name: options?.authorName || null,
|
|
546
|
+
metadata: skill.metadata,
|
|
547
|
+
homepage: skill.homepage || null,
|
|
548
|
+
category: options?.category || null,
|
|
549
|
+
source: skill.source,
|
|
550
|
+
});
|
|
857
551
|
|
|
858
552
|
log.info(`Skill "${name}" published to marketplace`);
|
|
859
|
-
return data
|
|
553
|
+
return data;
|
|
860
554
|
} catch (err) {
|
|
861
555
|
log.debug(`Publish error: ${err}`);
|
|
862
556
|
return null;
|
|
863
557
|
}
|
|
864
558
|
}
|
|
865
559
|
|
|
866
|
-
/**
|
|
867
|
-
* Browse marketplace skills (public + active).
|
|
868
|
-
*/
|
|
869
560
|
async browse(options?: {
|
|
870
561
|
query?: string;
|
|
871
562
|
category?: string;
|
|
@@ -885,18 +576,15 @@ export class SkillManager {
|
|
|
885
576
|
ratingCount: number;
|
|
886
577
|
}>> {
|
|
887
578
|
try {
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
p_offset: options?.offset || 0,
|
|
579
|
+
const data = await callMcpHandler<Record<string, unknown>[]>("skill.browse", {
|
|
580
|
+
query: options?.query || null,
|
|
581
|
+
category: options?.category || null,
|
|
582
|
+
sort: options?.sort || "popular",
|
|
583
|
+
limit: options?.limit || 20,
|
|
584
|
+
offset: options?.offset || 0,
|
|
895
585
|
});
|
|
896
586
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
return (data as Record<string, unknown>[]).map((r) => ({
|
|
587
|
+
return (data || []).map((r) => ({
|
|
900
588
|
id: r.id as string,
|
|
901
589
|
name: r.name as string,
|
|
902
590
|
description: (r.description as string) || "",
|
|
@@ -912,93 +600,10 @@ export class SkillManager {
|
|
|
912
600
|
return [];
|
|
913
601
|
}
|
|
914
602
|
}
|
|
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
603
|
}
|
|
966
604
|
|
|
967
605
|
// ── Exported Utility Functions ─────────────────────────────────────
|
|
968
606
|
|
|
969
|
-
/**
|
|
970
|
-
* Substitute {{variable_name}} placeholders in skill content with user config values.
|
|
971
|
-
* Also handles array values by joining with ", ".
|
|
972
|
-
*
|
|
973
|
-
* Unresolved variables are left as-is with a note: {{variable_name: [NOT CONFIGURED]}}
|
|
974
|
-
*/
|
|
975
|
-
export function substituteVariables(
|
|
976
|
-
content: string,
|
|
977
|
-
config: Record<string, unknown> | undefined,
|
|
978
|
-
variables?: SkillVariable[]
|
|
979
|
-
): string {
|
|
980
|
-
if (!content.includes("{{")) return content;
|
|
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
|
-
});
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* Substitute $ARGUMENTS, $ARGUMENTS[N], and $N placeholders in skill content.
|
|
1001
|
-
*/
|
|
1002
607
|
export function substituteArguments(content: string, args: string): string {
|
|
1003
608
|
const parts = args.split(/\s+/);
|
|
1004
609
|
content = content.replace(/\$ARGUMENTS/g, args);
|
|
@@ -1007,10 +612,6 @@ export function substituteArguments(content: string, args: string): string {
|
|
|
1007
612
|
return content;
|
|
1008
613
|
}
|
|
1009
614
|
|
|
1010
|
-
/**
|
|
1011
|
-
* Preprocess dynamic context in skill content.
|
|
1012
|
-
* Replaces !`command` patterns with the command's stdout.
|
|
1013
|
-
*/
|
|
1014
615
|
export function preprocessDynamicContext(content: string, cwd?: string): string {
|
|
1015
616
|
return content.replace(/!`([^`]+)`/g, (_, cmd) => {
|
|
1016
617
|
try {
|