assistme 0.2.6 → 0.2.7

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/index.js CHANGED
@@ -1812,6 +1812,11 @@ var SkillManager = class {
1812
1812
  * Convert a DB row to a Skill object.
1813
1813
  */
1814
1814
  rowToSkill(row) {
1815
+ const meta = row.metadata || {};
1816
+ const rawVars = row.variables || meta.variables;
1817
+ const variables = Array.isArray(rawVars) ? rawVars : void 0;
1818
+ const rawConfig = row.config;
1819
+ const config = rawConfig && typeof rawConfig === "object" ? rawConfig : void 0;
1815
1820
  return {
1816
1821
  name: row.name,
1817
1822
  description: row.description || "",
@@ -1827,7 +1832,9 @@ var SkillManager = class {
1827
1832
  filePath: "",
1828
1833
  source: row.source || "manual",
1829
1834
  dbId: row.id,
1830
- sourceSkillId: row.source_skill_id || void 0
1835
+ sourceSkillId: row.source_skill_id || void 0,
1836
+ variables,
1837
+ config
1831
1838
  };
1832
1839
  }
1833
1840
  /**
@@ -1966,7 +1973,13 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
1966
1973
  try {
1967
1974
  const sb = getSupabase();
1968
1975
  const source = options?.source || "manual";
1969
- const metadata = options?.emoji ? { openclaw: { emoji: options.emoji } } : {};
1976
+ const metadata = {};
1977
+ if (options?.emoji) {
1978
+ metadata.openclaw = { emoji: options.emoji };
1979
+ }
1980
+ if (options?.variables && options.variables.length > 0) {
1981
+ metadata.variables = options.variables;
1982
+ }
1970
1983
  const { data, error } = await sb.rpc("create_skill", {
1971
1984
  p_user_id: this.userId,
1972
1985
  p_name: name,
@@ -2138,6 +2151,10 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2138
2151
  if (!this.userId) return;
2139
2152
  try {
2140
2153
  const sb = getSupabase();
2154
+ const metadata = {};
2155
+ if (options?.variables && options.variables.length > 0) {
2156
+ metadata.variables = options.variables;
2157
+ }
2141
2158
  const { data, error } = await sb.rpc("upsert_agent_skill", {
2142
2159
  p_user_id: this.userId,
2143
2160
  p_name: name,
@@ -2148,7 +2165,8 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2148
2165
  p_emoji: options?.emoji || null,
2149
2166
  p_keywords: options?.keywords || [],
2150
2167
  p_change_summary: options?.changeSummary || null,
2151
- p_source_skill_id: options?.sourceSkillId || null
2168
+ p_source_skill_id: options?.sourceSkillId || null,
2169
+ p_metadata: Object.keys(metadata).length > 0 ? metadata : null
2152
2170
  });
2153
2171
  if (error) {
2154
2172
  log.debug(`DB skill sync failed for "${name}": ${error.message}`);
@@ -2158,6 +2176,9 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2158
2176
  if (skill && data && typeof data === "object" && "id" in data) {
2159
2177
  skill.dbId = data.id;
2160
2178
  }
2179
+ if (skill && options?.variables) {
2180
+ skill.variables = options.variables;
2181
+ }
2161
2182
  log.debug(`Skill "${name}" synced to agent_skills`);
2162
2183
  } catch (err) {
2163
2184
  log.debug(`DB skill sync error for "${name}": ${err}`);
@@ -2314,7 +2335,54 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2314
2335
  return [];
2315
2336
  }
2316
2337
  }
2338
+ // ── Variable Configuration ──────────────────────────────────────────
2339
+ /**
2340
+ * Update user-specific variable values (config) for a skill.
2341
+ * Persists to agent_skills.config in DB and updates in-memory.
2342
+ */
2343
+ async updateConfig(skillName, config) {
2344
+ const skill = this.skills.get(skillName);
2345
+ if (!skill) return false;
2346
+ const merged = { ...skill.config || {}, ...config };
2347
+ skill.config = merged;
2348
+ if (this.userId && skill.dbId) {
2349
+ try {
2350
+ const sb = getSupabase();
2351
+ await sb.from("agent_skills").update({ config: merged }).eq("id", skill.dbId).eq("user_id", this.userId);
2352
+ log.debug(`Config updated for skill "${skillName}"`);
2353
+ } catch (err) {
2354
+ log.debug(`Config update failed for "${skillName}": ${err}`);
2355
+ }
2356
+ }
2357
+ return true;
2358
+ }
2359
+ /**
2360
+ * Get the unresolved (unconfigured) variables for a skill.
2361
+ * Returns variables that are required but have no value in config.
2362
+ */
2363
+ getUnconfiguredVariables(skillName) {
2364
+ const skill = this.skills.get(skillName);
2365
+ if (!skill?.variables) return [];
2366
+ return skill.variables.filter((v) => {
2367
+ if (!v.required) return false;
2368
+ const value = skill.config?.[v.name];
2369
+ return value === void 0 || value === null || value === "";
2370
+ });
2371
+ }
2317
2372
  };
2373
+ function substituteVariables(content, config, variables) {
2374
+ if (!content.includes("{{")) return content;
2375
+ return content.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
2376
+ const value = config?.[varName];
2377
+ if (value !== void 0 && value !== null) {
2378
+ if (Array.isArray(value)) return value.join(", ");
2379
+ return String(value);
2380
+ }
2381
+ const varDef = variables?.find((v) => v.name === varName);
2382
+ if (varDef?.default !== void 0) return varDef.default;
2383
+ return `{{${varName}: [NOT CONFIGURED]}}`;
2384
+ });
2385
+ }
2318
2386
  function substituteArguments(content, args) {
2319
2387
  const parts = args.split(/\s+/);
2320
2388
  content = content.replace(/\$ARGUMENTS/g, args);
@@ -2976,12 +3044,24 @@ function createAgentToolsServer(deps) {
2976
3044
  ),
2977
3045
  tool(
2978
3046
  "skill_create",
2979
- "Create a new skill and add it to the user's collection. Returns the skill ID on success.",
3047
+ "Create a new skill and add it to the user's collection. Returns the skill ID on success. Use {{variable_name}} syntax in instructions for user-specific data (e.g. {{github_repos}}, {{slack_channel}}).",
2980
3048
  {
2981
3049
  name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
2982
3050
  description: z.string().describe("One-line description of what this skill does"),
2983
- instructions: z.string().describe("Markdown step-by-step instructions"),
2984
- emoji: z.string().optional().describe("Single emoji representing this skill")
3051
+ instructions: z.string().describe(
3052
+ "Markdown step-by-step instructions. Use {{variable_name}} placeholders for user-specific data (e.g. {{github_repos}}, {{trello_board}}, {{slack_channel}}). These will be resolved with user's config at invoke time."
3053
+ ),
3054
+ emoji: z.string().optional().describe("Single emoji representing this skill"),
3055
+ variables: z.array(z.object({
3056
+ name: z.string().describe("Variable name matching {{name}} in instructions"),
3057
+ description: z.string().describe("Human-readable description of what this variable is for"),
3058
+ type: z.enum(["string", "string[]", "number", "boolean"]).describe("Data type"),
3059
+ required: z.boolean().describe("Whether the skill needs this to function"),
3060
+ default: z.string().optional().describe("Default value if user doesn't configure"),
3061
+ example: z.string().optional().describe("Example value to guide the user")
3062
+ })).optional().describe(
3063
+ "Variables that need user-specific configuration. Define one for each {{variable}} used in instructions."
3064
+ )
2985
3065
  },
2986
3066
  async (args) => {
2987
3067
  const existing = skillManager.findSimilar(args.name);
@@ -2999,7 +3079,11 @@ function createAgentToolsServer(deps) {
2999
3079
  args.name,
3000
3080
  args.description,
3001
3081
  args.instructions,
3002
- { source: "manual", emoji: args.emoji }
3082
+ {
3083
+ source: "manual",
3084
+ emoji: args.emoji,
3085
+ variables: args.variables
3086
+ }
3003
3087
  );
3004
3088
  if (!result) {
3005
3089
  return {
@@ -3014,17 +3098,30 @@ function createAgentToolsServer(deps) {
3014
3098
  {
3015
3099
  source: "manual",
3016
3100
  emoji: args.emoji,
3017
- sourceSkillId: result.id
3101
+ sourceSkillId: result.id,
3102
+ variables: args.variables
3018
3103
  }
3019
3104
  );
3105
+ let responseText = `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`;
3106
+ if (args.variables && args.variables.length > 0) {
3107
+ const requiredVars = args.variables.filter((v) => v.required);
3108
+ if (requiredVars.length > 0) {
3109
+ responseText += `
3110
+
3111
+ **Variables that need configuration:**
3112
+ `;
3113
+ for (const v of requiredVars) {
3114
+ const example = v.example ? ` (e.g. ${v.example})` : "";
3115
+ responseText += `- \`{{${v.name}}}\`: ${v.description}${example}
3116
+ `;
3117
+ }
3118
+ responseText += `
3119
+ Use \`skill_configure\` to set these values for the user, or ask the user to provide them.`;
3120
+ }
3121
+ }
3020
3122
  log.success(`Skill "${args.name}" created and added to collection`);
3021
3123
  return {
3022
- content: [
3023
- {
3024
- type: "text",
3025
- text: `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`
3026
- }
3027
- ]
3124
+ content: [{ type: "text", text: responseText }]
3028
3125
  };
3029
3126
  }
3030
3127
  ),
@@ -3096,6 +3193,7 @@ function createAgentToolsServer(deps) {
3096
3193
  };
3097
3194
  }
3098
3195
  let content = skill.content;
3196
+ content = substituteVariables(content, skill.config, skill.variables);
3099
3197
  if (args.arguments) {
3100
3198
  content = substituteArguments(content, args.arguments);
3101
3199
  }
@@ -3114,6 +3212,18 @@ ${content}`;
3114
3212
  **Allowed tools for this skill:** ${skill.allowedTools.join(", ")}
3115
3213
  `;
3116
3214
  }
3215
+ const unconfigured = skillManager.getUnconfiguredVariables(args.name);
3216
+ if (unconfigured.length > 0) {
3217
+ response += `
3218
+
3219
+ **\u26A0 Unconfigured variables \u2014 use \`skill_configure\` to set these:**
3220
+ `;
3221
+ for (const v of unconfigured) {
3222
+ const example = v.example ? ` (e.g. ${v.example})` : "";
3223
+ response += `- \`{{${v.name}}}\`: ${v.description}${example}
3224
+ `;
3225
+ }
3226
+ }
3117
3227
  log.info(`Skill invoked: "${args.name}"`);
3118
3228
  skillManager.logInvocation(args.name, {
3119
3229
  messageId: taskId,
@@ -3153,6 +3263,53 @@ ${content}`;
3153
3263
  return { content: [{ type: "text", text: response }] };
3154
3264
  }
3155
3265
  ),
3266
+ tool(
3267
+ "skill_configure",
3268
+ "Set user-specific variable values for a skill. Variables are defined in the skill template (e.g. {{github_repos}}) and need to be configured with the user's actual data before the skill can work properly.",
3269
+ {
3270
+ name: z.string().describe("Name of the skill to configure"),
3271
+ config: z.record(z.unknown()).describe(
3272
+ 'Key-value pairs of variable values. Keys must match variable names defined in the skill. Example: {"github_repos": ["octocat/hello-world", "myorg/myrepo"], "github_username": "octocat"}'
3273
+ )
3274
+ },
3275
+ async (args) => {
3276
+ const skill = skillManager.get(args.name);
3277
+ if (!skill) {
3278
+ const available = skillManager.getAll().map((s) => s.name).join(", ");
3279
+ return {
3280
+ content: [{
3281
+ type: "text",
3282
+ text: `Skill "${args.name}" not found. Available: ${available}`
3283
+ }]
3284
+ };
3285
+ }
3286
+ const updated = await skillManager.updateConfig(args.name, args.config);
3287
+ if (!updated) {
3288
+ return {
3289
+ content: [{ type: "text", text: `Failed to update config for "${args.name}".` }]
3290
+ };
3291
+ }
3292
+ const unconfigured = skillManager.getUnconfiguredVariables(args.name);
3293
+ let responseText = `Configuration updated for skill "${args.name}".`;
3294
+ if (unconfigured.length > 0) {
3295
+ responseText += `
3296
+
3297
+ **Still needs configuration:**
3298
+ `;
3299
+ for (const v of unconfigured) {
3300
+ const example = v.example ? ` (e.g. ${v.example})` : "";
3301
+ responseText += `- \`{{${v.name}}}\`: ${v.description}${example}
3302
+ `;
3303
+ }
3304
+ } else {
3305
+ responseText += ` All required variables are configured.`;
3306
+ }
3307
+ log.info(`Config updated for skill "${args.name}": ${Object.keys(args.config).join(", ")}`);
3308
+ return {
3309
+ content: [{ type: "text", text: responseText }]
3310
+ };
3311
+ }
3312
+ ),
3156
3313
  tool(
3157
3314
  "skill_generate",
3158
3315
  "Prepare context for generating skills from a job description. Returns existing skills and job info so you can analyze the job and create skills using skill_create (which auto-adds to user's collection). After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
@@ -3201,6 +3358,8 @@ ${content}`;
3201
3358
  response += `- instructions: detailed step-by-step markdown instructions the agent can follow
3202
3359
  `;
3203
3360
  response += `- emoji: a single emoji representing the skill
3361
+ `;
3362
+ response += `- variables: define user-specific data needed by the skill (see below)
3204
3363
 
3205
3364
  `;
3206
3365
  response += `skill_create automatically adds the skill to the user's collection \u2014 no need to call skill_add.
@@ -3216,10 +3375,60 @@ ${content}`;
3216
3375
  response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks
3217
3376
  `;
3218
3377
  response += `- Include error handling steps
3219
- `;
3220
- response += `- Use placeholders like {query}, {date} for variable inputs
3221
3378
  `;
3222
3379
  response += `- Each skill should be a single, well-defined workflow (10-25 steps)
3380
+
3381
+ `;
3382
+ response += `**IMPORTANT \u2014 Use {{variables}} for user-specific data:**
3383
+ `;
3384
+ response += `Skills often need user-specific information (accounts, repos, boards, channels, etc.).
3385
+ `;
3386
+ response += `Instead of hardcoding or asking at runtime, use \`{{variable_name}}\` placeholders in instructions.
3387
+
3388
+ `;
3389
+ response += `Example: A GitHub PR review skill should use \`{{github_repos}}\` in its instructions:
3390
+ `;
3391
+ response += ` "Navigate to each repository in {{github_repos}} and check for open PRs..."
3392
+
3393
+ `;
3394
+ response += `For each \`{{variable}}\` used in instructions, add a matching entry in the \`variables\` array:
3395
+ `;
3396
+ response += `\`\`\`json
3397
+ `;
3398
+ response += `{
3399
+ `;
3400
+ response += ` "name": "github_repos",
3401
+ `;
3402
+ response += ` "description": "GitHub repositories to monitor (owner/repo format)",
3403
+ `;
3404
+ response += ` "type": "string[]",
3405
+ `;
3406
+ response += ` "required": true,
3407
+ `;
3408
+ response += ` "example": "octocat/hello-world, myorg/myrepo"
3409
+ `;
3410
+ response += `}
3411
+ `;
3412
+ response += `\`\`\`
3413
+
3414
+ `;
3415
+ response += `Common variables by platform:
3416
+ `;
3417
+ response += `- GitHub: github_repos, github_username, github_org
3418
+ `;
3419
+ response += `- Trello: trello_board_url, trello_list_names
3420
+ `;
3421
+ response += `- Slack: slack_channels, slack_workspace_url
3422
+ `;
3423
+ response += `- Email: email_labels, email_filters
3424
+ `;
3425
+ response += `- E-commerce: store_url, competitor_urls, product_categories
3426
+ `;
3427
+ response += `- General: timezone, language, notification_channel
3428
+
3429
+ `;
3430
+ response += `After creating skills, if any have required variables, call \`skill_configure\` to set initial values `;
3431
+ response += `(ask the user for the values first).
3223
3432
  `;
3224
3433
  return { content: [{ type: "text", text: response }] };
3225
3434
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,8 +8,8 @@ import { executeTool } from "../tools/index.js";
8
8
  import { getLimiterForTool } from "../utils/rate-limiter.js";
9
9
  import { log } from "../utils/logger.js";
10
10
  import type { MemoryManager, MemoryCategory } from "./memory.js";
11
- import type { SkillManager } from "./skills.js";
12
- import { substituteArguments, preprocessDynamicContext } from "./skills.js";
11
+ import type { SkillManager, SkillVariable } from "./skills.js";
12
+ import { substituteArguments, substituteVariables, preprocessDynamicContext } from "./skills.js";
13
13
  import { getSupabase, emitEvent, setActionRequest, pollActionResponse } from "../db/supabase.js";
14
14
  import { JobRunner } from "./job-runner.js";
15
15
  import {
@@ -214,12 +214,26 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
214
214
  ),
215
215
  tool(
216
216
  "skill_create",
217
- "Create a new skill and add it to the user's collection. Returns the skill ID on success.",
217
+ "Create a new skill and add it to the user's collection. Returns the skill ID on success. " +
218
+ "Use {{variable_name}} syntax in instructions for user-specific data (e.g. {{github_repos}}, {{slack_channel}}).",
218
219
  {
219
220
  name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
220
221
  description: z.string().describe("One-line description of what this skill does"),
221
- instructions: z.string().describe("Markdown step-by-step instructions"),
222
+ instructions: z.string().describe(
223
+ "Markdown step-by-step instructions. Use {{variable_name}} placeholders for user-specific data " +
224
+ "(e.g. {{github_repos}}, {{trello_board}}, {{slack_channel}}). These will be resolved with user's config at invoke time."
225
+ ),
222
226
  emoji: z.string().optional().describe("Single emoji representing this skill"),
227
+ variables: z.array(z.object({
228
+ name: z.string().describe("Variable name matching {{name}} in instructions"),
229
+ description: z.string().describe("Human-readable description of what this variable is for"),
230
+ type: z.enum(["string", "string[]", "number", "boolean"]).describe("Data type"),
231
+ required: z.boolean().describe("Whether the skill needs this to function"),
232
+ default: z.string().optional().describe("Default value if user doesn't configure"),
233
+ example: z.string().optional().describe("Example value to guide the user"),
234
+ })).optional().describe(
235
+ "Variables that need user-specific configuration. Define one for each {{variable}} used in instructions."
236
+ ),
223
237
  },
224
238
  async (args) => {
225
239
  // Check for duplicates in user's collection
@@ -240,7 +254,11 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
240
254
  args.name,
241
255
  args.description,
242
256
  args.instructions,
243
- { source: "manual", emoji: args.emoji }
257
+ {
258
+ source: "manual",
259
+ emoji: args.emoji,
260
+ variables: args.variables as SkillVariable[] | undefined,
261
+ }
244
262
  );
245
263
 
246
264
  if (!result) {
@@ -259,17 +277,27 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
259
277
  source: "manual",
260
278
  emoji: args.emoji,
261
279
  sourceSkillId: result.id,
280
+ variables: args.variables as SkillVariable[] | undefined,
262
281
  }
263
282
  );
264
283
 
284
+ // Build response with variable info
285
+ let responseText = `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`;
286
+ if (args.variables && args.variables.length > 0) {
287
+ const requiredVars = args.variables.filter((v) => v.required);
288
+ if (requiredVars.length > 0) {
289
+ responseText += `\n\n**Variables that need configuration:**\n`;
290
+ for (const v of requiredVars) {
291
+ const example = v.example ? ` (e.g. ${v.example})` : "";
292
+ responseText += `- \`{{${v.name}}}\`: ${v.description}${example}\n`;
293
+ }
294
+ responseText += `\nUse \`skill_configure\` to set these values for the user, or ask the user to provide them.`;
295
+ }
296
+ }
297
+
265
298
  log.success(`Skill "${args.name}" created and added to collection`);
266
299
  return {
267
- content: [
268
- {
269
- type: "text",
270
- text: `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`,
271
- },
272
- ],
300
+ content: [{ type: "text", text: responseText }],
273
301
  };
274
302
  }
