assistme 0.2.7 → 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.
@@ -1,6 +1,6 @@
1
1
  import { execSync } from "child_process";
2
2
  import { log } from "../utils/logger.js";
3
- import { getSupabase } from "../db/supabase.js";
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; // The markdown body (instructions)
102
- filePath: string; // Empty for DB-only skills
76
+ content: string;
77
+ filePath: string;
103
78
  source: "bundled" | "manual" | "external" | "auto_extracted" | "auto_improved" | "job_generated";
104
- dbId?: string; // UUID from agent_skills table
105
- sourceSkillId?: string; // UUID from skills table (origin)
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 sb = getSupabase();
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 dbRows = (data || []) as Record<string, unknown>[];
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[]; variables?: SkillVariable[] }
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 sb = getSupabase();
404
- const source = options?.source || "manual";
405
- const metadata: Record<string, unknown> = {};
406
- if (options?.emoji) {
407
- metadata.openclaw = { emoji: options.emoji };
408
- }
409
- if (options?.variables && options.variables.length > 0) {
410
- metadata.variables = options.variables;
411
- }
412
-
413
- const { data, error } = await sb.rpc("create_skill", {
414
- p_user_id: this.userId,
415
- p_name: name,
416
- p_description: description,
417
- p_content: content,
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 sb = getSupabase();
458
-
459
- // Fetch from skills table
460
- const { data: pub, error: fetchErr } = await sb
461
- .from("skills")
462
- .select("*")
463
- .eq("id", skillId)
464
- .eq("is_active", true)
465
- .single();
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
- .toLowerCase()
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
- const sb = getSupabase();
618
- void (async () => {
619
- try {
620
- await sb.from("skills")
621
- .update({ content: newContent, description: newDescription, version: newVersion })
622
- .eq("id", skill.sourceSkillId!)
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 sb = getSupabase();
656
-
657
- // Store variables in metadata for agent_skills
658
- const metadata: Record<string, unknown> = {};
659
- if (options?.variables && options.variables.length > 0) {
660
- metadata.variables = options.variables;
661
- }
662
-
663
- const { data, error } = await sb.rpc("upsert_agent_skill", {
664
- p_user_id: this.userId,
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 (data as Record<string, unknown>)) {
685
- skill.dbId = (data as Record<string, unknown>).id as string;
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
- const sb = getSupabase();
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 sb = getSupabase();
755
- const { data, error } = await sb.rpc("search_agent_skills", {
756
- p_user_id: this.userId,
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
- const sb = getSupabase();
795
- await sb
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 sb = getSupabase();
826
-
827
- // Upsert to skills table with is_public = true
828
- const { data, error } = await sb
829
- .from("skills")
830
- .upsert(
831
- {
832
- name: skill.name,
833
- description: skill.description,
834
- version: skill.version,
835
- emoji: skill.metadata.emoji || null,
836
- content: skill.content,
837
- argument_hint: skill.argumentHint || null,
838
- keywords: skill.keywords,
839
- allowed_tools: skill.allowedTools,
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 as { id: string };
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 sb = getSupabase();
889
- const { data, error } = await sb.rpc("browse_skills", {
890
- p_query: options?.query || null,
891
- p_category: options?.category || null,
892
- p_sort: options?.sort || "popular",
893
- p_limit: options?.limit || 20,
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
- if (error || !data) return [];
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 {