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/mcp-servers.ts
CHANGED
|
@@ -1,1021 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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 request_user_confirmation 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 \`request_user_confirmation\` with the formatted skill list as "message" 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 request_user_confirmation 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 Tools ──────────────────────────────────
|
|
620
|
-
|
|
621
|
-
tool(
|
|
622
|
-
"request_user_input",
|
|
623
|
-
"Ask the user a clarifying question and wait for their free-text response. " +
|
|
624
|
-
"Use this when you need information that cannot be inferred from context, memory, or the workspace — " +
|
|
625
|
-
"e.g. which account to use, specific preferences, ambiguous instructions, or missing parameters for a skill. " +
|
|
626
|
-
"Do NOT use this for information you can discover yourself (git remote, file contents, etc.).",
|
|
627
|
-
{
|
|
628
|
-
question: z.string().describe("The question to ask the user (supports markdown). Be specific about what you need and why."),
|
|
629
|
-
placeholder: z.string().optional().describe("Placeholder text for the input field (e.g. 'https://github.com/owner/repo')"),
|
|
630
|
-
timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)"),
|
|
631
|
-
},
|
|
632
|
-
async (args) => {
|
|
633
|
-
const actionId = `input_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
634
|
-
const timeout = (args.timeout_seconds || 300) * 1000;
|
|
635
|
-
|
|
636
|
-
const actionData = {
|
|
637
|
-
id: actionId,
|
|
638
|
-
type: "input",
|
|
639
|
-
message: args.question,
|
|
640
|
-
placeholder: args.placeholder || "",
|
|
641
|
-
created_at: new Date().toISOString(),
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
try {
|
|
645
|
-
await setActionRequest(taskId, actionData);
|
|
646
|
-
log.info(`Input request ${actionId}: "${args.question.slice(0, 80)}..."`);
|
|
647
|
-
|
|
648
|
-
emitEvent(taskId, "user_action_request", actionData).catch(() => {});
|
|
649
|
-
|
|
650
|
-
const startTime = Date.now();
|
|
651
|
-
const pollInterval = 2000;
|
|
652
|
-
|
|
653
|
-
while (Date.now() - startTime < timeout) {
|
|
654
|
-
const response = await pollActionResponse(taskId);
|
|
655
|
-
// Match response to this specific request by action_id
|
|
656
|
-
if (response && (!response.action_id || response.action_id === actionId)) {
|
|
657
|
-
const text = (response.text || response.value || "") as string;
|
|
658
|
-
log.info(`User input received: "${text.slice(0, 80)}"`);
|
|
659
|
-
return {
|
|
660
|
-
content: [{
|
|
661
|
-
type: "text",
|
|
662
|
-
text: JSON.stringify({ status: "responded", text }),
|
|
663
|
-
}],
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
log.warn(`Input request ${actionId} timed out`);
|
|
671
|
-
return {
|
|
672
|
-
content: [{
|
|
673
|
-
type: "text",
|
|
674
|
-
text: JSON.stringify({
|
|
675
|
-
status: "timeout",
|
|
676
|
-
message: "User did not respond within the timeout period.",
|
|
677
|
-
}),
|
|
678
|
-
}],
|
|
679
|
-
};
|
|
680
|
-
} catch (err) {
|
|
681
|
-
log.error(`request_user_input failed: ${err}`);
|
|
682
|
-
return {
|
|
683
|
-
content: [{
|
|
684
|
-
type: "text",
|
|
685
|
-
text: `Failed to request user input: ${err instanceof Error ? err.message : err}`,
|
|
686
|
-
}],
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
),
|
|
691
|
-
|
|
692
|
-
tool(
|
|
693
|
-
"request_user_confirmation",
|
|
694
|
-
"Pause and ask the user for approval or input via the web UI. " +
|
|
695
|
-
"Returns the user's chosen action_key. Use this BEFORE creating skills, making irreversible changes, etc. " +
|
|
696
|
-
"The agent will block until the user responds or the timeout expires.",
|
|
697
|
-
{
|
|
698
|
-
message: z.string().describe("What to show the user (supports markdown)"),
|
|
699
|
-
options: z.array(z.object({
|
|
700
|
-
label: z.string().describe("Button label shown to user"),
|
|
701
|
-
action_key: z.string().describe("Machine-readable key returned when selected"),
|
|
702
|
-
description: z.string().optional().describe("Tooltip/description for this option"),
|
|
703
|
-
})).describe("Buttons/options to show the user"),
|
|
704
|
-
timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)"),
|
|
705
|
-
},
|
|
706
|
-
async (args) => {
|
|
707
|
-
const actionId = `action_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
708
|
-
const timeout = (args.timeout_seconds || 300) * 1000;
|
|
709
|
-
|
|
710
|
-
const actionData = {
|
|
711
|
-
id: actionId,
|
|
712
|
-
type: "confirmation",
|
|
713
|
-
message: args.message,
|
|
714
|
-
options: args.options,
|
|
715
|
-
created_at: new Date().toISOString(),
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
// Store action request in message metadata via RPC — UI reads this
|
|
720
|
-
await setActionRequest(taskId, actionData);
|
|
721
|
-
log.info(`Action request ${actionId} stored in metadata, waiting for user response...`);
|
|
722
|
-
|
|
723
|
-
// Also emit event for real-time notification (best-effort)
|
|
724
|
-
emitEvent(taskId, "user_action_request", actionData).catch(() => {});
|
|
725
|
-
|
|
726
|
-
// Poll for response
|
|
727
|
-
const startTime = Date.now();
|
|
728
|
-
const pollInterval = 2000;
|
|
729
|
-
|
|
730
|
-
while (Date.now() - startTime < timeout) {
|
|
731
|
-
const response = await pollActionResponse(taskId);
|
|
732
|
-
// Match response to this specific request by action_id
|
|
733
|
-
if (response && (!response.action_id || response.action_id === actionId)) {
|
|
734
|
-
const actionKey = (response.action_key || response.action || "") as string;
|
|
735
|
-
const label = (response.label || actionKey) as string;
|
|
736
|
-
log.info(`User responded: ${label} (${actionKey})`);
|
|
737
|
-
return {
|
|
738
|
-
content: [{
|
|
739
|
-
type: "text",
|
|
740
|
-
text: JSON.stringify({
|
|
741
|
-
status: "responded",
|
|
742
|
-
action_key: actionKey,
|
|
743
|
-
label,
|
|
744
|
-
}),
|
|
745
|
-
}],
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
log.warn(`Action request ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
753
|
-
return {
|
|
754
|
-
content: [{
|
|
755
|
-
type: "text",
|
|
756
|
-
text: JSON.stringify({
|
|
757
|
-
status: "timeout",
|
|
758
|
-
message: "User did not respond within the timeout period.",
|
|
759
|
-
}),
|
|
760
|
-
}],
|
|
761
|
-
};
|
|
762
|
-
} catch (err) {
|
|
763
|
-
log.error(`request_user_confirmation failed: ${err}`);
|
|
764
|
-
return {
|
|
765
|
-
content: [{
|
|
766
|
-
type: "text",
|
|
767
|
-
text: `Failed to request user confirmation: ${err instanceof Error ? err.message : err}`,
|
|
768
|
-
}],
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
),
|
|
773
|
-
|
|
774
|
-
// ── Job Automation Tools ──────────────────────────────────────
|
|
775
|
-
|
|
776
|
-
tool(
|
|
777
|
-
"job_run",
|
|
778
|
-
"Run a job by loading its goal and available skills as capabilities. " +
|
|
779
|
-
"You then decide dynamically which skills to use, in what order, and how to chain them based on what you discover. " +
|
|
780
|
-
"Use this when the user asks to run their job, or when a scheduled job fires.",
|
|
781
|
-
{
|
|
782
|
-
job_name: z.string().describe(
|
|
783
|
-
"Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')"
|
|
784
|
-
),
|
|
785
|
-
},
|
|
786
|
-
async (args) => {
|
|
787
|
-
if (!userId) {
|
|
788
|
-
return {
|
|
789
|
-
content: [{ type: "text", text: "Not authenticated. Cannot run job." }],
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const runner = new JobRunner(userId);
|
|
794
|
-
const job = await runner.loadJob(args.job_name);
|
|
795
|
-
|
|
796
|
-
if (!job) {
|
|
797
|
-
// Try to list available jobs
|
|
798
|
-
const jobs = await runner.listJobs();
|
|
799
|
-
const available = jobs.length > 0
|
|
800
|
-
? `Available jobs: ${jobs.map((j) => `"${j.name}" (${j.skillCount} skills)`).join(", ")}`
|
|
801
|
-
: "No jobs defined yet. Use skill_generate to create a job from a job description.";
|
|
802
|
-
return {
|
|
803
|
-
content: [{ type: "text", text: `Job "${args.job_name}" not found. ${available}` }],
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
if (job.skills.length === 0) {
|
|
808
|
-
return {
|
|
809
|
-
content: [{
|
|
810
|
-
type: "text",
|
|
811
|
-
text: `Job "${args.job_name}" has no linked skills. Use skill_generate to add skills to this job.`,
|
|
812
|
-
}],
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// Create a job run record (execution trace lives in conversation transcript)
|
|
817
|
-
const runId = await runner.createRun(job.jobId, {
|
|
818
|
-
sessionId: sessionId,
|
|
819
|
-
messageId: taskId,
|
|
820
|
-
triggerType: "manual",
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
if (!runId) {
|
|
824
|
-
// Still return the prompt even if tracking fails
|
|
825
|
-
log.debug("Failed to create job run record, proceeding without tracking");
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// Build the structured prompt
|
|
829
|
-
const prompt = runner.buildJobPrompt(job, runId || "untracked");
|
|
830
|
-
|
|
831
|
-
log.info(`Job "${args.job_name}" started with ${job.skills.length} skills (run: ${runId?.slice(0, 8) || "untracked"})`);
|
|
832
|
-
|
|
833
|
-
return {
|
|
834
|
-
content: [{ type: "text", text: prompt }],
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
),
|
|
838
|
-
tool(
|
|
839
|
-
"job_schedule",
|
|
840
|
-
"Schedule a job to run automatically on a recurring basis using a cron expression. " +
|
|
841
|
-
"For example, schedule your 'software-engineer' job to run every morning at 9am.",
|
|
842
|
-
{
|
|
843
|
-
job_name: z.string().describe("Name of the job to schedule"),
|
|
844
|
-
cron: z.string().describe(
|
|
845
|
-
"Cron expression: 'minute hour day-of-month month day-of-week'. " +
|
|
846
|
-
"Examples: '0 9 * * *' (daily 9am), '0 9 * * 1-5' (weekdays 9am), '0 */2 * * *' (every 2 hours)"
|
|
847
|
-
),
|
|
848
|
-
timezone: z.string().optional().describe("Timezone (default: UTC). Examples: 'UTC', 'America/New_York'"),
|
|
849
|
-
schedule_name: z.string().optional().describe("Custom name for this schedule (default: 'Job: <job_name>')"),
|
|
850
|
-
},
|
|
851
|
-
async (args) => {
|
|
852
|
-
if (!userId) {
|
|
853
|
-
return {
|
|
854
|
-
content: [{ type: "text", text: "Not authenticated. Cannot schedule job." }],
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
const runner = new JobRunner(userId);
|
|
859
|
-
const job = await runner.loadJob(args.job_name);
|
|
860
|
-
|
|
861
|
-
if (!job) {
|
|
862
|
-
return {
|
|
863
|
-
content: [{ type: "text", text: `Job "${args.job_name}" not found. Create it first with skill_generate.` }],
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// Validate cron expression
|
|
868
|
-
try {
|
|
869
|
-
getNextRunTime(args.cron, args.timezone || "UTC");
|
|
870
|
-
} catch {
|
|
871
|
-
return {
|
|
872
|
-
content: [{
|
|
873
|
-
type: "text",
|
|
874
|
-
text: `Invalid cron expression: "${args.cron}". Use format: "minute hour day-of-month month day-of-week"`,
|
|
875
|
-
}],
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// Create the scheduled task with a job-run prompt
|
|
880
|
-
const name = args.schedule_name || `Job: ${args.job_name}`;
|
|
881
|
-
const prompt = `[JobRun: ${args.job_name}] Run the "${args.job_name}" job. Use job_run to execute it.`;
|
|
882
|
-
const tz = args.timezone || "UTC";
|
|
883
|
-
|
|
884
|
-
try {
|
|
885
|
-
const task = await createScheduledTask(userId, name, prompt, args.cron, tz);
|
|
886
|
-
|
|
887
|
-
// Link the job_id to the scheduled task
|
|
888
|
-
await callMcpHandler("schedule.link_job", {
|
|
889
|
-
task_id: task.id,
|
|
890
|
-
job_id: job.jobId,
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
const nextRun = task.next_run_at
|
|
894
|
-
? new Date(task.next_run_at).toLocaleString()
|
|
895
|
-
: "calculating...";
|
|
896
|
-
|
|
897
|
-
let response = `## Job Scheduled: ${args.job_name}\n\n`;
|
|
898
|
-
response += `- **Schedule:** ${args.cron} (${tz})\n`;
|
|
899
|
-
response += `- **Next run:** ${nextRun}\n`;
|
|
900
|
-
response += `- **Skills:** ${job.skills.length}\n\n`;
|
|
901
|
-
response += `The job "${args.job_name}" will automatically run on this schedule. `;
|
|
902
|
-
response += `Each run will execute ${job.skills.length} skills in sequence:\n`;
|
|
903
|
-
for (const skill of job.skills) {
|
|
904
|
-
const emoji = skill.skillEmoji ? `${skill.skillEmoji} ` : "";
|
|
905
|
-
response += ` - ${emoji}${skill.skillName}\n`;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
log.success(`Job "${args.job_name}" scheduled: ${args.cron}`);
|
|
909
|
-
return { content: [{ type: "text", text: response }] };
|
|
910
|
-
} catch (err) {
|
|
911
|
-
return {
|
|
912
|
-
content: [{
|
|
913
|
-
type: "text",
|
|
914
|
-
text: `Failed to schedule job: ${err instanceof Error ? err.message : err}`,
|
|
915
|
-
}],
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
),
|
|
920
|
-
tool(
|
|
921
|
-
"job_status",
|
|
922
|
-
"Check the status and run history of a job. Shows recent executions, success rates, and details.",
|
|
923
|
-
{
|
|
924
|
-
job_name: z.string().optional().describe("Job name to check (omit for all jobs)"),
|
|
925
|
-
limit: z.number().optional().describe("Max number of runs to show (default: 5)"),
|
|
926
|
-
},
|
|
927
|
-
async (args) => {
|
|
928
|
-
if (!userId) {
|
|
929
|
-
return {
|
|
930
|
-
content: [{ type: "text", text: "Not authenticated." }],
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const runner = new JobRunner(userId);
|
|
935
|
-
|
|
936
|
-
// If no job specified, list all jobs
|
|
937
|
-
if (!args.job_name) {
|
|
938
|
-
const jobs = await runner.listJobs();
|
|
939
|
-
if (jobs.length === 0) {
|
|
940
|
-
return {
|
|
941
|
-
content: [{
|
|
942
|
-
type: "text",
|
|
943
|
-
text: "No jobs defined. Use skill_generate to create a job from your job description.",
|
|
944
|
-
}],
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
let response = "## Your Jobs\n\n";
|
|
949
|
-
for (const job of jobs) {
|
|
950
|
-
response += `- **${job.name}** (${job.skillCount} skills): ${job.description.slice(0, 100)}\n`;
|
|
951
|
-
}
|
|
952
|
-
response += "\nUse `job_run` to execute a job, or `job_schedule` to automate it.";
|
|
953
|
-
return { content: [{ type: "text", text: response }] };
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// Show run history for specific job
|
|
957
|
-
const runs = await runner.getRunHistory(args.job_name, args.limit || 5);
|
|
958
|
-
|
|
959
|
-
if (runs.length === 0) {
|
|
960
|
-
return {
|
|
961
|
-
content: [{
|
|
962
|
-
type: "text",
|
|
963
|
-
text: `No runs found for job "${args.job_name}". Use job_run to execute it.`,
|
|
964
|
-
}],
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
let response = `## Job Status: ${args.job_name}\n\n`;
|
|
969
|
-
response += `### Recent Runs\n\n`;
|
|
970
|
-
|
|
971
|
-
for (const run of runs) {
|
|
972
|
-
const statusIcon =
|
|
973
|
-
run.status === "completed" ? "+" :
|
|
974
|
-
run.status === "failed" ? "x" :
|
|
975
|
-
run.status === "running" ? "~" : "-";
|
|
976
|
-
const date = new Date(run.startedAt).toLocaleString();
|
|
977
|
-
const duration = run.completedAt
|
|
978
|
-
? `${Math.round((new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()) / 1000)}s`
|
|
979
|
-
: "in progress";
|
|
980
|
-
|
|
981
|
-
response += `[${statusIcon}] ${date} | ${run.triggerType} | ${duration}\n`;
|
|
982
|
-
if (run.summary) {
|
|
983
|
-
response += ` ${run.summary.slice(0, 100)}\n`;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// Stats
|
|
988
|
-
const total = runs.length;
|
|
989
|
-
const completed = runs.filter((r) => r.status === "completed").length;
|
|
990
|
-
const failed = runs.filter((r) => r.status === "failed").length;
|
|
991
|
-
response += `\n**Stats:** ${completed}/${total} successful`;
|
|
992
|
-
if (failed > 0) response += `, ${failed} failed`;
|
|
993
|
-
response += "\n";
|
|
994
|
-
|
|
995
|
-
return { content: [{ type: "text", text: response }] };
|
|
996
|
-
}
|
|
997
|
-
),
|
|
998
|
-
],
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// ── Helper: persist job and link skills ────────────────────────────
|
|
1003
|
-
|
|
1004
|
-
async function saveJobToDb(
|
|
1005
|
-
_userId: string,
|
|
1006
|
-
jobName: string,
|
|
1007
|
-
jobDescription: string,
|
|
1008
|
-
createdSkillNames: string[]
|
|
1009
|
-
): Promise<void> {
|
|
1010
|
-
try {
|
|
1011
|
-
const data = await callMcpHandler("job.save_with_skills", {
|
|
1012
|
-
job_name: jobName,
|
|
1013
|
-
job_description: jobDescription,
|
|
1014
|
-
skill_names: createdSkillNames,
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
log.debug(`Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`);
|
|
1018
|
-
} catch (err) {
|
|
1019
|
-
log.debug(`saveJobToDb error: ${err}`);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
6
|
+
export { BROWSER_TOOL_NAMES, createBrowserMcpServer } from "../mcp/browser-server.js";
|
|
7
|
+
export { type AgentToolsDeps, createAgentToolsServer } from "../mcp/agent-tools-server.js";
|