275
303
  ),
@@ -354,6 +382,9 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
354
382
 
355
383
  let content = skill.content;
356
384
 
385
+ // Substitute {{variable}} placeholders with user config values
386
+ content = substituteVariables(content, skill.config, skill.variables);
387
+
357
388
  // Substitute $ARGUMENTS placeholders
358
389
  if (args.arguments) {
359
390
  content = substituteArguments(content, args.arguments);
@@ -374,6 +405,16 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
374
405
  response += `\n\n**Allowed tools for this skill:** ${skill.allowedTools.join(", ")}\n`;
375
406
  }
376
407
 
408
+ // Warn about unconfigured required variables
409
+ const unconfigured = skillManager.getUnconfiguredVariables(args.name);
410
+ if (unconfigured.length > 0) {
411
+ response += `\n\n**⚠ Unconfigured variables — use \`skill_configure\` to set these:**\n`;
412
+ for (const v of unconfigured) {
413
+ const example = v.example ? ` (e.g. ${v.example})` : "";
414
+ response += `- \`{{${v.name}}}\`: ${v.description}${example}\n`;
415
+ }
416
+ }
417
+
377
418
  log.info(`Skill invoked: "${args.name}"`);
378
419
 
379
420
  // Log invocation to DB (fire-and-forget)
