assistme 0.2.2 → 0.2.4

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,30 @@ _(${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
  }
1987
- log.info(`Skill "${name}" created in skills table (pending approval)`);
1988
- return data;
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
+ }
1988
+ const id = row.out_id || row.id;
1989
+ const skillName = row.out_name || row.name;
1990
+ log.info(`Skill "${skillName}" created in skills table (pending approval)`);
1991
+ return { id, name: skillName };
1989
1992
  } catch (err) {
1990
1993
  log.debug(`Skill create error: ${err}`);
1991
1994
  return null;
@@ -2327,91 +2330,6 @@ function preprocessDynamicContext(content, cwd) {
2327
2330
  });
2328
2331
  }
2329
2332
 
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
2333
  // src/utils/retry.ts
2416
2334
  async function withRetry(fn, opts = {}) {
2417
2335
  const {
@@ -2883,138 +2801,6 @@ function getLimiterForTool(toolName) {
2883
2801
  return null;
2884
2802
  }
2885
2803
 
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
2804
  // src/agent/mcp-servers.ts
3019
2805
  async function callTool(name, input) {
3020
2806
  const limiter = getLimiterForTool(name);
@@ -3356,130 +3142,93 @@ ${content}`;
3356
3142
  ),
3357
3143
  tool(
3358
3144
  "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.",
3145
+ "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
3146
  {
3361
3147
  job_name: z.string().describe(
3362
3148
  "Short name for this job/role. Example: '\u7535\u5546\u8FD0\u8425', 'Frontend Dev', 'Data Analyst'"
3363
3149
  ),
3364
3150
  job_description: z.string().describe(
3365
3151
  "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.")
3152
+ )
3368
3153
  },
3369
3154
  async (args) => {
3370
3155
  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}
3156
+ let response = `## Job: ${args.job_name}
3157
+ `;
3158
+ response += `**Description:** ${args.job_description}
3382
3159
 
3383
- I identified **${specs.length} skills** from your job description:
3160
+ `;
3161
+ if (existingNames.length > 0) {
3162
+ response += `**Existing skills (do NOT duplicate):** ${existingNames.join(", ")}
3384
3163
 
3385
3164
  `;
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}
3165
+ }
3166
+ response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills. `;
3167
+ response += `For each skill, call \`skill_create\` with:
3394
3168
  `;
3395
- for (const s of items) {
3396
- const auto = s.automatable ? "" : " (needs human assistance)";
3397
- response2 += `- **${s.name}**: ${s.description}${auto}
3169
+ response += `- name: kebab-case name (e.g. "slack-message-check")
3398
3170
  `;
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}"
3171
+ response += `- description: one-line description
3172
+ `;
3173
+ response += `- instructions: detailed step-by-step markdown instructions the agent can follow
3174
+ `;
3175
+ response += `- emoji: a single emoji representing the skill
3461
3176
 
3462
3177
  `;
3463
- response += `Created **${created.length}/${specs.length}** skills from your job description:
3178
+ response += `After creating each skill, call \`skill_add\` with the returned skill ID to add it to the user's collection.
3464
3179
 
3465
3180
  `;
3466
- for (const name of created) {
3467
- const skill = skillManager.get(name);
3468
- const emoji = skill?.metadata.emoji || "";
3469
- response += `- ${emoji} **${name}**: ${skill?.description || ""}
3181
+ 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.
3182
+
3470
3183
  `;
3471
- }
3472
- if (failed.length > 0) {
3473
- response += `
3474
- Failed to create: ${failed.join(", ")}
3184
+ response += `**Guidelines for skill instructions:**
3185
+ `;
3186
+ response += `- Write clear, actionable markdown steps
3187
+ `;
3188
+ response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks
3189
+ `;
3190
+ response += `- Include error handling steps
3191
+ `;
3192
+ response += `- Use placeholders like {query}, {date} for variable inputs
3193
+ `;
3194
+ response += `- Each skill should be a single, well-defined workflow (10-25 steps)
3475
3195
  `;
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
3196
  return { content: [{ type: "text", text: response }] };
3481
3197
  }
3482
3198
  ),
3199
+ tool(
3200
+ "skill_link_job",
3201
+ "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.",
3202
+ {
3203
+ job_name: z.string().describe("Name of the job to link skills to"),
3204
+ job_description: z.string().describe("Job description (used if job doesn't exist yet)"),
3205
+ skill_names: z.array(z.string()).describe("Names of skills to link to this job")
3206
+ },
3207
+ async (args) => {
3208
+ if (!userId) {
3209
+ return {
3210
+ content: [{ type: "text", text: "Not authenticated. Cannot link job." }]
3211
+ };
3212
+ }
3213
+ try {
3214
+ await saveJobToDb(userId, args.job_name, args.job_description, args.skill_names);
3215
+ log.success(`Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`);
3216
+ return {
3217
+ content: [{
3218
+ type: "text",
3219
+ text: `Job "${args.job_name}" linked with ${args.skill_names.length} skills and marked as analyzed.`
3220
+ }]
3221
+ };
3222
+ } catch (err) {
3223
+ return {
3224
+ content: [{
3225
+ type: "text",
3226
+ text: `Failed to link job: ${err instanceof Error ? err.message : err}`
3227
+ }]
3228
+ };
3229
+ }
3230
+ }
3231
+ ),
3483
3232
  tool(
3484
3233
  "skill_browse",
3485
3234
  "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 +3626,8 @@ Available capabilities:
3877
3626
  - You can remember things about the user using memory_store
3878
3627
  - Use this when you learn preferences, important facts, or standing instructions
3879
3628
  - Your stored memories persist across conversations
3629
+ - PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
3630
+ - Before completing a task, consider if anything learned should be remembered for future conversations
3880
3631
 
3881
3632
  4. SKILL PLANNING (pre-task):
3882
3633
  - Before executing a complex task, analyze if it matches an existing skill (use skill_invoke)
@@ -4019,6 +3770,7 @@ var TaskProcessor = class {
4019
3770
  "mcp__assistme-agent__skill_invoke",
4020
3771
  "mcp__assistme-agent__skill_search",
4021
3772
  "mcp__assistme-agent__skill_generate",
3773
+ "mcp__assistme-agent__skill_link_job",
4022
3774
  "mcp__assistme-agent__skill_browse",
4023
3775
  "mcp__assistme-agent__skill_add",
4024
3776
  "mcp__assistme-agent__skill_publish",
@@ -4132,27 +3884,6 @@ var TaskProcessor = class {
4132
3884
  convHistory.splice(0, convHistory.length - MAX_HISTORY_ENTRIES * 2);
4133
3885
  }
4134
3886
  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
3887
  } catch (err) {
4157
3888
  const errorMsg = err instanceof Error ? err.message : String(err);
4158
3889
  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.4",
4
4
  "description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",