assistme 0.2.2 → 0.2.3

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
@@ -1965,27 +1965,28 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
1965
1965
  const sb = getSupabase();
1966
1966
  const source = options?.source || "manual";
1967
1967
  const metadata = options?.emoji ? { openclaw: { emoji: options.emoji } } : {};
1968
- const { data, error } = await sb.from("skills").upsert(
1969
- {
1970
- name,
1971
- description,
1972
- content,
1973
- version: "1.0.0",
1974
- author_id: this.userId,
1975
- emoji: options?.emoji || null,
1976
- keywords: options?.keywords || [],
1977
- metadata,
1978
- source,
1979
- is_public: false
1980
- },
1981
- { onConflict: "author_id,name" }
1982
- ).select("id, name").single();
1968
+ const { data, error } = await sb.rpc("create_skill", {
1969
+ p_user_id: this.userId,
1970
+ p_name: name,
1971
+ p_description: description,
1972
+ p_content: content,
1973
+ p_version: "1.0.0",
1974
+ p_source: source,
1975
+ p_emoji: options?.emoji || null,
1976
+ p_keywords: options?.keywords || [],
1977
+ p_metadata: metadata
1978
+ });
1983
1979
  if (error) {
1984
1980
  log.debug(`Skill create failed for "${name}": ${error.message}`);
1985
1981
  return null;
1986
1982
  }
1983
+ const row = Array.isArray(data) ? data[0] : data;
1984
+ if (!row) {
1985
+ log.debug(`Skill create returned no data for "${name}"`);
1986
+ return null;
1987
+ }
1987
1988
  log.info(`Skill "${name}" created in skills table (pending approval)`);
1988
- return data;
1989
+ return row;
1989
1990
  } catch (err) {
1990
1991
  log.debug(`Skill create error: ${err}`);
1991
1992
  return null;
@@ -2327,91 +2328,6 @@ function preprocessDynamicContext(content, cwd) {
2327
2328
  });
2328
2329
  }
2329
2330
 
2330
- // src/agent/memory-extractor.ts
2331
- var EXTRACTION_PROMPT = `You are a memory extraction system. Analyze the following conversation between a user and an AI assistant. Extract any facts worth remembering about the user for future conversations.
2332
-
2333
- Rules:
2334
- - Only extract FACTUAL information about the user, not about the task itself
2335
- - Be concise: each memory should be a single clear statement
2336
- - Don't extract trivial/obvious things (e.g. "user asked a question")
2337
- - Categories: "preference" (likes/dislikes/habits), "instruction" (standing orders like "always do X"), "context" (work/life info), "fact" (specific facts), "general" (other)
2338
- - Importance: 3-4 for minor preferences, 5-6 for useful context, 7-8 for strong preferences/instructions, 9-10 for critical standing instructions
2339
- - If nothing worth remembering, return an empty array
2340
- - Max 3 memories per conversation
2341
- - Support both English and Chinese content
2342
-
2343
- Respond with ONLY valid JSON \u2014 no markdown, no explanation:
2344
- [{"content": "...", "category": "...", "importance": N, "tags": ["..."]}]
2345
-
2346
- If nothing to remember: []`;
2347
- async function extractMemoriesWithLLM(taskPrompt, taskResult) {
2348
- const config = getConfig();
2349
- const apiKey = config.anthropicApiKey;
2350
- if (!apiKey) {
2351
- log.debug("No API key, skipping LLM memory extraction");
2352
- return [];
2353
- }
2354
- const truncatedPrompt = taskPrompt.slice(0, 2e3);
2355
- const truncatedResult = taskResult.slice(0, 3e3);
2356
- const userMessage = `<user_request>
2357
- ${truncatedPrompt}
2358
- </user_request>
2359
-
2360
- <assistant_response>
2361
- ${truncatedResult}
2362
- </assistant_response>`;
2363
- try {
2364
- const response = await fetch("https://api.anthropic.com/v1/messages", {
2365
- method: "POST",
2366
- headers: {
2367
- "Content-Type": "application/json",
2368
- "x-api-key": apiKey,
2369
- "anthropic-version": "2023-06-01"
2370
- },
2371
- body: JSON.stringify({
2372
- model: "claude-haiku-4-5-20251001",
2373
- max_tokens: 512,
2374
- system: EXTRACTION_PROMPT,
2375
- messages: [{ role: "user", content: userMessage }]
2376
- })
2377
- });
2378
- if (!response.ok) {
2379
- const errText = await response.text();
2380
- log.debug(`Memory extraction API error: ${response.status} ${errText.slice(0, 200)}`);
2381
- return [];
2382
- }
2383
- const data = await response.json();
2384
- const text = data.content?.[0]?.type === "text" ? data.content[0].text : "";
2385
- if (!text || text.trim() === "[]") {
2386
- log.debug("LLM memory extraction: nothing to remember");
2387
- return [];
2388
- }
2389
- const jsonStr = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
2390
- const memories = JSON.parse(jsonStr);
2391
- const validCategories = [
2392
- "general",
2393
- "preference",
2394
- "instruction",
2395
- "context",
2396
- "skill_learned",
2397
- "fact"
2398
- ];
2399
- return memories.filter(
2400
- (m) => m.content && m.content.length > 5 && validCategories.includes(m.category)
2401
- ).map((m) => ({
2402
- content: m.content.slice(0, 500),
2403
- category: m.category,
2404
- importance: Math.max(1, Math.min(10, m.importance || 5)),
2405
- tags: Array.isArray(m.tags) ? m.tags.slice(0, 5) : []
2406
- })).slice(0, 3);
2407
- } catch (err) {
2408
- log.debug(
2409
- `LLM memory extraction failed: ${err instanceof Error ? err.message : err}`
2410
- );
2411
- return [];
2412
- }
2413
- }
2414
-
2415
2331
  // src/utils/retry.ts