@@ -416,6 +457,56 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
416
457
  return { content: [{ type: "text", text: response }] };
417
458
  }
418
459
  ),
460
+ tool(
461
+ "skill_configure",
462
+ "Set user-specific variable values for a skill. Variables are defined in the skill template (e.g. {{github_repos}}) " +
463
+ "and need to be configured with the user's actual data before the skill can work properly.",
464
+ {
465
+ name: z.string().describe("Name of the skill to configure"),
466
+ config: z.record(z.unknown()).describe(
467
+ "Key-value pairs of variable values. Keys must match variable names defined in the skill. " +
468
+ "Example: {\"github_repos\": [\"octocat/hello-world\", \"myorg/myrepo\"], \"github_username\": \"octocat\"}"
469
+ ),
470
+ },
471
+ async (args) => {
472
+ const skill = skillManager.get(args.name);
473
+ if (!skill) {
474
+ const available = skillManager.getAll().map((s) => s.name).join(", ");
475
+ return {
476
+ content: [{
477
+ type: "text",
478
+ text: `Skill "${args.name}" not found. Available: ${available}`,
479
+ }],
480
+ };
481
+ }
482
+
483
+ const updated = await skillManager.updateConfig(args.name, args.config);
484
+ if (!updated) {
485
+ return {
486
+ content: [{ type: "text", text: `Failed to update config for "${args.name}".` }],
487
+ };
488
+ }
489
+
490
+ // Check if there are still unconfigured required variables
491
+ const unconfigured = skillManager.getUnconfiguredVariables(args.name);
492
+ let responseText = `Configuration updated for skill "${args.name}".`;
493
+
494
+ if (unconfigured.length > 0) {
495
+ responseText += `\n\n**Still needs configuration:**\n`;
496
+ for (const v of unconfigured) {
497
+ const example = v.example ? ` (e.g. ${v.example})` : "";
498
+ responseText += `- \`{{${v.name}}}\`: ${v.description}${example}\n`;
499
+ }
500
+ } else {
501
+ responseText += ` All required variables are configured.`;
502
+ }
503
+
504
+ log.info(`Config updated for skill "${args.name}": ${Object.keys(args.config).join(", ")}`);
505
+ return {
506
+ content: [{ type: "text", text: responseText }],
507
+ };
508
+ }
509
+ ),
419
510
  tool(
420
511
  "skill_generate",
421
512
  "Prepare context for generating skills from a job description. Returns existing skills and job info " +
@@ -451,15 +542,39 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
451
542
  response += `- name: kebab-case name (e.g. "slack-message-check")\n`;
452
543
  response += `- description: one-line description\n`;
453
544
  response += `- instructions: detailed step-by-step markdown instructions the agent can follow\n`;
454
- response += `- emoji: a single emoji representing the skill\n\n`;
545
+ response += `- emoji: a single emoji representing the skill\n`;
546
+ response += `- variables: define user-specific data needed by the skill (see below)\n\n`;
455
547
  response += `skill_create automatically adds the skill to the user's collection — no need to call skill_add.\n\n`;
456
548
  response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}" and the list of created skill names to link them and mark the job as analyzed.\n\n`;
