assistme 0.2.5 → 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 +246 -34
- package/package.json +1 -1
- package/src/agent/mcp-servers.ts +152 -35
- package/src/agent/processor.ts +1 -1
- package/src/agent/skills.ts +136 -5
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 =
|
|
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(
|
|
2984
|
-
|
|
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
|
-
{
|
|
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.",
|
|
@@ -3179,19 +3336,17 @@ ${content}`;
|
|
|
3179
3336
|
response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.
|
|
3180
3337
|
|
|
3181
3338
|
`;
|
|
3182
|
-
response += `**IMPORTANT \u2014
|
|
3339
|
+
response += `**IMPORTANT \u2014 You MUST use request_user_confirmation before creating skills:**
|
|
3183
3340
|
`;
|
|
3184
|
-
response += `1.
|
|
3341
|
+
response += `1. Analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).
|
|
3185
3342
|
`;
|
|
3186
|
-
response += `2.
|
|
3343
|
+
response += `2. Call \`request_user_confirmation\` with the formatted skill list as "message" and these options:
|
|
3187
3344
|
`;
|
|
3188
|
-
response += ` -
|
|
3345
|
+
response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]
|
|
3189
3346
|
`;
|
|
3190
|
-
response += `
|
|
3347
|
+
response += `3. WAIT for the response. If action_key is "approve_all", create all skills using \`skill_create\`. If "cancel", stop.
|
|
3191
3348
|
`;
|
|
3192
|
-
response += `
|
|
3193
|
-
`;
|
|
3194
|
-
response += `4. If the user cancels, stop without creating anything.
|
|
3349
|
+
response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling request_user_confirmation first.
|
|
3195
3350
|
|
|
3196
3351
|
`;
|
|
3197
3352
|
response += `For each skill, call \`skill_create\` with:
|
|
@@ -3203,6 +3358,8 @@ ${content}`;
|
|
|
3203
3358
|
response += `- instructions: detailed step-by-step markdown instructions the agent can follow
|
|
3204
3359
|
`;
|
|
3205
3360
|
response += `- emoji: a single emoji representing the skill
|
|
3361
|
+
`;
|
|
3362
|
+
response += `- variables: define user-specific data needed by the skill (see below)
|
|
3206
3363
|
|
|
3207
3364
|
`;
|
|
3208
3365
|
response += `skill_create automatically adds the skill to the user's collection \u2014 no need to call skill_add.
|
|
@@ -3218,10 +3375,60 @@ ${content}`;
|
|
|
3218
3375
|
response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks
|
|
3219
3376
|
`;
|
|
3220
3377
|
response += `- Include error handling steps
|
|
3221
|
-
`;
|
|
3222
|
-
response += `- Use placeholders like {query}, {date} for variable inputs
|
|
3223
3378
|
`;
|
|
3224
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).
|
|
3225
3432
|
`;
|
|
3226
3433
|
return { content: [{ type: "text", text: response }] };
|
|
3227
3434
|
}
|
|
@@ -3361,18 +3568,18 @@ ${content}`;
|
|
|
3361
3568
|
// ── User Confirmation Tool ─────────────────────────────────
|
|
3362
3569
|
tool(
|
|
3363
3570
|
"request_user_confirmation",
|
|
3364
|
-
"Pause and ask the user for approval or input via the web UI. Returns the user's
|
|
3571
|
+
"Pause and ask the user for approval or input via the web UI. Returns the user's chosen action_key. Use this BEFORE creating skills, making irreversible changes, etc. The agent will block until the user responds or the timeout expires.",
|
|
3365
3572
|
{
|
|
3366
3573
|
message: z.string().describe("What to show the user (supports markdown)"),
|
|
3367
3574
|
options: z.array(z.object({
|
|
3368
|
-
label: z.string(),
|
|
3369
|
-
|
|
3370
|
-
description: z.string().optional()
|
|
3575
|
+
label: z.string().describe("Button label shown to user"),
|
|
3576
|
+
action_key: z.string().describe("Machine-readable key returned when selected"),
|
|
3577
|
+
description: z.string().optional().describe("Tooltip/description for this option")
|
|
3371
3578
|
})).describe("Buttons/options to show the user"),
|
|
3372
3579
|
timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)")
|
|
3373
3580
|
},
|
|
3374
3581
|
async (args) => {
|
|
3375
|
-
const actionId =
|
|
3582
|
+
const actionId = `action_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3376
3583
|
const timeout = (args.timeout_seconds || 300) * 1e3;
|
|
3377
3584
|
const actionData = {
|
|
3378
3585
|
id: actionId,
|
|
@@ -3383,20 +3590,24 @@ ${content}`;
|
|
|
3383
3590
|
};
|
|
3384
3591
|
try {
|
|
3385
3592
|
await setActionRequest(taskId, actionData);
|
|
3386
|
-
|
|
3593
|
+
log.info(`Action request ${actionId} stored in metadata, waiting for user response...`);
|
|
3594
|
+
emitEvent(taskId, "user_action_request", actionData).catch(() => {
|
|
3595
|
+
});
|
|
3387
3596
|
const startTime = Date.now();
|
|
3388
3597
|
const pollInterval = 2e3;
|
|
3389
3598
|
while (Date.now() - startTime < timeout) {
|
|
3390
3599
|
const response = await pollActionResponse(taskId);
|
|
3391
3600
|
if (response) {
|
|
3392
|
-
|
|
3601
|
+
const actionKey = response.action_key || response.action || "";
|
|
3602
|
+
const label = response.label || actionKey;
|
|
3603
|
+
log.info(`User responded: ${label} (${actionKey})`);
|
|
3393
3604
|
return {
|
|
3394
3605
|
content: [{
|
|
3395
3606
|
type: "text",
|
|
3396
3607
|
text: JSON.stringify({
|
|
3397
3608
|
status: "responded",
|
|
3398
|
-
|
|
3399
|
-
|
|
3609
|
+
action_key: actionKey,
|
|
3610
|
+
label
|
|
3400
3611
|
})
|
|
3401
3612
|
}]
|
|
3402
3613
|
};
|
|
@@ -3414,6 +3625,7 @@ ${content}`;
|
|
|
3414
3625
|
}]
|
|
3415
3626
|
};
|
|
3416
3627
|
} catch (err) {
|
|
3628
|
+
log.error(`request_user_confirmation failed: ${err}`);
|
|
3417
3629
|
return {
|
|
3418
3630
|
content: [{
|
|
3419
3631
|
type: "text",
|
|
@@ -3721,7 +3933,7 @@ Available capabilities:
|
|
|
3721
3933
|
|
|
3722
3934
|
5. JOB AUTOMATION:
|
|
3723
3935
|
- When the user describes their job/role/daily work, use skill_generate to decompose it into automatable skills
|
|
3724
|
-
-
|
|
3936
|
+
- ALWAYS use request_user_confirmation to get user approval before creating skills \u2014 never create skills without approval
|
|
3725
3937
|
- Use job_run to start a job \u2014 it gives you the job's goal and available skills as capabilities
|
|
3726
3938
|
- When running a job, be AGENTIC: decide dynamically what to do based on what you discover
|
|
3727
3939
|
- Do NOT follow a fixed sequence \u2014 if checking Slack reveals a task that needs GitHub, go do GitHub immediately
|
package/package.json
CHANGED
package/src/agent/mcp-servers.ts
CHANGED
|
@@ -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(
|
|
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
|
-
{
|
|
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 " +
|
|
@@ -441,26 +532,49 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
441
532
|
}
|
|
442
533
|
|
|
443
534
|
response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.\n\n`;
|
|
444
|
-
response += `**IMPORTANT —
|
|
445
|
-
response += `1.
|
|
446
|
-
response += `2.
|
|
447
|
-
response += ` -
|
|
448
|
-
response += `
|
|
449
|
-
response += `
|
|
450
|
-
response += `4. If the user cancels, stop without creating anything.\n\n`;
|
|
535
|
+
response += `**IMPORTANT — You MUST use request_user_confirmation before creating skills:**\n`;
|
|
536
|
+
response += `1. Analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).\n`;
|
|
537
|
+
response += `2. Call \`request_user_confirmation\` with the formatted skill list as "message" and these options:\n`;
|
|
538
|
+
response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]\n`;
|
|
539
|
+
response += `3. WAIT for the response. If action_key is "approve_all", create all skills using \`skill_create\`. If "cancel", stop.\n`;
|
|
540
|
+
response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling request_user_confirmation first.\n\n`;
|
|
451
541
|
response += `For each skill, call \`skill_create\` with:\n`;
|
|
452
542
|
response += `- name: kebab-case name (e.g. "slack-message-check")\n`;
|
|
453
543
|
response += `- description: one-line description\n`;
|
|
454
544
|
response += `- instructions: detailed step-by-step markdown instructions the agent can follow\n`;
|
|
455
|
-
response += `- emoji: a single emoji representing the skill\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`;
|
|
456
547
|
response += `skill_create automatically adds the skill to the user's collection — no need to call skill_add.\n\n`;
|
|
457
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`;
|
|
458
549
|
response += `**Guidelines for skill instructions:**\n`;
|
|
459
550
|
response += `- Write clear, actionable markdown steps\n`;
|
|
460
551
|
response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks\n`;
|
|
461
552
|
response += `- Include error handling steps\n`;
|
|
462
|
-
response += `-
|
|
463
|
-
response +=
|
|
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`;
|
|
464
578
|
|
|
465
579
|
return { content: [{ type: "text", text: response }] };
|
|
466
580
|
}
|
|
@@ -610,18 +724,19 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
610
724
|
tool(
|
|
611
725
|
"request_user_confirmation",
|
|
612
726
|
"Pause and ask the user for approval or input via the web UI. " +
|
|
613
|
-
"Returns the user's
|
|
727
|
+
"Returns the user's chosen action_key. Use this BEFORE creating skills, making irreversible changes, etc. " +
|
|
728
|
+
"The agent will block until the user responds or the timeout expires.",
|
|
614
729
|
{
|
|
615
730
|
message: z.string().describe("What to show the user (supports markdown)"),
|
|
616
731
|
options: z.array(z.object({
|
|
617
|
-
label: z.string(),
|
|
618
|
-
|
|
619
|
-
description: z.string().optional(),
|
|
732
|
+
label: z.string().describe("Button label shown to user"),
|
|
733
|
+
action_key: z.string().describe("Machine-readable key returned when selected"),
|
|
734
|
+
description: z.string().optional().describe("Tooltip/description for this option"),
|
|
620
735
|
})).describe("Buttons/options to show the user"),
|
|
621
736
|
timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)"),
|
|
622
737
|
},
|
|
623
738
|
async (args) => {
|
|
624
|
-
const actionId =
|
|
739
|
+
const actionId = `action_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
625
740
|
const timeout = (args.timeout_seconds || 300) * 1000;
|
|
626
741
|
|
|
627
742
|
const actionData = {
|
|
@@ -633,37 +748,38 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
633
748
|
};
|
|
634
749
|
|
|
635
750
|
try {
|
|
636
|
-
//
|
|
751
|
+
// Store action request in message metadata via RPC — UI reads this
|
|
637
752
|
await setActionRequest(taskId, actionData);
|
|
753
|
+
log.info(`Action request ${actionId} stored in metadata, waiting for user response...`);
|
|
638
754
|
|
|
639
|
-
//
|
|
640
|
-
|
|
755
|
+
// Also emit event for real-time notification (best-effort)
|
|
756
|
+
emitEvent(taskId, "user_action_request", actionData).catch(() => {});
|
|
641
757
|
|
|
642
|
-
//
|
|
758
|
+
// Poll for response
|
|
643
759
|
const startTime = Date.now();
|
|
644
760
|
const pollInterval = 2000;
|
|
645
761
|
|
|
646
762
|
while (Date.now() - startTime < timeout) {
|
|
647
763
|
const response = await pollActionResponse(taskId);
|
|
648
764
|
if (response) {
|
|
649
|
-
|
|
765
|
+
const actionKey = (response.action_key || response.action || "") as string;
|
|
766
|
+
const label = (response.label || actionKey) as string;
|
|
767
|
+
log.info(`User responded: ${label} (${actionKey})`);
|
|
650
768
|
return {
|
|
651
769
|
content: [{
|
|
652
770
|
type: "text",
|
|
653
771
|
text: JSON.stringify({
|
|
654
772
|
status: "responded",
|
|
655
|
-
|
|
656
|
-
|
|
773
|
+
action_key: actionKey,
|
|
774
|
+
label,
|
|
657
775
|
}),
|
|
658
776
|
}],
|
|
659
777
|
};
|
|
660
778
|
}
|
|
661
779
|
|
|
662
|
-
// Wait before next poll
|
|
663
780
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
664
781
|
}
|
|
665
782
|
|
|
666
|
-
// Timeout
|
|
667
783
|
log.warn(`Action request ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
668
784
|
return {
|
|
669
785
|
content: [{
|
|
@@ -675,6 +791,7 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
675
791
|
}],
|
|
676
792
|
};
|
|
677
793
|
} catch (err) {
|
|
794
|
+
log.error(`request_user_confirmation failed: ${err}`);
|
|
678
795
|
return {
|
|
679
796
|
content: [{
|
|
680
797
|
type: "text",
|
package/src/agent/processor.ts
CHANGED
|
@@ -68,7 +68,7 @@ Available capabilities:
|
|
|
68
68
|
|
|
69
69
|
5. JOB AUTOMATION:
|
|
70
70
|
- When the user describes their job/role/daily work, use skill_generate to decompose it into automatable skills
|
|
71
|
-
-
|
|
71
|
+
- ALWAYS use request_user_confirmation to get user approval before creating skills — never create skills without approval
|
|
72
72
|
- Use job_run to start a job — it gives you the job's goal and available skills as capabilities
|
|
73
73
|
- When running a job, be AGENTIC: decide dynamically what to do based on what you discover
|
|
74
74
|
- Do NOT follow a fixed sequence — if checking Slack reveals a task that needs GitHub, go do GitHub immediately
|
package/src/agent/skills.ts
CHANGED
|
@@ -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 =
|
|
371
|
-
|
|
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
|
|
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
|
*/
|