assistme 0.3.0 → 0.3.2
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/PLAN.md +14 -3
- package/dist/{chunk-UWE5WVQI.js → chunk-KX7ITO55.js} +20 -11
- package/dist/index.js +1791 -572
- package/dist/{job-runner-N4XAAWLJ.js → job-runner-P2L6MOOX.js} +1 -1
- package/package.json +5 -3
- package/src/agent/job-runner.ts +9 -13
- package/src/agent/mcp-servers.ts +6 -1020
- package/src/agent/memory.ts +2 -11
- package/src/agent/processor.ts +18 -108
- package/src/agent/scheduler.ts +2 -3
- package/src/agent/session.ts +20 -36
- package/src/agent/skills.ts +167 -61
- package/src/agent/system-prompt.ts +126 -0
- package/src/browser/chrome-launcher.ts +555 -0
- package/src/browser/controller.ts +1386 -0
- package/src/browser/types.ts +70 -0
- package/src/commands/credential.ts +190 -0
- package/src/commands/job.ts +14 -45
- package/src/commands/memory.ts +16 -29
- package/src/commands/schedule.ts +15 -37
- package/src/commands/start.ts +11 -43
- package/src/credentials/credential-store.test.ts +162 -0
- package/src/credentials/credential-store.ts +266 -0
- package/src/credentials/encryption.test.ts +98 -0
- package/src/credentials/encryption.ts +82 -0
- package/src/credentials/index.ts +15 -0
- package/src/credentials/local-store.ts +89 -0
- package/src/db/action.ts +19 -0
- package/src/db/api-client.ts +3 -32
- package/src/db/auth-store.ts +41 -0
- package/src/db/auth.ts +38 -0
- package/src/db/conversation.ts +39 -0
- package/src/db/event.ts +52 -0
- package/src/db/job-poll.ts +18 -0
- package/src/db/session.ts +60 -0
- package/src/db/supabase.ts +40 -383
- package/src/db/task.ts +69 -0
- package/src/db/types.ts +54 -0
- package/src/index.ts +2 -0
- package/src/mcp/agent-tools-server.ts +1047 -0
- package/src/mcp/browser-server.ts +258 -0
- package/src/tools/browser.ts +28 -1208
- package/src/tools/index.ts +32 -263
- package/src/tools/web.ts +0 -73
package/src/agent/memory.ts
CHANGED
|
@@ -26,10 +26,6 @@ export interface Memory {
|
|
|
26
26
|
// ── Memory Manager ──────────────────────────────────────────────────
|
|
27
27
|
|
|
28
28
|
export class MemoryManager {
|
|
29
|
-
constructor(_userId: string) {
|
|
30
|
-
// userId is no longer needed — auth is handled by the MCP token in callMcpHandler
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
/**
|
|
34
30
|
* Store a new memory. Called by the agent after completing tasks
|
|
35
31
|
* to remember important facts about the user.
|
|
@@ -45,9 +41,7 @@ export class MemoryManager {
|
|
|
45
41
|
}
|
|
46
42
|
): Promise<Memory> {
|
|
47
43
|
const expiresAt = options?.expiresInDays
|
|
48
|
-
? new Date(
|
|
49
|
-
Date.now() + options.expiresInDays * 86400_000
|
|
50
|
-
).toISOString()
|
|
44
|
+
? new Date(Date.now() + options.expiresInDays * 86400_000).toISOString()
|
|
51
45
|
: null;
|
|
52
46
|
|
|
53
47
|
const data = await callMcpHandler<Memory>("memory.store", {
|
|
@@ -132,10 +126,7 @@ export class MemoryManager {
|
|
|
132
126
|
|
|
133
127
|
// ── CRUD for CLI ────────────────────────────────────────────
|
|
134
128
|
|
|
135
|
-
async list(
|
|
136
|
-
category?: MemoryCategory,
|
|
137
|
-
limit = 20
|
|
138
|
-
): Promise<Memory[]> {
|
|
129
|
+
async list(category?: MemoryCategory, limit = 20): Promise<Memory[]> {
|
|
139
130
|
const data = await callMcpHandler<Memory[]>("memory.list", {
|
|
140
131
|
category: category || null,
|
|
141
132
|
limit,
|
package/src/agent/processor.ts
CHANGED
|
@@ -29,100 +29,7 @@ import {
|
|
|
29
29
|
BROWSER_TOOL_NAMES,
|
|
30
30
|
} from "./mcp-servers.js";
|
|
31
31
|
import { createEventHooks } from "./event-hooks.js";
|
|
32
|
-
|
|
33
|
-
const BASE_SYSTEM_PROMPT = `You are AssistMe, an AI assistant that operates like a real human on the user's computer. You control the user's actual Chrome browser and work with their real files.
|
|
34
|
-
|
|
35
|
-
KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This means:
|
|
36
|
-
- The browser has the user's real cookies, logins, and sessions
|
|
37
|
-
- When you navigate to amazon.com, you see the user's logged-in Amazon
|
|
38
|
-
- If a site needs login, the browser will auto-detect the login page and prompt the user
|
|
39
|
-
- After the user logs in, their session is saved in the persistent browser profile (~/.assistme/browser-profile)
|
|
40
|
-
- Saved sessions persist across assistme restarts — the user only needs to log in once per site
|
|
41
|
-
- You are like a human assistant sitting at the user's computer
|
|
42
|
-
- Chrome is automatically managed — just call browser_connect and it will auto-launch if needed
|
|
43
|
-
- NEVER ask the user to manually start Chrome or run any terminal commands for browser setup
|
|
44
|
-
|
|
45
|
-
Available capabilities:
|
|
46
|
-
1. BROWSER CONTROL (user's real Chrome via CDP):
|
|
47
|
-
- Use browser tools (browser_connect, browser_navigate, browser_read_page, browser_screenshot, browser_click, browser_type, browser_press_key, browser_scroll, browser_get_elements, browser_evaluate, browser_list_tabs, browser_switch_tab, browser_new_tab) to control the user's real Chrome
|
|
48
|
-
- If auth is needed: use browser_request_user_action to ask the user to log in
|
|
49
|
-
|
|
50
|
-
2. FILE OPERATIONS & SHELL:
|
|
51
|
-
- Read, Write, Edit tools for file operations
|
|
52
|
-
- Bash tool for shell commands
|
|
53
|
-
- Glob and Grep for file search
|
|
54
|
-
|
|
55
|
-
3. MEMORY:
|
|
56
|
-
- You can remember things about the user using memory_store
|
|
57
|
-
- Use this when you learn preferences, important facts, or standing instructions
|
|
58
|
-
- Your stored memories persist across conversations
|
|
59
|
-
- PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
|
|
60
|
-
- Before completing a task, consider if anything learned should be remembered for future conversations
|
|
61
|
-
|
|
62
|
-
4. SKILL-AWARE EXECUTION (CRITICAL — follow this for EVERY task):
|
|
63
|
-
Step A — Search: Before executing ANY task, check if an existing skill matches (use skill_invoke or skill_search).
|
|
64
|
-
Step B — If skill found: load it with skill_invoke and follow its instructions precisely. If the instructions are incomplete or wrong, adapt and improve as you go — note what changed.
|
|
65
|
-
Step C — If NO skill found: BEFORE executing, draft a skill plan following the Agent Skills format:
|
|
66
|
-
Skill Draft: [kebab-case-name]
|
|
67
|
-
Description: [what this skill does and when to use it]
|
|
68
|
-
Steps:
|
|
69
|
-
1. [first step]
|
|
70
|
-
2. [second step]
|
|
71
|
-
...
|
|
72
|
-
The draft should be a reusable workflow, not specific to this one request. Use generic placeholders where the user provided specific values.
|
|
73
|
-
Step D — Execute: Follow the skill draft (or loaded skill) step by step. Refine the draft as you discover better approaches, edge cases, or missing steps.
|
|
74
|
-
Step E — After execution: The system will automatically evaluate whether to save the skill. You do NOT need to call skill_create manually.
|
|
75
|
-
|
|
76
|
-
Agent Skills format reference (agentskills.io):
|
|
77
|
-
- name: 1-64 chars, lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens
|
|
78
|
-
- description: 1-1024 chars, describe what the skill does AND when to use it, include keywords for discoverability
|
|
79
|
-
- body: markdown step-by-step instructions, examples, edge cases. Keep under 500 lines.
|
|
80
|
-
- Progressive disclosure: metadata (~100 tokens) → instructions (<5000 tokens) → references (on demand)
|
|
81
|
-
|
|
82
|
-
5. JOB AUTOMATION:
|
|
83
|
-
- When the user describes their job/role/daily work, use skill_generate to decompose it into automatable skills
|
|
84
|
-
- ALWAYS use request_user_confirmation to get user approval before creating skills — never create skills without approval
|
|
85
|
-
- Use job_run to start a job — it gives you the job's goal and available skills as capabilities
|
|
86
|
-
- When running a job, be AGENTIC: decide dynamically what to do based on what you discover
|
|
87
|
-
- Do NOT follow a fixed sequence — if checking Slack reveals a task that needs GitHub, go do GitHub immediately
|
|
88
|
-
- Chain actions intelligently: one skill's findings should inform your next move
|
|
89
|
-
- Skip irrelevant skills, use tools directly when skills aren't needed
|
|
90
|
-
- Use job_schedule for recurring automation (e.g., "run my SE job every weekday morning")
|
|
91
|
-
- Use job_status to check run history
|
|
92
|
-
|
|
93
|
-
6. SKILL MARKETPLACE:
|
|
94
|
-
- Use skill_browse to discover community-published skills
|
|
95
|
-
- Use skill_add to add marketplace skills to the user's collection
|
|
96
|
-
- Use skill_publish to share the user's skills with the community
|
|
97
|
-
|
|
98
|
-
Workflow for web tasks (e.g. "查一下 kindle 最新款价格"):
|
|
99
|
-
1. browser_connect → connect to user's Chrome
|
|
100
|
-
2. browser_new_tab → open a new tab
|
|
101
|
-
3. browser_navigate → go to the website (login pages are auto-detected — the user will be prompted and their session saved)
|
|
102
|
-
4. browser_read_page or browser_screenshot → read the content
|
|
103
|
-
5. If login is needed but not auto-detected → use browser_request_user_action to ask the user
|
|
104
|
-
6. Repeat across multiple sites as needed
|
|
105
|
-
7. Summarize findings
|
|
106
|
-
|
|
107
|
-
Guidelines:
|
|
108
|
-
- Always use the real browser for web tasks, never try to fetch URLs programmatically
|
|
109
|
-
- Use browser_screenshot when you need to see the visual layout
|
|
110
|
-
- Use browser_get_elements to find clickable elements before clicking
|
|
111
|
-
- Login pages are auto-detected after navigation — the user is prompted and sessions are saved automatically
|
|
112
|
-
- If auto-detection misses a login page, use browser_request_user_action manually
|
|
113
|
-
- Be thorough: check multiple sources when comparing prices/products
|
|
114
|
-
- Summarize results clearly at the end
|
|
115
|
-
- When you learn something about the user (preferences, habits), use memory_store to remember it
|
|
116
|
-
|
|
117
|
-
CRITICAL — Ask before you guess:
|
|
118
|
-
- Before executing a task, verify you have all required information. If anything is ambiguous or missing, use request_user_input to ask.
|
|
119
|
-
- First try to resolve unknowns yourself: check memories, read workspace files (e.g. git remote, config files), or infer from conversation history.
|
|
120
|
-
- If you still lack a critical piece of information after self-resolution, ASK the user via request_user_input. Do NOT guess, assume defaults, or proceed with incomplete information.
|
|
121
|
-
- Examples of when to ask: which account/repo/project to target, what format the user wants, which of multiple options to choose, credentials or URLs that cannot be inferred.
|
|
122
|
-
- Keep questions specific and actionable. Explain what you already know and what exactly you need.
|
|
123
|
-
- After receiving the answer, store it with memory_store if it is likely to be useful in future conversations.
|
|
124
|
-
|
|
125
|
-
Workspace path: {workspace_path}`;
|
|
32
|
+
import { BASE_SYSTEM_PROMPT } from "./system-prompt.js";
|
|
126
33
|
|
|
127
34
|
const MAX_HISTORY_ENTRIES = 10;
|
|
128
35
|
const MAX_RESPONSE_LENGTH = 1500;
|
|
@@ -130,7 +37,6 @@ const MAX_RESPONSE_LENGTH = 1500;
|
|
|
130
37
|
export class TaskProcessor {
|
|
131
38
|
private memoryManager: MemoryManager | null = null;
|
|
132
39
|
private skillManager: SkillManager;
|
|
133
|
-
private userId: string | null = null;
|
|
134
40
|
private sessionId: string | null = null;
|
|
135
41
|
/** In-memory conversation history, keyed by conversation_id */
|
|
136
42
|
private historyCache: Map<string, HistoryEntry[]> = new Map();
|
|
@@ -139,9 +45,8 @@ export class TaskProcessor {
|
|
|
139
45
|
this.skillManager = new SkillManager();
|
|
140
46
|
}
|
|
141
47
|
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
this.memoryManager = new MemoryManager(userId);
|
|
48
|
+
init(userId: string): void {
|
|
49
|
+
this.memoryManager = new MemoryManager();
|
|
145
50
|
this.skillManager.setUserId(userId);
|
|
146
51
|
// Load DB skills asynchronously (non-blocking)
|
|
147
52
|
this.skillManager.loadFromDb().catch((err) => {
|
|
@@ -158,10 +63,7 @@ export class TaskProcessor {
|
|
|
158
63
|
* to create/update a skill. The agent already has full context from
|
|
159
64
|
* the task it just completed — no need to re-describe anything.
|
|
160
65
|
*/
|
|
161
|
-
private async evaluateSkillPostTask(
|
|
162
|
-
agentSessionId: string,
|
|
163
|
-
model: string
|
|
164
|
-
): Promise<void> {
|
|
66
|
+
private async evaluateSkillPostTask(agentSessionId: string, model: string): Promise<void> {
|
|
165
67
|
await evaluateAndMaybeCreateSkill({
|
|
166
68
|
sessionId: agentSessionId,
|
|
167
69
|
skillManager: this.skillManager,
|
|
@@ -248,7 +150,6 @@ export class TaskProcessor {
|
|
|
248
150
|
skillManager: this.skillManager,
|
|
249
151
|
taskId: task.id,
|
|
250
152
|
sessionId: this.sessionId || undefined,
|
|
251
|
-
userId: this.userId || undefined,
|
|
252
153
|
});
|
|
253
154
|
|
|
254
155
|
// Create event hooks for Supabase event emission
|
|
@@ -277,12 +178,16 @@ export class TaskProcessor {
|
|
|
277
178
|
"mcp__assistme-agent__skill_add",
|
|
278
179
|
"mcp__assistme-agent__skill_publish",
|
|
279
180
|
// User interaction
|
|
280
|
-
"mcp__assistme-
|
|
281
|
-
"mcp__assistme-agent__request_user_confirmation",
|
|
181
|
+
"mcp__assistme-agent__ask_user",
|
|
282
182
|
// Job automation tools
|
|
283
183
|
"mcp__assistme-agent__job_run",
|
|
284
184
|
"mcp__assistme-agent__job_schedule",
|
|
285
185
|
"mcp__assistme-agent__job_status",
|
|
186
|
+
// Credential tools (local storage)
|
|
187
|
+
"mcp__assistme-agent__credential_get",
|
|
188
|
+
"mcp__assistme-agent__credential_set",
|
|
189
|
+
"mcp__assistme-agent__credential_list",
|
|
190
|
+
"mcp__assistme-agent__credential_remove",
|
|
286
191
|
];
|
|
287
192
|
|
|
288
193
|
// Build async generator for prompt (required for MCP tools)
|
|
@@ -385,7 +290,11 @@ export class TaskProcessor {
|
|
|
385
290
|
|
|
386
291
|
default:
|
|
387
292
|
// Capture session ID from init message for post-task session resume
|
|
388
|
-
if (
|
|
293
|
+
if (
|
|
294
|
+
message.type === "system" &&
|
|
295
|
+
"subtype" in message &&
|
|
296
|
+
(message as Record<string, unknown>).subtype === "init"
|
|
297
|
+
) {
|
|
389
298
|
agentSessionId = (message as Record<string, unknown>).session_id as string;
|
|
390
299
|
}
|
|
391
300
|
log.debug(`SDK message type: ${message.type}`);
|
|
@@ -416,8 +325,9 @@ export class TaskProcessor {
|
|
|
416
325
|
|
|
417
326
|
// Post-task: resume the same session to evaluate skill creation (fire-and-forget)
|
|
418
327
|
if (agentSessionId) {
|
|
419
|
-
this.evaluateSkillPostTask(agentSessionId, config.model)
|
|
420
|
-
|
|
328
|
+
this.evaluateSkillPostTask(agentSessionId, config.model).catch((err) =>
|
|
329
|
+
log.debug(`Post-task skill evaluation skipped: ${err}`)
|
|
330
|
+
);
|
|
421
331
|
}
|
|
422
332
|
} catch (err) {
|
|
423
333
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
package/src/agent/scheduler.ts
CHANGED
|
@@ -174,7 +174,6 @@ export class Scheduler {
|
|
|
174
174
|
// ── CRUD helpers for CLI commands ──────────────────────────────────
|
|
175
175
|
|
|
176
176
|
export async function createScheduledTask(
|
|
177
|
-
_userId: string,
|
|
178
177
|
name: string,
|
|
179
178
|
prompt: string,
|
|
180
179
|
cronExpression: string,
|
|
@@ -191,7 +190,7 @@ export async function createScheduledTask(
|
|
|
191
190
|
});
|
|
192
191
|
}
|
|
193
192
|
|
|
194
|
-
export async function listScheduledTasks(
|
|
193
|
+
export async function listScheduledTasks(): Promise<ScheduledTask[]> {
|
|
195
194
|
return callMcpHandler<ScheduledTask[]>("schedule.list");
|
|
196
195
|
}
|
|
197
196
|
|
|
@@ -202,7 +201,7 @@ export async function toggleScheduledTask(taskId: string, enabled: boolean): Pro
|
|
|
202
201
|
// Need to recalculate next run when re-enabling
|
|
203
202
|
const taskData = await callMcpHandler<{ cron_expression: string; timezone: string }>(
|
|
204
203
|
"schedule.get_task",
|
|
205
|
-
{ task_id: taskId }
|
|
204
|
+
{ task_id: taskId }
|
|
206
205
|
);
|
|
207
206
|
if (taskData) {
|
|
208
207
|
const nextRun = getNextRunTime(taskData.cron_expression, taskData.timezone);
|
package/src/agent/session.ts
CHANGED
|
@@ -52,23 +52,13 @@ export class SessionManager {
|
|
|
52
52
|
this.onTask = onTask;
|
|
53
53
|
this.userId = userId;
|
|
54
54
|
|
|
55
|
-
this.session = await createSession(
|
|
56
|
-
userId,
|
|
57
|
-
config.sessionName,
|
|
58
|
-
config.workspacePath,
|
|
59
|
-
"0.1.0"
|
|
60
|
-
);
|
|
55
|
+
this.session = await createSession(config.sessionName, config.workspacePath, "0.1.0");
|
|
61
56
|
|
|
62
57
|
// Get or create CLI conversation for this session
|
|
63
|
-
this.conversationId = await getOrCreateCliConversation(
|
|
64
|
-
userId,
|
|
65
|
-
this.session.id
|
|
66
|
-
);
|
|
58
|
+
this.conversationId = await getOrCreateCliConversation();
|
|
67
59
|
|
|
68
60
|
this.running = true;
|
|
69
|
-
log.success(
|
|
70
|
-
`Session started: ${this.session.id} (${config.sessionName})`
|
|
71
|
-
);
|
|
61
|
+
log.success(`Session started: ${this.session.id} (${config.sessionName})`);
|
|
72
62
|
log.info(`Workspace: ${config.workspacePath}`);
|
|
73
63
|
|
|
74
64
|
// Start heartbeat
|
|
@@ -107,44 +97,43 @@ export class SessionManager {
|
|
|
107
97
|
this.pollTimer = setTimeout(() => this.pollForTasks(), delay);
|
|
108
98
|
}
|
|
109
99
|
|
|
110
|
-
private async executeScheduledTask(
|
|
111
|
-
scheduledTask: ScheduledTask
|
|
112
|
-
): Promise<void> {
|
|
100
|
+
private async executeScheduledTask(scheduledTask: ScheduledTask): Promise<void> {
|
|
113
101
|
if (!this.session || !this.userId || !this.conversationId) return;
|
|
114
102
|
|
|
115
103
|
log.info(`Running scheduled task: "${scheduledTask.name}"`);
|
|
116
104
|
|
|
117
105
|
try {
|
|
118
|
-
await this.submitTask(
|
|
119
|
-
`[Scheduled: ${scheduledTask.name}] ${scheduledTask.prompt}`
|
|
120
|
-
);
|
|
106
|
+
await this.submitTask(`[Scheduled: ${scheduledTask.name}] ${scheduledTask.prompt}`);
|
|
121
107
|
} catch (err) {
|
|
122
108
|
log.error(`Scheduled task error: ${err}`);
|
|
123
109
|
}
|
|
124
110
|
}
|
|
125
111
|
|
|
126
112
|
private async executeJobRun(jobRun: PendingJobRun): Promise<void> {
|
|
127
|
-
if (!this.session || !this.userId || !this.conversationId || !this.onTask)
|
|
128
|
-
return;
|
|
113
|
+
if (!this.session || !this.userId || !this.conversationId || !this.onTask) return;
|
|
129
114
|
|
|
130
|
-
log.info(
|
|
131
|
-
`Executing job run: ${jobRun.job_name} (${jobRun.id.slice(0, 8)}...)`
|
|
132
|
-
);
|
|
115
|
+
log.info(`Executing job run: ${jobRun.job_name} (${jobRun.id.slice(0, 8)}...)`);
|
|
133
116
|
|
|
134
|
-
const runner = new JobRunner(
|
|
117
|
+
const runner = new JobRunner();
|
|
135
118
|
const job = await runner.loadJob(jobRun.job_name);
|
|
136
119
|
|
|
137
120
|
if (!job) {
|
|
138
121
|
log.error(`Job "${jobRun.job_name}" not found, marking run as failed`);
|
|
139
|
-
try {
|
|
140
|
-
|
|
122
|
+
try {
|
|
123
|
+
await runner.completeRun(jobRun.id, "failed", `Job "${jobRun.job_name}" not found`);
|
|
124
|
+
} catch {
|
|
125
|
+
/* already logged inside completeRun */
|
|
126
|
+
}
|
|
141
127
|
return;
|
|
142
128
|
}
|
|
143
129
|
|
|
144
130
|
if (job.skills.length === 0) {
|
|
145
131
|
log.error(`Job "${jobRun.job_name}" has no skills, marking run as failed`);
|
|
146
|
-
try {
|
|
147
|
-
|
|
132
|
+
try {
|
|
133
|
+
await runner.completeRun(jobRun.id, "failed", "Job has no linked skills");
|
|
134
|
+
} catch {
|
|
135
|
+
/* already logged inside completeRun */
|
|
136
|
+
}
|
|
148
137
|
return;
|
|
149
138
|
}
|
|
150
139
|
|
|
@@ -214,7 +203,7 @@ export class SessionManager {
|
|
|
214
203
|
}
|
|
215
204
|
} else if (this.userId) {
|
|
216
205
|
// Priority 2: Pending job runs (triggered from web UI)
|
|
217
|
-
const jobRun = await pollAndClaimJobRun(
|
|
206
|
+
const jobRun = await pollAndClaimJobRun();
|
|
218
207
|
|
|
219
208
|
if (jobRun) {
|
|
220
209
|
this.processingDepth++;
|
|
@@ -267,12 +256,7 @@ export class SessionManager {
|
|
|
267
256
|
await setSessionBusy(this.session.id, true);
|
|
268
257
|
|
|
269
258
|
try {
|
|
270
|
-
const task = await createTask(
|
|
271
|
-
this.conversationId,
|
|
272
|
-
this.userId,
|
|
273
|
-
this.session.id,
|
|
274
|
-
prompt
|
|
275
|
-
);
|
|
259
|
+
const task = await createTask(this.conversationId, this.session.id, prompt);
|
|
276
260
|
|
|
277
261
|
// Claim immediately so poll can't grab it even after depth decrements
|
|
278
262
|
await claimTask(task.id);
|
package/src/agent/skills.ts
CHANGED
|
@@ -6,18 +6,113 @@ import { callMcpHandler } from "../db/api-client.js";
|
|
|
6
6
|
|
|
7
7
|
// Common English stop words to filter out of matching
|
|
8
8
|
const STOP_WORDS = new Set([
|
|
9
|
-
"the",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
9
|
+
"the",
|
|
10
|
+
"a",
|
|
11
|
+
"an",
|
|
12
|
+
"is",
|
|
13
|
+
"are",
|
|
14
|
+
"was",
|
|
15
|
+
"were",
|
|
16
|
+
"be",
|
|
17
|
+
"been",
|
|
18
|
+
"being",
|
|
19
|
+
"have",
|
|
20
|
+
"has",
|
|
21
|
+
"had",
|
|
22
|
+
"do",
|
|
23
|
+
"does",
|
|
24
|
+
"did",
|
|
25
|
+
"will",
|
|
26
|
+
"would",
|
|
27
|
+
"could",
|
|
28
|
+
"should",
|
|
29
|
+
"may",
|
|
30
|
+
"might",
|
|
31
|
+
"shall",
|
|
32
|
+
"can",
|
|
33
|
+
"need",
|
|
34
|
+
"dare",
|
|
35
|
+
"ought",
|
|
36
|
+
"used",
|
|
37
|
+
"to",
|
|
38
|
+
"of",
|
|
39
|
+
"in",
|
|
40
|
+
"for",
|
|
41
|
+
"on",
|
|
42
|
+
"with",
|
|
43
|
+
"at",
|
|
44
|
+
"by",
|
|
45
|
+
"from",
|
|
46
|
+
"as",
|
|
47
|
+
"into",
|
|
48
|
+
"through",
|
|
49
|
+
"during",
|
|
50
|
+
"before",
|
|
51
|
+
"after",
|
|
52
|
+
"above",
|
|
53
|
+
"below",
|
|
54
|
+
"between",
|
|
55
|
+
"out",
|
|
56
|
+
"off",
|
|
57
|
+
"over",
|
|
58
|
+
"under",
|
|
59
|
+
"again",
|
|
60
|
+
"further",
|
|
61
|
+
"then",
|
|
62
|
+
"once",
|
|
63
|
+
"here",
|
|
64
|
+
"there",
|
|
65
|
+
"when",
|
|
66
|
+
"where",
|
|
67
|
+
"why",
|
|
68
|
+
"how",
|
|
69
|
+
"all",
|
|
70
|
+
"each",
|
|
71
|
+
"every",
|
|
72
|
+
"both",
|
|
73
|
+
"few",
|
|
74
|
+
"more",
|
|
75
|
+
"most",
|
|
76
|
+
"other",
|
|
77
|
+
"some",
|
|
78
|
+
"such",
|
|
79
|
+
"no",
|
|
80
|
+
"nor",
|
|
81
|
+
"not",
|
|
82
|
+
"only",
|
|
83
|
+
"own",
|
|
84
|
+
"same",
|
|
85
|
+
"so",
|
|
86
|
+
"than",
|
|
87
|
+
"too",
|
|
88
|
+
"very",
|
|
89
|
+
"and",
|
|
90
|
+
"but",
|
|
91
|
+
"or",
|
|
92
|
+
"if",
|
|
93
|
+
"this",
|
|
94
|
+
"that",
|
|
95
|
+
"these",
|
|
96
|
+
"those",
|
|
97
|
+
"it",
|
|
98
|
+
"its",
|
|
99
|
+
"i",
|
|
100
|
+
"me",
|
|
101
|
+
"my",
|
|
102
|
+
"we",
|
|
103
|
+
"our",
|
|
104
|
+
"you",
|
|
105
|
+
"your",
|
|
106
|
+
"he",
|
|
107
|
+
"him",
|
|
108
|
+
"she",
|
|
109
|
+
"her",
|
|
110
|
+
"they",
|
|
111
|
+
"them",
|
|
112
|
+
"what",
|
|
113
|
+
"which",
|
|
114
|
+
"who",
|
|
115
|
+
"whom",
|
|
21
116
|
]);
|
|
22
117
|
|
|
23
118
|
/**
|
|
@@ -48,6 +143,13 @@ function bigrams(tokens: string[]): Set<string> {
|
|
|
48
143
|
|
|
49
144
|
// ── Skill Interfaces ────────────────────────────────────────────────
|
|
50
145
|
|
|
146
|
+
export interface SkillCredentialRequirement {
|
|
147
|
+
name: string;
|
|
148
|
+
type: "api_key" | "oauth_token" | "login" | "secret" | "custom";
|
|
149
|
+
description: string;
|
|
150
|
+
required: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
51
153
|
export interface SkillMetadata {
|
|
52
154
|
requires?: {
|
|
53
155
|
bins?: string[];
|
|
@@ -60,6 +162,7 @@ export interface SkillMetadata {
|
|
|
60
162
|
os?: string[];
|
|
61
163
|
always?: boolean;
|
|
62
164
|
skillKey?: string;
|
|
165
|
+
credentials?: SkillCredentialRequirement[];
|
|
63
166
|
}
|
|
64
167
|
|
|
65
168
|
export interface Skill {
|
|
@@ -92,6 +195,7 @@ function parseDbMetadata(raw: unknown): SkillMetadata {
|
|
|
92
195
|
os: openclaw.os as string[] | undefined,
|
|
93
196
|
always: openclaw.always as boolean | undefined,
|
|
94
197
|
skillKey: openclaw.skillKey as string | undefined,
|
|
198
|
+
credentials: openclaw.credentials as SkillCredentialRequirement[] | undefined,
|
|
95
199
|
};
|
|
96
200
|
}
|
|
97
201
|
|
|
@@ -165,7 +269,8 @@ export class SkillManager {
|
|
|
165
269
|
const docFreq = new Map<string, number>();
|
|
166
270
|
const totalSkills = this.skills.size || 1;
|
|
167
271
|
for (const skill of this.skills.values()) {
|
|
168
|
-
const allText =
|
|
272
|
+
const allText =
|
|
273
|
+
`${skill.name} ${skill.description} ${skill.content} ${skill.keywords.join(" ")}`.toLowerCase();
|
|
169
274
|
const words = new Set(tokenize(allText));
|
|
170
275
|
for (const w of words) {
|
|
171
276
|
docFreq.set(w, (docFreq.get(w) || 0) + 1);
|
|
@@ -266,8 +371,10 @@ export class SkillManager {
|
|
|
266
371
|
|
|
267
372
|
let budget = this.DESCRIPTION_BUDGET_CHARS;
|
|
268
373
|
let prompt = "\n\n## Your Skills\n";
|
|
269
|
-
prompt +=
|
|
270
|
-
|
|
374
|
+
prompt +=
|
|
375
|
+
"These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n";
|
|
376
|
+
prompt +=
|
|
377
|
+
"If no skill matches but the task is a reusable pattern, consider creating one with skill_create.\n\n";
|
|
271
378
|
let included = 0;
|
|
272
379
|
|
|
273
380
|
for (const skill of skills) {
|
|
@@ -287,24 +394,6 @@ export class SkillManager {
|
|
|
287
394
|
return prompt;
|
|
288
395
|
}
|
|
289
396
|
|
|
290
|
-
/** @deprecated Use buildSkillDescriptions() + skill_invoke tool instead. */
|
|
291
|
-
buildSkillPrompt(taskPrompt: string): string {
|
|
292
|
-
const relevant = this.findRelevant(taskPrompt);
|
|
293
|
-
if (relevant.length === 0) return "";
|
|
294
|
-
|
|
295
|
-
let prompt = "\n\n## Available Skills\n";
|
|
296
|
-
prompt += "The following skills provide detailed instructions for this type of task:\n\n";
|
|
297
|
-
|
|
298
|
-
for (const skill of relevant) {
|
|
299
|
-
const emoji = skill.metadata.emoji || "";
|
|
300
|
-
prompt += `### ${emoji ? emoji + " " : ""}${skill.name}\n`;
|
|
301
|
-
prompt += `*${skill.description}*\n\n`;
|
|
302
|
-
prompt += skill.content + "\n\n";
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return prompt;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
397
|
async create(
|
|
309
398
|
name: string,
|
|
310
399
|
description: string,
|
|
@@ -327,7 +416,7 @@ export class SkillManager {
|
|
|
327
416
|
emoji: options?.emoji || null,
|
|
328
417
|
keywords: options?.keywords || [],
|
|
329
418
|
metadata,
|
|
330
|
-
}
|
|
419
|
+
}
|
|
331
420
|
);
|
|
332
421
|
|
|
333
422
|
const row = (Array.isArray(data) ? data[0] : data) as Record<string, unknown> | null;
|
|
@@ -373,13 +462,13 @@ export class SkillManager {
|
|
|
373
462
|
try {
|
|
374
463
|
const result = await callMcpHandler<{ skill: Record<string, unknown>; agent_skill: unknown }>(
|
|
375
464
|
"skill.fetch_and_add",
|
|
376
|
-
{ skill_id: skillId }
|
|
465
|
+
{ skill_id: skillId }
|
|
377
466
|
);
|
|
378
467
|
|
|
379
468
|
const row = result.skill;
|
|
380
|
-
const agentSkillRow = (
|
|
381
|
-
? result.agent_skill
|
|
382
|
-
|
|
469
|
+
const agentSkillRow = (
|
|
470
|
+
result.agent_skill && typeof result.agent_skill === "object" ? result.agent_skill : row
|
|
471
|
+
) as Record<string, unknown>;
|
|
383
472
|
const skill = this.rowToSkill({
|
|
384
473
|
...agentSkillRow,
|
|
385
474
|
name: row.name,
|
|
@@ -425,7 +514,10 @@ export class SkillManager {
|
|
|
425
514
|
const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
426
515
|
for (const [existingName, skill] of this.skills) {
|
|
427
516
|
const normalizedExisting = existingName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
428
|
-
if (
|
|
517
|
+
if (
|
|
518
|
+
normalizedName.includes(normalizedExisting) ||
|
|
519
|
+
normalizedExisting.includes(normalizedName)
|
|
520
|
+
) {
|
|
429
521
|
return skill;
|
|
430
522
|
}
|
|
431
523
|
const nameWords = new Set(name.split("-"));
|
|
@@ -546,16 +638,24 @@ export class SkillManager {
|
|
|
546
638
|
}
|
|
547
639
|
}
|
|
548
640
|
|
|
549
|
-
async searchDb(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
641
|
+
async searchDb(
|
|
642
|
+
query: string,
|
|
643
|
+
limit = 10
|
|
644
|
+
): Promise<
|
|
645
|
+
Array<{
|
|
646
|
+
name: string;
|
|
647
|
+
description: string;
|
|
648
|
+
emoji: string;
|
|
649
|
+
source: string;
|
|
650
|
+
invocationCount: number;
|
|
651
|
+
}>
|
|
652
|
+
> {
|
|
556
653
|
if (this.userId) {
|
|
557
654
|
try {
|
|
558
|
-
const data = await callMcpHandler<Record<string, unknown>[]>("skill.search", {
|
|
655
|
+
const data = await callMcpHandler<Record<string, unknown>[]>("skill.search", {
|
|
656
|
+
query,
|
|
657
|
+
limit,
|
|
658
|
+
});
|
|
559
659
|
if (data) {
|
|
560
660
|
return data.map((row) => ({
|
|
561
661
|
name: row.name as string,
|
|
@@ -565,7 +665,9 @@ export class SkillManager {
|
|
|
565
665
|
invocationCount: (row.invocation_count as number) || 0,
|
|
566
666
|
}));
|
|
567
667
|
}
|
|
568
|
-
} catch {
|
|
668
|
+
} catch {
|
|
669
|
+
/* Fall through to in-memory search */
|
|
670
|
+
}
|
|
569
671
|
}
|
|
570
672
|
|
|
571
673
|
const results = this.findRelevant(query, limit);
|
|
@@ -582,7 +684,9 @@ export class SkillManager {
|
|
|
582
684
|
if (!this.userId) return;
|
|
583
685
|
try {
|
|
584
686
|
await callMcpHandler("skill.remove", { name });
|
|
585
|
-
} catch {
|
|
687
|
+
} catch {
|
|
688
|
+
/* Non-critical */
|
|
689
|
+
}
|
|
586
690
|
}
|
|
587
691
|
|
|
588
692
|
// ── Marketplace ────────────────────────────────────────────────────
|
|
@@ -630,18 +734,20 @@ export class SkillManager {
|
|
|
630
734
|
sort?: "popular" | "recent" | "rating";
|
|
631
735
|
limit?: number;
|
|
632
736
|
offset?: number;
|
|
633
|
-
}): Promise<
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
737
|
+
}): Promise<
|
|
738
|
+
Array<{
|
|
739
|
+
id: string;
|
|
740
|
+
name: string;
|
|
741
|
+
description: string;
|
|
742
|
+
emoji: string;
|
|
743
|
+
version: string;
|
|
744
|
+
authorName: string;
|
|
745
|
+
category: string;
|
|
746
|
+
installCount: number;
|
|
747
|
+
avgRating: number | null;
|
|
748
|
+
ratingCount: number;
|
|
749
|
+
}>
|
|
750
|
+
> {
|
|
645
751
|
try {
|
|
646
752
|
const data = await callMcpHandler<Record<string, unknown>[]>("skill.browse", {
|
|
647
753
|
query: options?.query || null,
|