457
549
  response += `**Guidelines for skill instructions:**\n`;
458
550
  response += `- Write clear, actionable markdown steps\n`;
459
551
  response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks\n`;
460
552
  response += `- Include error handling steps\n`;
461
- response += `- Use placeholders like {query}, {date} for variable inputs\n`;
462
- response += `- Each skill should be a single, well-defined workflow (10-25 steps)\n`;
553
+ response += `- Each skill should be a single, well-defined workflow (10-25 steps)\n\n`;
554
+ response += `**IMPORTANT Use {{variables}} for user-specific data:**\n`;
555
+ response += `Skills often need user-specific information (accounts, repos, boards, channels, etc.).\n`;
556
+ response += `Instead of hardcoding or asking at runtime, use \`{{variable_name}}\` placeholders in instructions.\n\n`;
557
+ response += `Example: A GitHub PR review skill should use \`{{github_repos}}\` in its instructions:\n`;
558
+ response += ` "Navigate to each repository in {{github_repos}} and check for open PRs..."\n\n`;
559
+ response += `For each \`{{variable}}\` used in instructions, add a matching entry in the \`variables\` array:\n`;
560
+ response += `\`\`\`json\n`;
561
+ response += `{\n`;
562
+ response += ` "name": "github_repos",\n`;
563
+ response += ` "description": "GitHub repositories to monitor (owner/repo format)",\n`;
564
+ response += ` "type": "string[]",\n`;
565
+ response += ` "required": true,\n`;
566
+ response += ` "example": "octocat/hello-world, myorg/myrepo"\n`;
567
+ response += `}\n`;
568
+ response += `\`\`\`\n\n`;
569
+ response += `Common variables by platform:\n`;
570
+ response += `- GitHub: github_repos, github_username, github_org\n`;
571
+ response += `- Trello: trello_board_url, trello_list_names\n`;
572
+ response += `- Slack: slack_channels, slack_workspace_url\n`;
573
+ response += `- Email: email_labels, email_filters\n`;
574
+ response += `- E-commerce: store_url, competitor_urls, product_categories\n`;
575
+ response += `- General: timezone, language, notification_channel\n\n`;
576
+ response += `After creating skills, if any have required variables, call \`skill_configure\` to set initial values `;
577
+ response += `(ask the user for the values first).\n`;
463
578
 
