assistme 0.3.1 → 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.
Files changed (44) hide show
  1. package/PLAN.md +14 -3
  2. package/dist/{chunk-UWE5WVQI.js → chunk-KX7ITO55.js} +20 -11
  3. package/dist/index.js +1771 -496
  4. package/dist/{job-runner-N4XAAWLJ.js → job-runner-P2L6MOOX.js} +1 -1
  5. package/package.json +5 -3
  6. package/src/agent/job-runner.ts +9 -13
  7. package/src/agent/mcp-servers.ts +6 -952
  8. package/src/agent/memory.ts +2 -11
  9. package/src/agent/processor.ts +17 -107
  10. package/src/agent/scheduler.ts +2 -3
  11. package/src/agent/session.ts +20 -36
  12. package/src/agent/skills.ts +167 -61
  13. package/src/agent/system-prompt.ts +126 -0
  14. package/src/browser/chrome-launcher.ts +555 -0
  15. package/src/browser/controller.ts +1386 -0
  16. package/src/browser/types.ts +70 -0
  17. package/src/commands/credential.ts +190 -0
  18. package/src/commands/job.ts +14 -45
  19. package/src/commands/memory.ts +16 -29
  20. package/src/commands/schedule.ts +15 -37
  21. package/src/commands/start.ts +11 -43
  22. package/src/credentials/credential-store.test.ts +162 -0
  23. package/src/credentials/credential-store.ts +266 -0
  24. package/src/credentials/encryption.test.ts +98 -0
  25. package/src/credentials/encryption.ts +82 -0
  26. package/src/credentials/index.ts +15 -0
  27. package/src/credentials/local-store.ts +89 -0
  28. package/src/db/action.ts +19 -0
  29. package/src/db/api-client.ts +3 -32
  30. package/src/db/auth-store.ts +41 -0
  31. package/src/db/auth.ts +38 -0
  32. package/src/db/conversation.ts +39 -0
  33. package/src/db/event.ts +52 -0
  34. package/src/db/job-poll.ts +18 -0
  35. package/src/db/session.ts +60 -0
  36. package/src/db/supabase.ts +40 -383
  37. package/src/db/task.ts +69 -0
  38. package/src/db/types.ts +54 -0
  39. package/src/index.ts +2 -0
  40. package/src/mcp/agent-tools-server.ts +1047 -0
  41. package/src/mcp/browser-server.ts +258 -0
  42. package/src/tools/browser.ts +28 -1208
  43. package/src/tools/index.ts +32 -263
  44. package/src/tools/web.ts +0 -73
@@ -1,953 +1,7 @@
1
- import {
2
- createSdkMcpServer,
3
- tool,
4
- type McpSdkServerConfigWithInstance,
5
- } from "@anthropic-ai/claude-agent-sdk";
6
- import { z } from "zod/v4";
7
- import { executeTool } from "../tools/index.js";
8
- import { getLimiterForTool } from "../utils/rate-limiter.js";
9
- import { log } from "../utils/logger.js";
10
- import type { MemoryManager, MemoryCategory } from "./memory.js";
11
- import type { SkillManager } from "./skills.js";
12
- import { substituteArguments, preprocessDynamicContext, validateSkillName } from "./skills.js";
13
- import { emitEvent, setActionRequest, pollActionResponse } from "../db/supabase.js";
14
- import { callMcpHandler } from "../db/api-client.js";
15
- import { JobRunner } from "./job-runner.js";
16
- import {
17
- createScheduledTask,
18
- getNextRunTime,
19
- } from "./scheduler.js";
1
+ /**
2
+ * Re-export barrel — preserves backward compatibility while the actual logic
3
+ * lives in focused modules under mcp/.
4
+ */
20
5
 