2416
2332
  async function withRetry(fn, opts = {}) {
2417
2333
  const {
@@ -2883,138 +2799,6 @@ function getLimiterForTool(toolName) {
2883
2799
  return null;
2884
2800
  }
2885
2801
 
2886
- // src/agent/skill-extractor.ts
2887
- var DECOMPOSE_JOB_PROMPT = `You are a job analysis system. Given a person's job description, decompose their work into individual AUTOMATABLE SKILLS that an AI agent could learn and execute.
2888
-
2889
- The AI agent has these capabilities:
2890
- - Control a real Chrome browser (navigate, click, type, read pages, take screenshots)
2891
- - Read/write files on the local machine
2892
- - Execute shell commands
2893
- - Store memories and learn from interactions
2894
-
2895
- Rules:
2896
- - Focus on RECURRING, REPETITIVE tasks that benefit from automation
2897
- - Each skill should be a single, well-defined workflow
2898
- - Prioritize tasks the person does frequently
2899
- - Mark tasks as automatable:true only if the agent can handle them end-to-end
2900
- - Name skills in kebab-case
2901
- - Be specific \u2014 "check-email" is too vague, "summarize-unread-gmail" is better
2902
- - Generate 5-15 skills depending on job complexity
2903
- - Consider both web-based and file-based workflows
2904
-
2905
- Respond with ONLY valid JSON array \u2014 no markdown wrapping:
2906
- [{"name": "skill-name", "description": "What this skill does", "category": "category", "priority": "high|medium|low", "automatable": true|false}]`;
2907
- async function decomposeJob(jobDescription, existingSkillNames = []) {
2908
- const config = getConfig();
2909
- const apiKey = config.anthropicApiKey;
2910
- if (!apiKey) return [];
2911
- const existingNote = existingSkillNames.length > 0 ? `
2912
-
2913
- The agent already has these skills (do NOT duplicate): ${existingSkillNames.join(", ")}` : "";
2914
- try {
2915
- const response = await fetch("https://api.anthropic.com/v1/messages", {
2916
- method: "POST",
2917
- headers: {
2918
- "Content-Type": "application/json",
2919
- "x-api-key": apiKey,
2920
- "anthropic-version": "2023-06-01"
2921
- },
2922
- body: JSON.stringify({
2923
- model: "claude-haiku-4-5-20251001",
2924
- max_tokens: 2048,
2925
- system: DECOMPOSE_JOB_PROMPT,
2926
- messages: [
2927
- {
2928
- role: "user",
2929
- content: `<job_description>
2930
- ${jobDescription.slice(0, 3e3)}
2931
- </job_description>${existingNote}`
2932
- }
2933
- ]
2934
- })
2935
- });
2936
- if (!response.ok) return [];
2937
- const data = await response.json();
2938
- const text = data.content?.[0]?.type === "text" ? data.content[0].text : "";
2939
- if (!text) return [];
2940
- const jsonStr = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
2941
- const specs = JSON.parse(jsonStr);
2942
- if (!Array.isArray(specs)) return [];
2943
- return specs.filter((s) => s.name && s.description).map((s) => ({
2944
- ...s,
2945
- name: s.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")
2946
- }));
2947
- } catch (err) {
2948
- log.debug(`Job decomposition failed: ${err}`);
2949
- return [];
2950
- }
2951
- }
2952
- var GENERATE_PROMPT = `You are a skill authoring system for an AI agent. Given a skill name, description, and context about the user's work, generate detailed step-by-step instructions that the AI agent can follow to execute this skill.
2953
-
2954
- The AI agent has these capabilities:
2955
- - Control a real Chrome browser (navigate, click, type, read pages, take screenshots)
2956
- - Read/write files on the local machine
2957
- - Execute shell commands (Bash)
2958
- - Store memories about the user
2959
-
2960
- Rules:
2961
- - Write clear, actionable markdown instructions
2962
- - Use numbered steps with bold action names
2963
- - Include error handling (what to do if a page doesn't load, element not found, etc.)
2964
- - Use placeholders like {query}, {date}, {recipient} for variable inputs
2965
- - Include a $ARGUMENTS line at the top if the skill accepts parameters
2966
- - Be specific about which browser tools to use (browser_navigate, browser_click, etc.)
2967
- - Include validation steps (verify the action worked)
2968
- - Keep instructions thorough but concise (10-25 steps)
2969
-
2970
- Respond with ONLY valid JSON \u2014 no markdown wrapping:
2971
- {"name": "skill-name", "description": "One-line description", "steps": "## Workflow\\n\\n$ARGUMENTS: describe expected arguments\\n\\n1. **Step one**\\n...", "emoji": "\u{1F527}", "keywords": ["keyword1", "keyword2"]}`;
2972
- async function generateSkillFromDescription(name, description, jobContext) {
2973
- const config = getConfig();
2974
- const apiKey = config.anthropicApiKey;
2975
- if (!apiKey) return null;
2976
- const contextBlock = jobContext ? `
2977
-
2978
- <user_job_context>
2979
- ${jobContext.slice(0, 1e3)}
2980
- </user_job_context>` : "";
2981
- try {
2982
- const response = await fetch("https://api.anthropic.com/v1/messages", {
2983
- method: "POST",
2984
- headers: {
2985
- "Content-Type": "application/json",
2986
- "x-api-key": apiKey,
2987
- "anthropic-version": "2023-06-01"
2988
- },
2989
- body: JSON.stringify({
2990
- model: "claude-haiku-4-5-20251001",
2991
- max_tokens: 2048,
2992
- system: GENERATE_PROMPT,
2993
- messages: [
2994
- {
2995
- role: "user",
2996
- content: `Generate a skill for:
2997
- Name: ${name}
2998
- Description: ${description}${contextBlock}`
2999
- }
3000
- ]
3001
- })
3002
- });
3003
- if (!response.ok) return null;
3004
- const data = await response.json();
3005
- const text = data.content?.[0]?.type === "text" ? data.content[0].text : "";
3006
- if (!text) return null;
3007
- const jsonStr = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
3008
- const skill = JSON.parse(jsonStr);
3009
- if (!skill || !skill.name || !skill.steps) return null;
3010
- skill.name = skill.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3011
- return skill;
3012
- } catch (err) {
3013
- log.debug(`Skill generation failed: ${err}`);
3014
- return null;
3015
- }
3016
- }
3017
-
3018
2802
  // src/agent/mcp-servers.ts
3019
2803
  async function callTool(name, input) {
3020
2804
  const limiter = getLimiterForTool(name);
@@ -3356,130 +3140,93 @@ ${content}`;
3356
3140
  ),
3357
3141
  tool(
3358
3142
  "skill_generate",
3359
- "Generate new skills from a job description. Analyzes the user's work, decomposes it into automatable tasks, and creates skills for each one. Use this when the user describes their job/role and you want to set up skills to handle their work.",
3143
+ "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 + skill_add. After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
3360
3144
  {
3361
3145
  job_name: z.string().describe(
3362
3146
  "Short name for this job/role. Example: '\u7535\u5546\u8FD0\u8425', 'Frontend Dev', 'Data Analyst'"
3363
3147
  ),
3364
3148
  job_description: z.string().describe(
3365
3149
  "Description of the user's job, role, and daily tasks. Can be in any language. Example: '\u6211\u662F\u7535\u5546\u8FD0\u8425\uFF0C\u6BCF\u5929\u8981\u770B\u7ADE\u54C1\u4EF7\u683C\u3001\u5199\u5546\u54C1\u6587\u6848\u3001\u56DE\u590D\u5BA2\u6237\u8BC4\u8BBA'"
3366
- ),
3367
- auto_create: z.boolean().optional().describe("If true, automatically create all generated skills. If false (default), return the plan for review first.")
3150
+ )
3368
3151
  },
3369
3152
  async (args) => {
3370
3153
  const existingNames = skillManager.getAll().map((s) => s.name);
3371
- const specs = await decomposeJob(args.job_description, existingNames);
3372
- if (specs.length === 0) {
3373
- return {
3374
- content: [{
3375
- type: "text",
3376
- text: "Could not decompose the job description into automatable skills. Try providing more detail about your daily tasks."
3377
- }]
3378
- };
3379
- }
3380
- if (!args.auto_create) {
3381
- let response2 = `## Job Analysis: ${args.job_name}
3154
+ let response = `## Job: ${args.job_name}
3155
+ `;
3156
+ response += `**Description:** ${args.job_description}
3382
3157
 
3383
- I identified **${specs.length} skills** from your job description:
3158
+ `;
3159
+ if (existingNames.length > 0) {
3160
+ response += `**Existing skills (do NOT duplicate):** ${existingNames.join(", ")}
3384
3161
 
3385
3162
  `;
3386
- const byPriority = { high: [], medium: [], low: [] };
3387
- for (const s of specs) {
3388
- (byPriority[s.priority] || byPriority.medium).push(s);
3389
- }
3390
- for (const [priority, items] of Object.entries(byPriority)) {
3391
- if (items.length === 0) continue;
3392
- const label = priority === "high" ? "High Priority" : priority === "medium" ? "Medium Priority" : "Low Priority";
3393
- response2 += `### ${label}
3163
+ }
3164
+ response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills. `;
3165
+ response += `For each skill, call \`skill_create\` with:
3394
3166
  `;
3395
- for (const s of items) {
3396
- const auto = s.automatable ? "" : " (needs human assistance)";
3397
- response2 += `- **${s.name}**: ${s.description}${auto}
3167
+ response += `- name: kebab-case name (e.g. "slack-message-check")
3398
3168
  `;
3399
- }
3400
- response2 += "\n";
3401
- }
3402
- response2 += `Call \`skill_generate\` again with \`job_name: "${args.job_name}"\` and \`auto_create: true\` to create all these skills, `;
3403
- response2 += "or use `skill_create` to create individual skills manually.";
3404
- return { content: [{ type: "text", text: response2 }] };
3405
- }
3406
- const created = [];
3407
- const failed = [];
3408
- for (let i = 0; i < specs.length; i += 3) {
3409
- const batch = specs.slice(i, i + 3);
3410
- const results = await Promise.allSettled(
3411
- batch.map(
3412
- (spec) => generateSkillFromDescription(spec.name, spec.description, args.job_description)
3413
- )
3414
- );
3415
- for (let j = 0; j < results.length; j++) {
3416
- const spec = batch[j];
3417
- const result = results[j];
3418
- if (result.status === "fulfilled" && result.value) {
3419
- const generated = result.value;
3420
- const existing = skillManager.findSimilar(generated.name);
3421
- if (existing) {
3422
- failed.push(`${spec.name} (duplicate of "${existing.name}")`);
3423
- continue;
3424
- }
3425
- const createResult = await skillManager.create(
3426
- generated.name,
3427
- generated.description || spec.description,
3428
- generated.steps,
3429
- {
3430
- source: "job_generated",
3431
- emoji: generated.emoji,
3432
- keywords: generated.keywords
3433
- }
3434
- );
3435
- if (createResult) {
3436
- const added = await skillManager.addSkill(createResult.id);
3437
- if (added) {
3438
- created.push(generated.name);
3439
- } else {
3440
- failed.push(`${spec.name} (add failed)`);
3441
- }
3442
- } else {
3443
- failed.push(spec.name);
3444
- }
3445
- } else {
3446
- failed.push(spec.name);
3447
- }
3448
- }
3449
- }
3450
- if (userId) {
3451
- saveJobToDb(
3452
- userId,
3453
- args.job_name,
3454
- args.job_description,
3455
- created
3456
- ).catch((err) => {
3457
- log.debug(`saveJobToDb error: ${err}`);
3458
- });
3459
- }
3460
- let response = `## Skills Generated for "${args.job_name}"
3169
+ response += `- description: one-line description
3170
+ `;
3171
+ response += `- instructions: detailed step-by-step markdown instructions the agent can follow
3172
+ `;
3173
+ response += `- emoji: a single emoji representing the skill
3461
3174
 
3462
3175
  `;
3463
- response += `Created **${created.length}/${specs.length}** skills from your job description:
3176
+ response += `After creating each skill, call \`skill_add\` with the returned skill ID to add it to the user's collection.
3464
3177
 
3465
3178
  `;
3466
- for (const name of created) {
3467
- const skill = skillManager.get(name);
3468
- const emoji = skill?.metadata.emoji || "";
3469
- response += `- ${emoji} **${name}**: ${skill?.description || ""}
3179
+ response += `After ALL skills are created and added, 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.
3180
+
3470
3181
  `;
3471
- }
3472
- if (failed.length > 0) {
3473
- response += `
3474
- Failed to create: ${failed.join(", ")}
3182
+ response += `**Guidelines for skill instructions:**
3183
+ `;
3184
+ response += `- Write clear, actionable markdown steps
3185
+ `;
3186
+ response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks
3187
+ `;
3188
+ response += `- Include error handling steps
3189
+ `;
3190
+ response += `- Use placeholders like {query}, {date} for variable inputs
3191
+ `;
3192
+ response += `- Each skill should be a single, well-defined workflow (10-25 steps)
3475
3193
  `;
3476
- }
3477
- response += "\nThese skills are now available. When you give me a task that matches, I'll automatically use the right skill.";
3478
- response += "\nUse `skill_invoke` to test any skill, or `skill_improve` to refine them after use.";
3479
- log.success(`Job "${args.job_name}": created ${created.length} skills`);
3480
3194
  return { content: [{ type: "text", text: response }] };
3481
3195
  }
3482
3196
  ),
3197
+ tool(
3198
+ "skill_link_job",
3199
+ "Link created skills to a job and mark it as analyzed. Call this after creating all skills for a job via skill_create + skill_add.",
3200
+ {
3201
+ job_name: z.string().describe("Name of the job to link skills to"),
3202
+ job_description: z.string().describe("Job description (used if job doesn't exist yet)"),
3203
+ skill_names: z.array(z.string()).describe("Names of skills to link to this job")
3204
+ },
3205
+ async (args) => {
3206
+ if (!userId) {
3207
+ return {
3208
+ content: [{ type: "text", text: "Not authenticated. Cannot link job." }]
3209
+ };
3210
+ }
3211
+ try {
3212
+ await saveJobToDb(userId, args.job_name, args.job_description, args.skill_names);
3213
+ log.success(`Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`);
3214
+ return {
3215
+ content: [{
3216
+ type: "text",
3217
+ text: `Job "${args.job_name}" linked with ${args.skill_names.length} skills and marked as analyzed.`
3218
+ }]
3219
+ };
3220
+ } catch (err) {
3221
+ return {
3222
+ content: [{
3223
+ type: "text",
3224
+ text: `Failed to link job: ${err instanceof Error ? err.message : err}`
3225
+ }]
3226
+ };
3227
+ }
3228
+ }
3229
+ ),
3483
3230
  tool(
3484
3231
  "skill_browse",
3485
3232
  "Browse the skill marketplace to discover skills published by the community. Search by keyword, filter by category, and sort by popularity or rating.",
@@ -3877,6 +3624,8 @@ Available capabilities:
3877
3624
  - You can remember things about the user using memory_store
3878
3625
  - Use this when you learn preferences, important facts, or standing instructions
3879
3626
  - Your stored memories persist across conversations
3627
+ - PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
3628
+ - Before completing a task, consider if anything learned should be remembered for future conversations
3880
3629
 
3881
3630
  4. SKILL PLANNING (pre-task):
3882
3631
  - Before executing a complex task, analyze if it matches an existing skill (use skill_invoke)
@@ -4019,6 +3768,7 @@ var TaskProcessor = class {
4019
3768
  "mcp__assistme-agent__skill_invoke",
4020
3769
  "mcp__assistme-agent__skill_search",
4021
3770
  "mcp__assistme-agent__skill_generate",
3771
+ "mcp__assistme-agent__skill_link_job",
4022
3772
  "mcp__assistme-agent__skill_browse",
4023
3773
  "mcp__assistme-agent__skill_add",
4024
3774
  "mcp__assistme-agent__skill_publish",
@@ -4132,27 +3882,6 @@ var TaskProcessor = class {
4132
3882
  convHistory.splice(0, convHistory.length - MAX_HISTORY_ENTRIES * 2);
4133
3883
  }
4134
3884
  this.historyCache.set(task.conversation_id, convHistory);
4135
- if (this.memoryManager && finalResponse) {
4136
- const mm = this.memoryManager;
4137
- const taskIdRef = task.id;
4138
- extractMemoriesWithLLM(task.prompt, finalResponse).then(async (memories) => {
4139
- for (const mem of memories) {
4140
- try {
4141
- await mm.remember(mem.content, mem.category, {
4142
- importance: mem.importance,
4143
- tags: mem.tags,
4144
- sourceMessageId: taskIdRef
4145
- });
4146
- log.info(`Memory extracted: [${mem.category}] ${mem.content.slice(0, 60)}...`);
4147
- } catch {
4148
- }
4149
- }
4150
- if (memories.length > 0) {
4151
- log.success(`${memories.length} memory(s) auto-extracted`);
4152
- }
4153
- }).catch(() => {
4154
- });
4155
- }
4156
3885
  } catch (err) {
4157
3886
  const errorMsg = err instanceof Error ? err.message : String(err);
4158
3887
  log.error(`Task failed: ${errorMsg}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,8 @@ import { log } from "../utils/logger.js";
10
10
  import type { MemoryManager, MemoryCategory } from "./memory.js";
11
11
  import type { SkillManager } from "./skills.js";
12
12
  import { substituteArguments, preprocessDynamicContext } from "./skills.js";
13
- import { decomposeJob, generateSkillFromDescription } from "./skill-extractor.js";
13
+ // decomposeJob and generateSkillFromDescription removed the agent SDK handles
14
+ // job analysis and skill generation directly (no separate LLM API calls needed)
14
15
  import { getSupabase } from "../db/supabase.js";
15
16
  import { JobRunner } from "./job-runner.js";
16
17
  import {
@@ -405,8 +406,9 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
405
406
  ),
406
407
  tool(
407
408
  "skill_generate",
408
- "Generate new skills from a job description. Analyzes the user's work, decomposes it into automatable tasks, " +
409
- "and creates skills for each one. Use this when the user describes their job/role and you want to set up skills to handle their work.",
409
+ "Prepare context for generating skills from a job description. Returns existing skills and job info " +
410
+ "so you can analyze the job and create skills using skill_create + skill_add. " +
411
+ "After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
410
412
  {
411
413
  job_name: z.string().describe(
412
414
  "Short name for this job/role. Example: '电商运营', 'Frontend Dev', 'Data Analyst'"
@@ -415,137 +417,67 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
415
417
  "Description of the user's job, role, and daily tasks. Can be in any language. " +
416
418
  "Example: '我是电商运营,每天要看竞品价格、写商品文案、回复客户评论'"
417
419
  ),
418
- auto_create: z
419
- .boolean()
420
- .optional()
421
- .describe("If true, automatically create all generated skills. If false (default), return the plan for review first."),
422
420
  },
423
421
  async (args) => {
424
- // Step 1: Decompose job into skill specs
425
422
  const existingNames = skillManager.getAll().map((s) => s.name);
426
- const specs = await decomposeJob(args.job_description, existingNames);
427
-
428
- if (specs.length === 0) {
429
- return {
430
- content: [{
431
- type: "text",
432
- text: "Could not decompose the job description into automatable skills. Try providing more detail about your daily tasks.",
433
- }],
434
- };
435
- }
436
-
437
- // If not auto-creating, return the decomposition plan
438
- if (!args.auto_create) {
439
- let response = `## Job Analysis: ${args.job_name}\n\nI identified **${specs.length} skills** from your job description:\n\n`;
440
423
 
441
- const byPriority = { high: [] as typeof specs, medium: [] as typeof specs, low: [] as typeof specs };
442
- for (const s of specs) {
443
- (byPriority[s.priority] || byPriority.medium).push(s);
444
- }
445
-
446
- for (const [priority, items] of Object.entries(byPriority)) {
447
- if (items.length === 0) continue;
448
- const label = priority === "high" ? "High Priority" : priority === "medium" ? "Medium Priority" : "Low Priority";
449
- response += `### ${label}\n`;
450
- for (const s of items) {
451
- const auto = s.automatable ? "" : " (needs human assistance)";
452
- response += `- **${s.name}**: ${s.description}${auto}\n`;
453
- }
454
- response += "\n";
455
- }
424
+ let response = `## Job: ${args.job_name}\n`;
425
+ response += `**Description:** ${args.job_description}\n\n`;
456
426
 
457
- response += `Call \`skill_generate\` again with \`job_name: "${args.job_name}"\` and \`auto_create: true\` to create all these skills, `;
458
- response += "or use `skill_create` to create individual skills manually.";
459
-
460
- return { content: [{ type: "text", text: response }] };
461
- }
462
-
463
- // Step 2: Auto-create — generate full skills for each spec
464
- const created: string[] = [];
465
- const failed: string[] = [];
466
-
467
- // Generate skills in parallel (batch of 3 to avoid rate limits)
468
- for (let i = 0; i < specs.length; i += 3) {
469
- const batch = specs.slice(i, i + 3);
470
- const results = await Promise.allSettled(
471
- batch.map((spec) =>
472
- generateSkillFromDescription(spec.name, spec.description, args.job_description)
473
- )
474
- );
475
-
476
- for (let j = 0; j < results.length; j++) {
477
- const spec = batch[j];
478
- const result = results[j];
479
-
480
- if (result.status === "fulfilled" && result.value) {
481
- const generated = result.value;
482
- // Check for duplicates
483
- const existing = skillManager.findSimilar(generated.name);
484
- if (existing) {
485
- failed.push(`${spec.name} (duplicate of "${existing.name}")`);
486
- continue;
487
- }
488
-
489
- // Create in skills table (draft)
490
- const createResult = await skillManager.create(
491
- generated.name,
492
- generated.description || spec.description,
493
- generated.steps,
494
- {
495
- source: "job_generated",
496
- emoji: generated.emoji,
497
- keywords: generated.keywords,
498
- }
499
- );
500
-
501
- if (createResult) {
502
- // Add to user's collection (approval)
503
- const added = await skillManager.addSkill(createResult.id);
504
- if (added) {
505
- created.push(generated.name);
506
- } else {
507
- failed.push(`${spec.name} (add failed)`);
508
- }
509
- } else {
510
- failed.push(spec.name);
511
- }
512
- } else {
513
- failed.push(spec.name);
514
- }
515
- }
427
+ if (existingNames.length > 0) {
428
+ response += `**Existing skills (do NOT duplicate):** ${existingNames.join(", ")}\n\n`;
516
429
  }
517
430
 
518
- // Step 3: Save job to DB, link generated skills, and mark as analyzed
519
- if (userId) {
520
- saveJobToDb(
521
- userId,
522
- args.job_name,
523
- args.job_description,
524
- created
525
- ).catch((err) => {
526
- log.debug(`saveJobToDb error: ${err}`);
527
- });
528
- }
431
+ response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills. `;
432
+ response += `For each skill, call \`skill_create\` with:\n`;
433
+ response += `- name: kebab-case name (e.g. "slack-message-check")\n`;
434
+ response += `- description: one-line description\n`;
435
+ response += `- instructions: detailed step-by-step markdown instructions the agent can follow\n`;
436
+ response += `- emoji: a single emoji representing the skill\n\n`;
437
+ response += `After creating each skill, call \`skill_add\` with the returned skill ID to add it to the user's collection.\n\n`;
438
+ response += `After ALL skills are created and added, 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`;
439
+ response += `**Guidelines for skill instructions:**\n`;
440
+ response += `- Write clear, actionable markdown steps\n`;
441
+ response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks\n`;
442
+ response += `- Include error handling steps\n`;
443
+ response += `- Use placeholders like {query}, {date} for variable inputs\n`;
444
+ response += `- Each skill should be a single, well-defined workflow (10-25 steps)\n`;
529
445
 
530
- let response = `## Skills Generated for "${args.job_name}"\n\n`;
531
- response += `Created **${created.length}/${specs.length}** skills from your job description:\n\n`;
532
-
533
- for (const name of created) {
534
- const skill = skillManager.get(name);
535
- const emoji = skill?.metadata.emoji || "";
536
- response += `- ${emoji} **${name}**: ${skill?.description || ""}\n`;
446
+ return { content: [{ type: "text", text: response }] };
447
+ }
448
+ ),
449
+ tool(
450
+ "skill_link_job",
451
+ "Link created skills to a job and mark it as analyzed. Call this after creating all skills for a job via skill_create + skill_add.",
452
+ {
453
+ job_name: z.string().describe("Name of the job to link skills to"),
454
+ job_description: z.string().describe("Job description (used if job doesn't exist yet)"),
455
+ skill_names: z.array(z.string()).describe("Names of skills to link to this job"),
456
+ },
457
+ async (args) => {
458
+ if (!userId) {
459
+ return {
460
+ content: [{ type: "text", text: "Not authenticated. Cannot link job." }],
461
+ };
537
462
  }
538
463
 
539
- if (failed.length > 0) {
540
- response += `\nFailed to create: ${failed.join(", ")}\n`;
464
+ try {
465
+ await saveJobToDb(userId, args.job_name, args.job_description, args.skill_names);
466
+ log.success(`Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`);
467
+ return {
468
+ content: [{
469
+ type: "text",
470
+ text: `Job "${args.job_name}" linked with ${args.skill_names.length} skills and marked as analyzed.`,
471
+ }],
472
+ };
473
+ } catch (err) {
474
+ return {
475
+ content: [{
476
+ type: "text",
477
+ text: `Failed to link job: ${err instanceof Error ? err.message : err}`,
478
+ }],
479
+ };
541
480
  }
542
-
543
- response += "\nThese skills are now available. When you give me a task that matches, I'll automatically use the right skill.";
544
- response += "\nUse `skill_invoke` to test any skill, or `skill_improve` to refine them after use.";
545
-
546
- log.success(`Job "${args.job_name}": created ${created.length} skills`);
547
-
548
- return { content: [{ type: "text", text: response }] };
549
481
  }
550
482
  ),
551
483
  tool(
@@ -20,7 +20,6 @@ import { log, newCorrelationId, setCorrelationId } from "../utils/logger.js";
20
20
  import { getBrowser } from "../tools/browser.js";
21
21
  import { MemoryManager } from "./memory.js";
22
22
  import { SkillManager } from "./skills.js";
23
- import { extractMemoriesWithLLM } from "./memory-extractor.js";
24
23
  import { type ToolCallRecord } from "./skill-extractor.js";
25
24
  import { withRetry } from "../utils/retry.js";
26
25
  import {
@@ -56,6 +55,8 @@ Available capabilities:
56
55
  - You can remember things about the user using memory_store
57
56
  - Use this when you learn preferences, important facts, or standing instructions
58
57
  - Your stored memories persist across conversations
58
+ - PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
59
+ - Before completing a task, consider if anything learned should be remembered for future conversations
59
60
 
60
61
  4. SKILL PLANNING (pre-task):
61
62
  - Before executing a complex task, analyze if it matches an existing skill (use skill_invoke)
@@ -232,6 +233,7 @@ export class TaskProcessor {
232
233
  "mcp__assistme-agent__skill_invoke",
233
234
  "mcp__assistme-agent__skill_search",
234
235
  "mcp__assistme-agent__skill_generate",
236
+ "mcp__assistme-agent__skill_link_job",
235
237
  "mcp__assistme-agent__skill_browse",
236
238
  "mcp__assistme-agent__skill_add",
237
239
  "mcp__assistme-agent__skill_publish",
@@ -367,35 +369,9 @@ export class TaskProcessor {
367
369
  }
368
370
  this.historyCache.set(task.conversation_id, convHistory);
369
371
 
370
- // ── Post-task extraction (fire-and-forget, non-blocking) ──────
371
-
372
- // Auto-extract memories using LLM
373
- if (this.memoryManager && finalResponse) {
374
- const mm = this.memoryManager;
375
- const taskIdRef = task.id;
376
- extractMemoriesWithLLM(task.prompt, finalResponse)
377
- .then(async (memories) => {
378
- for (const mem of memories) {
379
- try {
380
- await mm.remember(mem.content, mem.category, {
381
- importance: mem.importance,
382
- tags: mem.tags,
383
- sourceMessageId: taskIdRef,
384
- });
385
- log.info(`Memory extracted: [${mem.category}] ${mem.content.slice(0, 60)}...`);
386
- } catch {
387
- // Non-critical — skip individual memory failures
388
- }
389
- }
390
- if (memories.length > 0) {
391
- log.success(`${memories.length} memory(s) auto-extracted`);
392
- }
393
- })
394
- .catch(() => {});
395
- }
396
-
397
- // Note: Skill creation/improvement is now handled pre-task via the
398
- // skill_create → user approval → skill_add flow, not post-task extraction.
372
+ // Note: Memory extraction and skill creation are handled by the agent itself
373
+ // during task execution via memory_store and skill_create tools.
374
+ // No separate LLM API calls needed — the agent SDK handles everything.
399
375
  } catch (err) {
400
376
  const errorMsg = err instanceof Error ? err.message : String(err);
401
377
  log.error(`Task failed: ${errorMsg}`);
@@ -228,12 +228,19 @@ export async function decomposeJob(
228
228
  }),
229
229
  });
230
230
 
231
- if (!response.ok) return [];
231
+ if (!response.ok) {
232
+ const errText = await response.text().catch(() => "");
233
+ log.debug(`decomposeJob API error: ${response.status} ${errText.slice(0, 300)}`);
234
+ return [];
235
+ }
232
236
 
233
237
  const data = await response.json();
234
238
  const text =
235
239
  data.content?.[0]?.type === "text" ? data.content[0].text : "";
236
- if (!text) return [];
240
+ if (!text) {
241
+ log.debug(`decomposeJob: empty response from API`);
242
+ return [];
243
+ }
237
244
 
238
245
  const jsonStr = text
239
246
  .replace(/```json\n?/g, "")
@@ -241,7 +248,10 @@ export async function decomposeJob(
241
248
  .trim();
242
249
  const specs: SkillSpec[] = JSON.parse(jsonStr);
243
250
 
244
- if (!Array.isArray(specs)) return [];
251
+ if (!Array.isArray(specs)) {
252
+ log.debug(`decomposeJob: response is not an array: ${jsonStr.slice(0, 200)}`);
253
+ return [];
254
+ }
245
255
 
246
256
  // Sanitize names
247
257
  return specs
@@ -255,7 +265,7 @@ export async function decomposeJob(
255
265
  .replace(/^-|-$/g, ""),
256
266
  }));
257
267
  } catch (err) {
258
- log.debug(`Job decomposition failed: ${err}`);
268
+ log.debug(`Job decomposition failed: ${err instanceof Error ? err.message : err}`);
259
269
  return [];
260
270
  }
261
271
  }
@@ -321,12 +331,19 @@ export async function generateSkillFromDescription(
321
331
  }),
322
332
  });
323
333
 
324
- if (!response.ok) return null;
334
+ if (!response.ok) {
335
+ const errText = await response.text().catch(() => "");
336
+ log.debug(`generateSkill API error: ${response.status} ${errText.slice(0, 300)}`);
337
+ return null;
338
+ }
325
339
 
326
340
  const data = await response.json();
327
341
  const text =
328
342
  data.content?.[0]?.type === "text" ? data.content[0].text : "";
329
- if (!text) return null;
343
+ if (!text) {
344
+ log.debug(`generateSkill: empty response from API`);
345
+ return null;
346
+ }
330
347
 
331
348
  const jsonStr = text
332
349
  .replace(/```json\n?/g, "")
@@ -334,7 +351,10 @@ export async function generateSkillFromDescription(
334
351
  .trim();
335
352
  const skill = JSON.parse(jsonStr);
336
353
 
337
- if (!skill || !skill.name || !skill.steps) return null;
354
+ if (!skill || !skill.name || !skill.steps) {
355
+ log.debug(`generateSkill: invalid response structure: ${jsonStr.slice(0, 200)}`);
356
+ return null;
357
+ }
338
358
 
339
359
  skill.name = skill.name
340
360
  .toLowerCase()
@@ -344,7 +364,7 @@ export async function generateSkillFromDescription(
344
364
 
345
365
  return skill;
346
366
  } catch (err) {
347
- log.debug(`Skill generation failed: ${err}`);
367
+ log.debug(`Skill generation failed: ${err instanceof Error ? err.message : err}`);
348
368
  return null;
349
369
  }
350
370
  }
@@ -371,33 +371,31 @@ export class SkillManager {
371
371
  ? { openclaw: { emoji: options.emoji } }
372
372
  : {};
373
373
 
374
- const { data, error } = await sb
375
- .from("skills")
376
- .upsert(
377
- {
378
- name,
379
- description,
380
- content,
381
- version: "1.0.0",
382
- author_id: this.userId,
383
- emoji: options?.emoji || null,
384
- keywords: options?.keywords || [],
385
- metadata,
386
- source,
387
- is_public: false,
388
- },
389
- { onConflict: "author_id,name" }
390
- )
391
- .select("id, name")
392
- .single();
374
+ const { data, error } = await sb.rpc("create_skill", {
375
+ p_user_id: this.userId,
376
+ p_name: name,
377
+ p_description: description,
378
+ p_content: content,
379
+ p_version: "1.0.0",
380
+ p_source: source,
381
+ p_emoji: options?.emoji || null,
382
+ p_keywords: options?.keywords || [],
383
+ p_metadata: metadata,
384
+ });
393
385
 
394
386
  if (error) {
395
387
  log.debug(`Skill create failed for "${name}": ${error.message}`);
396
388
  return null;
397
389
  }
398
390
 
391
+ const row = Array.isArray(data) ? data[0] : data;
392
+ if (!row) {
393
+ log.debug(`Skill create returned no data for "${name}"`);
394
+ return null;
395
+ }
396
+
399
397
  log.info(`Skill "${name}" created in skills table (pending approval)`);
400
- return data as { id: string; name: string };
398
+ return row as { id: string; name: string };
401
399
  } catch (err) {
402
400
  log.debug(`Skill create error: ${err}`);
403
401
  return null;