464
579
  return { content: [{ type: "text", text: response }] };
465
580
  }
@@ -67,6 +67,26 @@ export interface SkillMetadata {
67
67
  skillKey?: string;
68
68
  }
69
69
 
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
+
70
90
  export interface Skill {
71
91
  name: string;
72
92
  description: string;
@@ -83,6 +103,10 @@ export interface Skill {
83
103
  source: "bundled" | "manual" | "external" | "auto_extracted" | "auto_improved" | "job_generated";
84
104
  dbId?: string; // UUID from agent_skills table
85
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>;
86
110
  }
87
111
 
88
112
  /**
@@ -174,6 +198,15 @@ export class SkillManager {
174
198
  * Convert a DB row to a Skill object.
175
199
  */
176
200
  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
+
177
210
  return {
178
211
  name: row.name as string,
179
212
  description: (row.description as string) || "",
@@ -190,6 +223,8 @@ export class SkillManager {
190
223
  source: (row.source as Skill["source"]) || "manual",
191
224
  dbId: row.id as string,
192
225
  sourceSkillId: (row.source_skill_id as string) || undefined,
226
+ variables,
227
+ config,
193
228
  };
194
229
  }
195
230
 
@@ -360,16 +395,20 @@ export class SkillManager {
360
395
  name: string,
361
396
  description: string,
362
397
  content: string,
363
- options?: { source?: string; emoji?: string; keywords?: string[] }
398
+ options?: { source?: string; emoji?: string; keywords?: string[]; variables?: SkillVariable[] }
364
399
  ): Promise<{ id: string; name: string } | null> {
365
400
  if (!this.userId) return null;
366
401
 
367
402
  try {
368
403
  const sb = getSupabase();
369
404
  const source = options?.source || "manual";
370
- const metadata = options?.emoji
371
- ? { openclaw: { emoji: options.emoji } }
372
- : {};
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
+ }
373
412
 
374
413
  const { data, error } = await sb.rpc("create_skill", {
375
414
  p_user_id: this.userId,
@@ -607,12 +646,20 @@ export class SkillManager {
607
646
  keywords?: string[];
608
647
  changeSummary?: string;
609
648
  sourceSkillId?: string;
649
+ variables?: SkillVariable[];
610
650
  }
611
651
  ): Promise<void> {
612
652
  if (!this.userId) return;
613
653
 
614
654
  try {
615
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
+
616
663
  const { data, error } = await sb.rpc("upsert_agent_skill", {
617
664
  p_user_id: this.userId,
618
665
  p_name: name,
@@ -624,6 +671,7 @@ export class SkillManager {
624
671
  p_keywords: options?.keywords || [],
625
672
  p_change_summary: options?.changeSummary || null,
626
673
  p_source_skill_id: options?.sourceSkillId || null,
674
+ p_metadata: Object.keys(metadata).length > 0 ? metadata : null,
627
675
  });
628
676
 
629
677
  if (error) {
@@ -631,11 +679,14 @@ export class SkillManager {
631
679
  return;
632
680
  }
633
681
 
634
- // Update in-memory dbId if returned
682
+ // Update in-memory skill with dbId and variables
635
683
  const skill = this.skills.get(name);
636
684
  if (skill && data && typeof data === "object" && "id" in (data as Record<string, unknown>)) {
637
685
  skill.dbId = (data as Record<string, unknown>).id as string;
638
686
  }
687
+ if (skill && options?.variables) {
688
+ skill.variables = options.variables;
689
+ }
639
690
 
640
691
  log.debug(`Skill "${name}" synced to agent_skills`);
641
692
  } catch (err) {
@@ -861,10 +912,90 @@ export class SkillManager {
861
912
  return [];
862
913
  }
863
914
  }
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
+ }
864
965
  }
865
966
 
866
967
  // ── Exported Utility Functions ─────────────────────────────────────
867
968
 
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
+
868
999
  /**
869
1000
  * Substitute $ARGUMENTS, $ARGUMENTS[N], and $N placeholders in skill content.
870
1001
  */