21
- // ── Helper ──────────────────────────────────────────────────────────
22
-
23
- /** Wrap executeTool with rate limiting and text result. */
24
- async function callTool(
25
- name: string,
26
- input: Record<string, unknown>
27
- ): Promise<{ content: Array<{ type: "text"; text: string }> }> {
28
- const limiter = getLimiterForTool(name);
29
- if (limiter) await limiter.acquire();
30
- const result = await executeTool(name, input);
31
- return { content: [{ type: "text", text: result }] };
32
- }
33
-
34
- // ── Browser MCP Server ──────────────────────────────────────────────
35
-
36
- export const BROWSER_TOOL_NAMES = [
37
- "browser_connect",
38
- "browser_navigate",
39
- "browser_read_page",
40
- "browser_screenshot",
41
- "browser_click",
42
- "browser_type",
43
- "browser_press_key",
44
- "browser_scroll",
45
- "browser_get_elements",
46
- "browser_evaluate",
47
- "browser_list_tabs",
48
- "browser_switch_tab",
49
- "browser_new_tab",
50
- "browser_request_user_action",
51
- ] as const;
52
-
53
- export function createBrowserMcpServer(): McpSdkServerConfigWithInstance {
54
- return createSdkMcpServer({
55
- name: "assistme-browser",
56
- version: "1.0.0",
57
- tools: [
58
- tool(
59
- "browser_connect",
60
- "Connect to the user's real Chrome browser via CDP. Chrome will be auto-launched if not already running.",
61
- { tab_index: z.number().optional().describe("Tab index (default: 0)") },
62
- async (args) => callTool("browser_connect", args)
63
- ),
64
- tool(
65
- "browser_navigate",
66
- "Navigate the user's browser to a URL, using the user's real browser with all their cookies and logins.",
67
- { url: z.string().describe("URL to navigate to") },
68
- async (args) => callTool("browser_navigate", args)
69
- ),
70
- tool(
71
- "browser_read_page",
72
- "Read the text content of the currently open page. Returns page title, URL, and main text content.",
73
- {},
74
- async () => callTool("browser_read_page", {})
75
- ),
76
- tool(
77
- "browser_screenshot",
78
- "Take a screenshot of the current browser page. Returns a base64-encoded PNG image.",
79
- {},
80
- async () => {
81
- const limiter = getLimiterForTool("browser_screenshot");
82
- if (limiter) await limiter.acquire();
83
- const base64 = await executeTool("browser_screenshot", {});
84
- if (base64.length > 100) {
85
- return {
86
- content: [
87
- {
88
- type: "image" as const,
89
- data: base64,
90
- mimeType: "image/png",
91
- } as unknown as { type: "text"; text: string },
92
- ],
93
- };
94
- }
95
- return { content: [{ type: "text", text: base64 }] };
96
- }
97
- ),
98
- tool(
99
- "browser_click",
100
- "Click on an element in the user's browser using a CSS selector.",
101
- { selector: z.string().describe("CSS selector of the element to click") },
102
- async (args) => callTool("browser_click", args)
103
- ),
104
- tool(
105
- "browser_type",
106
- "Type text into an input field in the user's browser.",
107
- {
108
- selector: z.string().describe("CSS selector of the input element"),
109
- text: z.string().describe("Text to type"),
110
- },
111
- async (args) => callTool("browser_type", args)
112
- ),
113
- tool(
114
- "browser_press_key",
115
- "Press a keyboard key in the browser. Supports: Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp.",
116
- { key: z.string().describe("Key to press") },
117
- async (args) => callTool("browser_press_key", args)
118
- ),
119
- tool(
120
- "browser_scroll",
121
- "Scroll the page up or down.",
122
- { direction: z.string().describe("'down' or 'up'") },
123
- async (args) => callTool("browser_scroll", args)
124
- ),
125
- tool(
126
- "browser_get_elements",
127
- "Find all interactive elements (links, buttons, inputs) on the current page.",
128
- {},
129
- async () => callTool("browser_get_elements", {})
130
- ),
131
- tool(
132
- "browser_evaluate",
133
- "Execute JavaScript in the browser page context.",
134
- { expression: z.string().describe("JavaScript expression to evaluate") },
135
- async (args) => callTool("browser_evaluate", args)
136
- ),
137
- tool("browser_list_tabs", "List all open tabs in the user's browser.", {}, async () =>
138
- callTool("browser_list_tabs", {})
139
- ),
140
- tool(
141
- "browser_switch_tab",
142
- "Switch to a different browser tab by index.",
143
- { index: z.number().describe("Tab index") },
144
- async (args) => callTool("browser_switch_tab", args)
145
- ),
146
- tool(
147
- "browser_new_tab",
148
- "Open a new tab in the user's browser, optionally navigating to a URL.",
149
- { url: z.string().optional().describe("URL to open (default: blank)") },
150
- async (args) => callTool("browser_new_tab", args)
151
- ),
152
- tool(
153
- "browser_request_user_action",
154
- "Request the user to perform an action in their browser (login, CAPTCHA, 2FA, etc.).",
155
- {
156
- message: z.string().describe("Clear description of what the user needs to do"),
157
- wait_seconds: z.number().optional().describe("How long to wait (default: 60)"),
158
- },
159
- async (args) => callTool("browser_request_user_action", args)
160
- ),
161
- ],
162
- });
163
- }
164
-
165
- // ── Agent Tools MCP Server (memory, skills) ─────────────────────────
166
-
167
- export interface AgentToolsDeps {
168
- memoryManager: MemoryManager | null;
169
- skillManager: SkillManager;
170
- taskId: string;
171
- sessionId?: string;
172
- userId?: string;
173
- }
174
-
175
- export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfigWithInstance {
176
- const { memoryManager, skillManager, taskId, sessionId, userId } = deps;
177
-
178
- return createSdkMcpServer({
179
- name: "assistme-agent",
180
- version: "1.0.0",
181
- tools: [
182
- tool(
183
- "memory_store",
184
- "Store a memory about the user that persists across conversations. Use when you learn preferences, habits, or standing instructions.",
185
- {
186
- content: z.string().describe("What to remember (concise, factual statement)"),
187
- category: z
188
- .string()
189
- .optional()
190
- .describe("Category: general, preference, instruction, context, skill_learned, fact"),
191
- importance: z
192
- .number()
193
- .optional()
194
- .describe("Importance 1-10 (default: 5). Use 8+ for instructions"),
195
- tags: z.array(z.string()).optional().describe("Optional tags for searchability"),
196
- },
197
- async (args) => {
198
- if (!memoryManager) {
199
- return {
200
- content: [{ type: "text", text: "Memory manager not available." }],
201
- };
202
- }
203
- const mem = await memoryManager.remember(
204
- args.content,
205
- (args.category as MemoryCategory) || "general",
206
- {
207
- importance: args.importance || 5,
208
- tags: args.tags || [],
209
- sourceMessageId: taskId,
210
- }
211
- );
212
- const result = `Memory stored: "${mem.content}" [${mem.category}, importance: ${mem.importance}]`;
213
- return { content: [{ type: "text", text: result }] };
214
- }
215
- ),
216
- tool(
217
- "skill_create",
218
- "Create a new skill and add it to the user's collection. Returns the skill ID on success.",
219
- {
220
- name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
221
- description: z.string().describe("One-line description of what this skill does"),
222
- instructions: z.string().describe("Markdown step-by-step instructions"),
223
- emoji: z.string().optional().describe("Single emoji representing this skill"),
224
- },
225
- async (args) => {
226
- // Validate skill name format
227
- const nameError = validateSkillName(args.name);
228
- if (nameError) {
229
- return {
230
- content: [{
231
- type: "text",
232
- text: `Invalid skill name: ${nameError}. Use lowercase kebab-case like "flight-booking".`,
233
- }],
234
- };
235
- }
236
-
237
- // Check for duplicates in user's collection
238
- const existing = skillManager.findSimilar(args.name);
239
- if (existing) {
240
- return {
241
- content: [
242
- {
243
- type: "text",
244
- text: `A similar skill "${existing.name}" already exists in your collection. Use skill_improve to update it instead.`,
245
- },
246
- ],
247
- };
248
- }
249
-
250
- // Create in skills table (repository)
251
- const result = await skillManager.create(
252
- args.name,
253
- args.description,
254
- args.instructions,
255
- { source: "manual", emoji: args.emoji }
256
- );
257
-
258
- if (!result) {
259
- return {
260
- content: [{ type: "text", text: `Failed to create skill "${args.name}".` }],
261
- };
262
- }
263
-
264
- // Auto-add to user's collection via upsert_agent_skill RPC
265
- await skillManager.syncToAgentSkills(
266
- args.name,
267
- args.description,
268
- args.instructions,
269
- "1.0.0",
270
- {
271
- source: "manual",
272
- emoji: args.emoji,
273
- sourceSkillId: result.id,
274
- }
275
- );
276
-
277
- log.success(`Skill "${args.name}" created and added to collection`);
278
- return {
279
- content: [
280
- {
281
- type: "text",
282
- text: `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`,
283
- },
284
- ],
285
- };
286
- }
287
- ),
288
- tool(
289
- "skill_improve",
290
- "Improve an existing skill with better instructions based on what you just learned. Version auto-bumped.",
291
- {
292
- name: z.string().describe("Name of the existing skill to improve"),
293
- improved_instructions: z
294
- .string()
295
- .describe("Full updated markdown instructions (not a diff)"),
296
- description: z.string().optional().describe("Updated description (optional)"),
297
- },
298
- async (args) => {
299
- const existing = skillManager.get(args.name);
300
- if (!existing) {
301
- const available = skillManager
302
- .getAll()
303
- .map((s) => s.name)
304
- .join(", ");
305
- return {
306
- content: [
307
- {
308
- type: "text",
309
- text: `Skill "${args.name}" not found. Available skills: ${available}`,
310
- },
311
- ],
312
- };
313
- }
314
-
315
- const updated = skillManager.update(
316
- args.name,
317
- args.improved_instructions,
318
- args.description
319
- );
320
- if (updated) {
321
- log.success(`Self-improvement: improved skill "${args.name}"`);
322
- return {
323
- content: [
324
- {
325
- type: "text",
326
- text: `Skill "${args.name}" improved and version bumped.`,
327
- },
328
- ],
329
- };
330
- }
331
-
332
- return {
333
- content: [
334
- {
335
- type: "text",
336
- text: `Failed to update skill "${args.name}".`,
337
- },
338
- ],
339
- };
340
- }
341
- ),
342
- tool(
343
- "skill_invoke",
344
- "Load a skill's full instructions when relevant to the current task. " +
345
- "Call this when you determine a skill from the Available Skills list matches the user's request.",
346
- {
347
- name: z.string().describe("Skill name from the Available Skills list"),
348
- arguments: z.string().optional().describe("Arguments to pass to the skill (replaces $ARGUMENTS placeholders)"),
349
- },
350
- async (args) => {
351
- const skill = skillManager.get(args.name);
352
- if (!skill) {
353
- const available = skillManager
354
- .getAll()
355
- .map((s) => s.name)
356
- .join(", ");
357
- return {
358
- content: [
359
- {
360
- type: "text",
361
- text: `Skill "${args.name}" not found. Available skills: ${available}`,
362
- },
363
- ],
364
- };
365
- }
366
-
367
- let content = skill.content;
368
-
369
- // Substitute $ARGUMENTS placeholders
370
- if (args.arguments) {
371
- content = substituteArguments(content, args.arguments);
372
- }
373
-
374
- // Preprocess dynamic context (!`command` syntax)
375
- content = preprocessDynamicContext(content);
376
-
377
- // Build response with skill content
378
- let response = `## Skill: ${skill.name}\n`;
379
- if (skill.description) {
380
- response += `*${skill.description}*\n`;
381
- }
382
- response += `\n${content}`;
383
-
384
- // Note allowed tools restriction if specified
385
- if (skill.allowedTools.length > 0) {
386
- response += `\n\n**Allowed tools for this skill:** ${skill.allowedTools.join(", ")}\n`;
387
- }
388
-
389
- log.info(`Skill invoked: "${args.name}"`);
390
-
391
- // Log invocation to DB (fire-and-forget)
392
- skillManager.logInvocation(args.name, {
393
- messageId: taskId,
394
- sessionId,
395
- arguments: args.arguments,
396
- }).catch(() => {});
397
-
398
- return {
399
- content: [{ type: "text", text: response }],
400
- };
401
- }
402
- ),
403
- tool(
404
- "skill_search",
405
- "Search for skills by keyword. Uses full-text search across skill names, descriptions, and content. " +
406
- "Use this to discover relevant skills when the Available Skills list doesn't have an obvious match.",
407
- {
408
- query: z.string().describe("Search query (keywords, topic, or task description)"),
409
- limit: z.number().optional().describe("Max results (default: 5)"),
410
- },
411
- async (args) => {
412
- const results = await skillManager.searchDb(args.query, args.limit || 5);
413
-
414
- if (results.length === 0) {
415
- return {
416
- content: [{ type: "text", text: `No skills found for "${args.query}".` }],
417
- };
418
- }
419
-
420
- let response = `## Skills matching "${args.query}"\n\n`;
421
- for (const r of results) {
422
- const emoji = r.emoji ? `${r.emoji} ` : "";
423
- const usage = r.invocationCount > 0 ? ` (used ${r.invocationCount}x)` : "";
424
- response += `- **${emoji}${r.name}**${usage}: ${r.description}\n`;
425
- }
426
- response += "\nUse skill_invoke to load any of these skills.";
427
-
428
- return { content: [{ type: "text", text: response }] };
429
- }
430
- ),
431
- tool(
432
- "skill_generate",
433
- "Prepare context for generating skills from a job description. Returns existing skills and job info " +
434
- "so you can analyze the job and create skills using skill_create (which auto-adds to user's collection). " +
435
- "After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
436
- {
437
- job_name: z.string().describe(
438
- "Short name for this job/role. Example: '电商运营', 'Frontend Dev', 'Data Analyst'"
439
- ),
440
- job_description: z.string().describe(
441
- "Description of the user's job, role, and daily tasks. Can be in any language. " +
442
- "Example: '我是电商运营,每天要看竞品价格、写商品文案、回复客户评论'"
443
- ),
444
- },
445
- async (args) => {
446
- const existingNames = skillManager.getAll().map((s) => s.name);
447
-
448
- let response = `## Job: ${args.job_name}\n`;
449
- response += `**Description:** ${args.job_description}\n\n`;
450
-
451
- if (existingNames.length > 0) {
452
- response += `**Existing skills (do NOT duplicate):** ${existingNames.join(", ")}\n\n`;
453
- }
454
-
455
- response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.\n\n`;
456
- response += `**IMPORTANT — You MUST use ask_user before creating skills:**\n`;
457
- response += `1. Analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).\n`;
458
- response += `2. Call \`ask_user\` with the formatted skill list as "question" and these options:\n`;
459
- response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]\n`;
460
- response += `3. WAIT for the response. If action_key is "approve_all", create all skills using \`skill_create\`. If "cancel", stop.\n`;
461
- response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling ask_user first.\n\n`;
462
- response += `For each skill, call \`skill_create\` with:\n`;
463
- response += `- name: kebab-case name (e.g. "slack-message-check")\n`;
464
- response += `- description: one-line description\n`;
465
- response += `- instructions: detailed step-by-step markdown instructions the agent can follow\n`;
466
- response += `- emoji: a single emoji representing the skill\n\n`;
467
- response += `skill_create automatically adds the skill to the user's collection — no need to call skill_add.\n\n`;
468
- response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}" and the list of created skill names to link them and mark the job as analyzed.\n\n`;
469
- response += `**Guidelines for skill instructions:**\n`;
470
- response += `- Write clear, actionable markdown steps\n`;
471
- response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks\n`;
472
- response += `- Include error handling steps\n`;
473
- response += `- Use placeholders like {query}, {date} for variable inputs\n`;
474
- response += `- Each skill should be a single, well-defined workflow (10-25 steps)\n`;
475
-
476
- return { content: [{ type: "text", text: response }] };
477
- }
478
- ),
479
- tool(
480
- "skill_link_job",
481
- "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.",
482
- {
483
- job_name: z.string().describe("Name of the job to link skills to"),
484
- job_description: z.string().describe("Job description (used if job doesn't exist yet)"),
485
- skill_names: z.array(z.string()).describe("Names of skills to link to this job"),
486
- },
487
- async (args) => {
488
- if (!userId) {
489
- return {
490
- content: [{ type: "text", text: "Not authenticated. Cannot link job." }],
491
- };
492
- }
493
-
494
- try {
495
- await saveJobToDb(userId, args.job_name, args.job_description, args.skill_names);
496
- log.success(`Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`);
497
- return {
498
- content: [{
499
- type: "text",
500
- text: `Job "${args.job_name}" linked with ${args.skill_names.length} skills and marked as analyzed.`,
501
- }],
502
- };
503
- } catch (err) {
504
- return {
505
- content: [{
506
- type: "text",
507
- text: `Failed to link job: ${err instanceof Error ? err.message : err}`,
508
- }],
509
- };
510
- }
511
- }
512
- ),
513
- tool(
514
- "skill_browse",
515
- "Browse the skill marketplace to discover skills published by the community. " +
516
- "Search by keyword, filter by category, and sort by popularity or rating.",
517
- {
518
- query: z.string().optional().describe("Search keywords"),
519
- category: z.string().optional().describe("Filter by category (e.g. 'productivity', 'ecommerce', 'dev-tools')"),
520
- sort: z.enum(["popular", "recent", "rating"]).optional().describe("Sort order (default: popular)"),
521
- limit: z.number().optional().describe("Max results (default: 10)"),
522
- },
523
- async (args) => {
524
- const results = await skillManager.browse({
525
- query: args.query,
526
- category: args.category,
527
- sort: args.sort,
528
- limit: args.limit || 10,
529
- });
530
-
531
- if (results.length === 0) {
532
- const hint = args.query ? ` for "${args.query}"` : "";
533
- return {
534
- content: [{ type: "text", text: `No skills found in marketplace${hint}.` }],
535
- };
536
- }
537
-
538
- let response = "## Skill Marketplace\n\n";
539
- for (const s of results) {
540
- const emoji = s.emoji ? `${s.emoji} ` : "";
541
- const rating = s.avgRating ? ` ★${s.avgRating}` : "";
542
- const installs = s.installCount > 0 ? ` (${s.installCount} installs)` : "";
543
- const author = s.authorName ? ` by ${s.authorName}` : "";
544
- response += `- **${emoji}${s.name}** v${s.version}${author}${installs}${rating}\n`;
545
- response += ` ${s.description}\n`;
546
- response += ` ID: \`${s.id}\`\n\n`;
547
- }
548
- response += "Use `skill_add` with the skill ID to add any of these to your collection.";
549
-
550
- return { content: [{ type: "text", text: response }] };
551
- }
552
- ),
553
- tool(
554
- "skill_add",
555
- "Add a skill to your personal collection. Works for both marketplace skills and newly created drafts. " +
556
- "This is the approval step — after adding, the skill becomes available for use via skill_invoke.",
557
- {
558
- skill_id: z.string().describe("The skill UUID (from skill_browse or skill_create results)"),
559
- },
560
- async (args) => {
561
- const added = await skillManager.addSkill(args.skill_id);
562
- if (!added) {
563
- return {
564
- content: [{ type: "text", text: `Failed to add skill. Check that the ID is correct.` }],
565
- };
566
- }
567
-
568
- const emoji = added.metadata.emoji ? `${added.metadata.emoji} ` : "";
569
- return {
570
- content: [{
571
- type: "text",
572
- text: `Added **${emoji}${added.name}** v${added.version} to your collection. It's now available for use via skill_invoke.`,
573
- }],
574
- };
575
- }
576
- ),
577
- tool(
578
- "skill_publish",
579
- "Publish one of your skills to the marketplace so others can discover and install it.",
580
- {
581
- name: z.string().describe("Name of your skill to publish"),
582
- category: z.string().optional().describe("Category (e.g. 'productivity', 'ecommerce', 'dev-tools')"),
583
- author_name: z.string().optional().describe("Your display name as the author"),
584
- },
585
- async (args) => {
586
- const skill = skillManager.get(args.name);
587
- if (!skill) {
588
- return {
589
- content: [{ type: "text", text: `Skill "${args.name}" not found in your collection.` }],
590
- };
591
- }
592
-
593
- if (skill.source === "external") {
594
- return {
595
- content: [{ type: "text", text: `Cannot publish external skills.` }],
596
- };
597
- }
598
-
599
- const result = await skillManager.publish(args.name, {
600
- category: args.category,
601
- authorName: args.author_name,
602
- });
603
-
604
- if (!result) {
605
- return {
606
- content: [{ type: "text", text: `Failed to publish "${args.name}". The name may already be taken by another author.` }],
607
- };
608
- }
609
-
610
- return {
611
- content: [{
612
- type: "text",
613
- text: `Skill "${args.name}" published to the marketplace! Others can now find and install it.`,
614
- }],
615
- };
616
- }
617
- ),
618
-
619
- // ── User Interaction Tool ───────────────────────────────────
620
-
621
- tool(
622
- "ask_user",
623
- "Ask the user a question via the web UI and wait for their response. " +
624
- "Shows a message with optional predefined option buttons PLUS a free-text input field — " +
625
- "the user can either click a suggested option or type a custom answer. " +
626
- "ALWAYS provide options when you can suggest likely answers. " +
627
- "Do NOT use this for information you can discover yourself (git remote, file contents, etc.).",
628
- {
629
- question: z.string().describe("The question to ask (supports markdown). Be specific about what you need and why."),
630
- options: z.array(z.object({
631
- label: z.string().describe("Button label shown to user"),
632
- action_key: z.string().describe("Machine-readable key returned when selected"),
633
- description: z.string().optional().describe("Tooltip/description for this option"),
634
- })).optional().describe("Suggested options shown as buttons. The user can always type a custom answer instead."),
635
- placeholder: z.string().optional().describe("Placeholder text for the free-text input field"),
636
- timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)"),
637
- },
638
- async (args) => {
639
- const actionId = `ask_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
640
- const timeout = (args.timeout_seconds || 300) * 1000;
641
-
642
- const actionData = {
643
- id: actionId,
644
- type: "ask_user",
645
- message: args.question,
646
- options: args.options || [],
647
- placeholder: args.placeholder || "",
648
- created_at: new Date().toISOString(),
649
- };
650
-
651
- try {
652
- await setActionRequest(taskId, actionData);
653
- log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
654
-
655
- emitEvent(taskId, "user_action_request", actionData).catch(() => {});
656
-
657
- const startTime = Date.now();
658
- const pollInterval = 2000;
659
-
660
- while (Date.now() - startTime < timeout) {
661
- const response = await pollActionResponse(taskId);
662
- if (response && (!response.action_id || response.action_id === actionId)) {
663
- // Response can be either an option click or free-text input
664
- const actionKey = (response.action_key || "") as string;
665
- const text = (response.text || "") as string;
666
- const label = (response.label || actionKey || text) as string;
667
- log.info(`User responded: "${label}"`);
668
- return {
669
- content: [{
670
- type: "text",
671
- text: JSON.stringify({
672
- status: "responded",
673
- action_key: actionKey || "custom_input",
674
- label,
675
- text: text || label,
676
- }),
677
- }],
678
- };
679
- }
680
-
681
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
682
- }
683
-
684
- log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
685
- return {
686
- content: [{
687
- type: "text",
688
- text: JSON.stringify({
689
- status: "timeout",
690
- message: "User did not respond within the timeout period.",
691
- }),
692
- }],
693
- };
694
- } catch (err) {
695
- log.error(`ask_user failed: ${err}`);
696
- return {
697
- content: [{
698
- type: "text",
699
- text: `Failed to ask user: ${err instanceof Error ? err.message : err}`,
700
- }],
701
- };
702
- }
703
- }
704
- ),
705
-
706
- // ── Job Automation Tools ──────────────────────────────────────
707
-
708
- tool(
709
- "job_run",
710
- "Run a job by loading its goal and available skills as capabilities. " +
711
- "You then decide dynamically which skills to use, in what order, and how to chain them based on what you discover. " +
712
- "Use this when the user asks to run their job, or when a scheduled job fires.",
713
- {
714
- job_name: z.string().describe(
715
- "Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')"
716
- ),
717
- },
718
- async (args) => {
719
- if (!userId) {
720
- return {
721
- content: [{ type: "text", text: "Not authenticated. Cannot run job." }],
722
- };
723
- }
724
-
725
- const runner = new JobRunner(userId);
726
- const job = await runner.loadJob(args.job_name);
727
-
728
- if (!job) {
729
- // Try to list available jobs
730
- const jobs = await runner.listJobs();
731
- const available = jobs.length > 0
732
- ? `Available jobs: ${jobs.map((j) => `"${j.name}" (${j.skillCount} skills)`).join(", ")}`
733
- : "No jobs defined yet. Use skill_generate to create a job from a job description.";
734
- return {
735
- content: [{ type: "text", text: `Job "${args.job_name}" not found. ${available}` }],
736
- };
737
- }
738
-
739
- if (job.skills.length === 0) {
740
- return {
741
- content: [{
742
- type: "text",
743
- text: `Job "${args.job_name}" has no linked skills. Use skill_generate to add skills to this job.`,
744
- }],
745
- };
746
- }
747
-
748
- // Create a job run record (execution trace lives in conversation transcript)
749
- const runId = await runner.createRun(job.jobId, {
750
- sessionId: sessionId,
751
- messageId: taskId,
752
- triggerType: "manual",
753
- });
754
-
755
- if (!runId) {
756
- // Still return the prompt even if tracking fails
757
- log.debug("Failed to create job run record, proceeding without tracking");
758
- }
759
-
760
- // Build the structured prompt
761
- const prompt = runner.buildJobPrompt(job, runId || "untracked");
762
-
763
- log.info(`Job "${args.job_name}" started with ${job.skills.length} skills (run: ${runId?.slice(0, 8) || "untracked"})`);
764
-
765
- return {
766
- content: [{ type: "text", text: prompt }],
767
- };
768
- }
769
- ),
770
- tool(
771
- "job_schedule",
772
- "Schedule a job to run automatically on a recurring basis using a cron expression. " +
773
- "For example, schedule your 'software-engineer' job to run every morning at 9am.",
774
- {
775
- job_name: z.string().describe("Name of the job to schedule"),
776
- cron: z.string().describe(
777
- "Cron expression: 'minute hour day-of-month month day-of-week'. " +
778
- "Examples: '0 9 * * *' (daily 9am), '0 9 * * 1-5' (weekdays 9am), '0 */2 * * *' (every 2 hours)"
779
- ),
780
- timezone: z.string().optional().describe("Timezone (default: UTC). Examples: 'UTC', 'America/New_York'"),
781
- schedule_name: z.string().optional().describe("Custom name for this schedule (default: 'Job: <job_name>')"),
782
- },
783
- async (args) => {
784
- if (!userId) {
785
- return {
786
- content: [{ type: "text", text: "Not authenticated. Cannot schedule job." }],
787
- };
788
- }
789
-
790
- const runner = new JobRunner(userId);
791
- const job = await runner.loadJob(args.job_name);
792
-
793
- if (!job) {
794
- return {
795
- content: [{ type: "text", text: `Job "${args.job_name}" not found. Create it first with skill_generate.` }],
796
- };
797
- }
798
-
799
- // Validate cron expression
800
- try {
801
- getNextRunTime(args.cron, args.timezone || "UTC");
802
- } catch {
803
- return {
804
- content: [{
805
- type: "text",
806
- text: `Invalid cron expression: "${args.cron}". Use format: "minute hour day-of-month month day-of-week"`,
807
- }],
808
- };
809
- }
810
-
811
- // Create the scheduled task with a job-run prompt
812
- const name = args.schedule_name || `Job: ${args.job_name}`;
813
- const prompt = `[JobRun: ${args.job_name}] Run the "${args.job_name}" job. Use job_run to execute it.`;
814
- const tz = args.timezone || "UTC";
815
-
816
- try {
817
- const task = await createScheduledTask(userId, name, prompt, args.cron, tz);
818
-
819
- // Link the job_id to the scheduled task
820
- await callMcpHandler("schedule.link_job", {
821
- task_id: task.id,
822
- job_id: job.jobId,
823
- });
824
-
825
- const nextRun = task.next_run_at
826
- ? new Date(task.next_run_at).toLocaleString()
827
- : "calculating...";
828
-
829
- let response = `## Job Scheduled: ${args.job_name}\n\n`;
830
- response += `- **Schedule:** ${args.cron} (${tz})\n`;
831
- response += `- **Next run:** ${nextRun}\n`;
832
- response += `- **Skills:** ${job.skills.length}\n\n`;
833
- response += `The job "${args.job_name}" will automatically run on this schedule. `;
834
- response += `Each run will execute ${job.skills.length} skills in sequence:\n`;
835
- for (const skill of job.skills) {
836
- const emoji = skill.skillEmoji ? `${skill.skillEmoji} ` : "";
837
- response += ` - ${emoji}${skill.skillName}\n`;
838
- }
839
-
840
- log.success(`Job "${args.job_name}" scheduled: ${args.cron}`);
841
- return { content: [{ type: "text", text: response }] };
842
- } catch (err) {
843
- return {
844
- content: [{
845
- type: "text",
846
- text: `Failed to schedule job: ${err instanceof Error ? err.message : err}`,
847
- }],
848
- };
849
- }
850
- }
851
- ),
852
- tool(
853
- "job_status",
854
- "Check the status and run history of a job. Shows recent executions, success rates, and details.",
855
- {
856
- job_name: z.string().optional().describe("Job name to check (omit for all jobs)"),
857
- limit: z.number().optional().describe("Max number of runs to show (default: 5)"),
858
- },
859
- async (args) => {
860
- if (!userId) {
861
- return {
862
- content: [{ type: "text", text: "Not authenticated." }],
863
- };
864
- }
865
-
866
- const runner = new JobRunner(userId);
867
-
868
- // If no job specified, list all jobs
869
- if (!args.job_name) {
870
- const jobs = await runner.listJobs();
871
- if (jobs.length === 0) {
872
- return {
873
- content: [{
874
- type: "text",
875
- text: "No jobs defined. Use skill_generate to create a job from your job description.",
876
- }],
877
- };
878
- }
879
-
880
- let response = "## Your Jobs\n\n";
881
- for (const job of jobs) {
882
- response += `- **${job.name}** (${job.skillCount} skills): ${job.description.slice(0, 100)}\n`;
883
- }
884
- response += "\nUse `job_run` to execute a job, or `job_schedule` to automate it.";
885
- return { content: [{ type: "text", text: response }] };
886
- }
887
-
888
- // Show run history for specific job
889
- const runs = await runner.getRunHistory(args.job_name, args.limit || 5);
890
-
891
- if (runs.length === 0) {
892
- return {
893
- content: [{
894
- type: "text",
895
- text: `No runs found for job "${args.job_name}". Use job_run to execute it.`,
896
- }],
897
- };
898
- }
899
-
900
- let response = `## Job Status: ${args.job_name}\n\n`;
901
- response += `### Recent Runs\n\n`;
902
-
903
- for (const run of runs) {
904
- const statusIcon =
905
- run.status === "completed" ? "+" :
906
- run.status === "failed" ? "x" :
907
- run.status === "running" ? "~" : "-";
908
- const date = new Date(run.startedAt).toLocaleString();
909
- const duration = run.completedAt
910
- ? `${Math.round((new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()) / 1000)}s`
911
- : "in progress";
912
-
913
- response += `[${statusIcon}] ${date} | ${run.triggerType} | ${duration}\n`;
914
- if (run.summary) {
915
- response += ` ${run.summary.slice(0, 100)}\n`;
916
- }
917
- }
918
-
919
- // Stats
920
- const total = runs.length;
921
- const completed = runs.filter((r) => r.status === "completed").length;
922
- const failed = runs.filter((r) => r.status === "failed").length;
923
- response += `\n**Stats:** ${completed}/${total} successful`;
924
- if (failed > 0) response += `, ${failed} failed`;
925
- response += "\n";
926
-
927
- return { content: [{ type: "text", text: response }] };
928
- }
929
- ),
930
- ],
931
- });
932
- }
933
-
934
- // ── Helper: persist job and link skills ────────────────────────────
935
-
936
- async function saveJobToDb(
937
- _userId: string,
938
- jobName: string,
939
- jobDescription: string,
940
- createdSkillNames: string[]
941
- ): Promise<void> {
942
- try {
943
- const data = await callMcpHandler("job.save_with_skills", {
944
- job_name: jobName,
945
- job_description: jobDescription,
946
- skill_names: createdSkillNames,
947
- });
948
-
949
- log.debug(`Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`);
950
- } catch (err) {
951
- log.debug(`saveJobToDb error: ${err}`);
952
- }
953
- }
6
+ export { BROWSER_TOOL_NAMES, createBrowserMcpServer } from "../mcp/browser-server.js";
7
+ export { type AgentToolsDeps, createAgentToolsServer } from "../mcp/agent-tools-server.js";