assistme 0.7.0 → 0.8.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.
@@ -0,0 +1,3280 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BrowserController,
4
+ MemoryManager,
5
+ SkillManager,
6
+ completeTask,
7
+ createScheduledTask,
8
+ emitEvent,
9
+ ensureBrowserAvailable,
10
+ executeShell,
11
+ failTask,
12
+ getBrowser,
13
+ getConversationHistory,
14
+ getCredentialStore,
15
+ getLocalStore,
16
+ getNextRunTime,
17
+ normalizeSkillName,
18
+ pollActionResponse,
19
+ preprocessDynamicContext,
20
+ resetEventSequence,
21
+ setActionRequest,
22
+ substituteArguments,
23
+ validateSkillName
24
+ } from "../chunk-IKYXC4RJ.js";
25
+ import {
26
+ AppError,
27
+ EDSGER_PRODUCT_SLUG,
28
+ JobRunner,
29
+ MAX_BUDGET_USD,
30
+ MAX_COMPLETE_TASK_RETRIES,
31
+ MAX_CONTENT_SEARCH_FILES,
32
+ MAX_CONTENT_SEARCH_RESULTS,
33
+ MAX_FILE_SEARCH_RESULTS,
34
+ MAX_HISTORY_ENTRIES,
35
+ MAX_HISTORY_RESPONSE_LENGTH,
36
+ MAX_RESPONSE_CONTENT_LENGTH,
37
+ MAX_SKILL_RECORD_RESULT_LENGTH,
38
+ MAX_TOOL_INPUT_LOG_LENGTH,
39
+ MAX_TOOL_RESULT_LENGTH,
40
+ SELF_ANALYSIS_EVENT_CONTEXT_CHARS,
41
+ SELF_ANALYSIS_LOG_CONTEXT_CHARS,
42
+ SELF_ANALYSIS_MAX_CONVERSATION_MESSAGES,
43
+ SELF_ANALYSIS_MAX_MESSAGE_EVENTS,
44
+ SELF_ANALYSIS_MAX_SESSION_LOGS,
45
+ SELF_ANALYSIS_TIMEOUT_MS,
46
+ SkillDecisionSchema,
47
+ callMcpHandler,
48
+ errorMessage,
49
+ log,
50
+ newCorrelationId,
51
+ safeParse,
52
+ setCorrelationId,
53
+ setLogTransport
54
+ } from "../chunk-A2NR7LCQ.js";
55
+ import {
56
+ getConfig
57
+ } from "../chunk-YYSJHZSO.js";
58
+
59
+ // src/workers/conversation.ts
60
+ import { config as loadEnv } from "dotenv";
61
+
62
+ // src/workers/base-handler.ts
63
+ var BaseHandler = class {
64
+ config = null;
65
+ running = false;
66
+ /** Start listening for IPC messages from the orchestrator. */
67
+ start() {
68
+ this.running = true;
69
+ setLogTransport((method, message, extra) => {
70
+ this.send({ type: "log_forward", method, message, extra });
71
+ });
72
+ process.on("message", async (raw) => {
73
+ const message = raw;
74
+ try {
75
+ switch (message.type) {
76
+ case "init":
77
+ this.config = message.config;
78
+ await this.onInit(message.config);
79
+ this.send({ type: "ready" });
80
+ break;
81
+ case "shutdown":
82
+ this.running = false;
83
+ setLogTransport(null);
84
+ await this.onShutdown();
85
+ process.exit(0);
86
+ break;
87
+ default:
88
+ await this.onMessage(message);
89
+ break;
90
+ }
91
+ } catch (err) {
92
+ this.send({
93
+ type: "error",
94
+ error: err instanceof Error ? err.message : String(err)
95
+ });
96
+ }
97
+ });
98
+ process.on("SIGTERM", async () => {
99
+ this.running = false;
100
+ await this.onShutdown();
101
+ process.exit(0);
102
+ });
103
+ process.on("disconnect", () => {
104
+ this.running = false;
105
+ process.exit(0);
106
+ });
107
+ }
108
+ /** Send a message to the orchestrator (main process). */
109
+ send(message) {
110
+ if (process.send) {
111
+ process.send(message);
112
+ }
113
+ }
114
+ /** Log via IPC (logs are displayed by the orchestrator). */
115
+ log(level, message) {
116
+ this.send({ type: "log", level, message });
117
+ }
118
+ };
119
+
120
+ // src/agent/processor.ts
121
+ import {
122
+ query as query3
123
+ } from "@anthropic-ai/claude-agent-sdk";
124
+
125
+ // src/agent/skill-evaluator.ts
126
+ import {
127
+ query
128
+ } from "@anthropic-ai/claude-agent-sdk";
129
+ var SKILL_DECISION_OUTPUT_FORMAT = {
130
+ type: "json_schema",
131
+ schema: {
132
+ type: "object",
133
+ properties: {
134
+ action: { type: "string", enum: ["create", "update", "skip"] },
135
+ name: { type: "string" },
136
+ description: { type: "string" },
137
+ instructions: { type: "string" },
138
+ emoji: { type: "string" },
139
+ keywords: { type: "array", items: { type: "string" } },
140
+ existing_skill_name: { type: "string" },
141
+ improved_instructions: { type: "string" },
142
+ improved_description: { type: "string" },
143
+ reason: { type: "string" }
144
+ },
145
+ required: ["action", "reason"]
146
+ }
147
+ };
148
+ var SKILL_VALIDATION_OUTPUT_FORMAT = {
149
+ type: "json_schema",
150
+ schema: {
151
+ type: "object",
152
+ properties: {
153
+ valid: { type: "boolean" },
154
+ improvements: { type: "string" }
155
+ },
156
+ required: ["valid"]
157
+ }
158
+ };
159
+ var SKILL_EVALUATION_PROMPT = `You just completed a task. Now evaluate whether it should be saved as a reusable Agent Skill.
160
+
161
+ ## Agent Skills Format (agentskills.io)
162
+
163
+ A skill follows the SKILL.md format:
164
+ - name: 1-64 chars, lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens
165
+ - description: 1-1024 chars, describe WHAT it does AND WHEN to use it, include searchable keywords
166
+ - body: markdown step-by-step instructions, examples, edge cases. Keep under 500 lines, <5000 tokens.
167
+ - Use generic placeholders (e.g. {url}, {query}, {product_name}) instead of specific values
168
+ - Instructions should be a REUSABLE workflow, not a transcript of what just happened
169
+ - Include error handling steps and tool references (browser_navigate, browser_read_page, Bash, Read, etc.)
170
+
171
+ ## Your Decision
172
+
173
+ Respond with ONLY a JSON object (no markdown, no explanation outside the JSON). Choose one action:
174
+
175
+ 1. **"create"** \u2014 The task is a reusable workflow worth saving.
176
+ Include: name, description, instructions (full SKILL.md body), emoji, keywords (3-5, include Chinese if task was in Chinese)
177
+
178
+ 2. **"update"** \u2014 An existing skill should be improved based on what you just learned.
179
+ Include: existing_skill_name, improved_instructions (full updated body), improved_description (if changed)
180
+
181
+ 3. **"skip"** \u2014 Not worth capturing (simple Q&A, one-off, too vague, already fully covered by existing skill).
182
+
183
+ Always include "reason" explaining your decision.
184
+
185
+ Use your judgment \u2014 no rigid rules. Consider: Is this repeatable? Can it be generalized? Would it save time next time?`;
186
+ var SKILL_VALIDATION_PROMPT = `Validate this auto-generated skill before it becomes active.
187
+
188
+ Check:
189
+ 1. Are the instructions clear, complete, and actionable?
190
+ 2. Do they use generic placeholders (not hardcoded values)?
191
+ 3. Are error handling steps included?
192
+ 4. Is the description accurate and searchable?
193
+ 5. Would this actually work if followed step-by-step?
194
+
195
+ Respond with ONLY a JSON object:
196
+ - {"valid": true, "improvements": null}
197
+ - {"valid": false, "improvements": "Specific improvements needed"}
198
+ - {"valid": true, "improvements": "Optional minor improvements"}
199
+
200
+ Skill to validate:
201
+ `;
202
+ async function evaluateAndMaybeCreateSkill(opts) {
203
+ const { sessionId, skillManager, model } = opts;
204
+ if (!sessionId) {
205
+ log.debug("Skill evaluation skipped: no session ID to resume");
206
+ return;
207
+ }
208
+ const existingSkills = skillManager.getAll();
209
+ const existingList = existingSkills.length > 0 ? existingSkills.map((s) => `- ${s.name}: ${s.description}`).join("\n") : "(no existing skills)";
210
+ const prompt = `${SKILL_EVALUATION_PROMPT}
211
+
212
+ ## Existing Skills (do NOT duplicate these)
213
+ ${existingList}
214
+
215
+ Respond with a JSON object now.`;
216
+ try {
217
+ let structuredOutput;
218
+ for await (const message of query({
219
+ prompt,
220
+ options: {
221
+ resume: sessionId,
222
+ model,
223
+ allowedTools: [],
224
+ effort: "low",
225
+ outputFormat: SKILL_DECISION_OUTPUT_FORMAT
226
+ }
227
+ })) {
228
+ if (message.type === "result") {
229
+ const resultMsg = message;
230
+ if (resultMsg.subtype === "success") {
231
+ const successMsg = resultMsg;
232
+ structuredOutput = successMsg.structured_output;
233
+ log.debug(
234
+ `Skill evaluation cost: $${successMsg.total_cost_usd.toFixed(4)}`
235
+ );
236
+ }
237
+ }
238
+ }
239
+ const decision = structuredOutput ? safeParse(SkillDecisionSchema, structuredOutput) : null;
240
+ if (!decision) {
241
+ log.debug("Skill evaluation: no valid JSON in response");
242
+ return;
243
+ }
244
+ await executeSkillDecision(decision, skillManager, sessionId, model);
245
+ } catch (err) {
246
+ log.debug(`Skill evaluation error: ${errorMessage(err)}`);
247
+ }
248
+ }
249
+ async function validateSkill(name, description, instructions, sessionId, model) {
250
+ try {
251
+ const skillDoc = `Name: ${name}
252
+ Description: ${description}
253
+
254
+ Instructions:
255
+ ${instructions}`;
256
+ let structuredOutput;
257
+ for await (const message of query({
258
+ prompt: SKILL_VALIDATION_PROMPT + skillDoc,
259
+ options: {
260
+ resume: sessionId,
261
+ model,
262
+ allowedTools: [],
263
+ effort: "low",
264
+ outputFormat: SKILL_VALIDATION_OUTPUT_FORMAT
265
+ }
266
+ })) {
267
+ if (message.type === "result") {
268
+ const resultMsg = message;
269
+ if (resultMsg.subtype === "success") {
270
+ structuredOutput = resultMsg.structured_output;
271
+ }
272
+ }
273
+ }
274
+ const parsed = structuredOutput;
275
+ if (parsed) {
276
+ return { valid: parsed.valid, improvements: parsed.improvements || void 0 };
277
+ }
278
+ return { valid: true };
279
+ } catch (err) {
280
+ log.debug(`Skill validation error: ${errorMessage(err)}`);
281
+ return { valid: true };
282
+ }
283
+ }
284
+ async function executeSkillDecision(decision, skillManager, sessionId, model) {
285
+ switch (decision.action) {
286
+ case "create": {
287
+ if (!decision.name || !decision.instructions) {
288
+ log.debug("Skill create skipped: missing name or instructions");
289
+ return;
290
+ }
291
+ const skillName = normalizeSkillName(decision.name);
292
+ if (!skillName) {
293
+ log.debug(`Skill create skipped: name "${decision.name}" cannot be normalized`);
294
+ return;
295
+ }
296
+ const validationError = validateSkillName(skillName);
297
+ if (validationError) {
298
+ log.debug(`Skill create skipped: ${validationError}`);
299
+ return;
300
+ }
301
+ if (skillName !== decision.name) {
302
+ log.debug(`Normalized skill name: "${decision.name}" \u2192 "${skillName}"`);
303
+ }
304
+ const existing = skillManager.findSimilar(skillName);
305
+ if (existing) {
306
+ log.debug(`Skill create skipped: similar skill "${existing.name}" exists`);
307
+ return;
308
+ }
309
+ let instructions = decision.instructions;
310
+ if (sessionId) {
311
+ log.debug(`Validating skill "${skillName}" before activation...`);
312
+ const validation = await validateSkill(
313
+ skillName,
314
+ decision.description || "",
315
+ instructions,
316
+ sessionId,
317
+ model
318
+ );
319
+ if (!validation.valid) {
320
+ log.info(
321
+ `Skill "${skillName}" failed validation: ${validation.improvements}. Skipping creation.`
322
+ );
323
+ return;
324
+ }
325
+ if (validation.improvements) {
326
+ log.debug(`Skill "${skillName}" validated with suggestions: ${validation.improvements}`);
327
+ instructions += `
328
+
329
+ <!-- Validation notes: ${validation.improvements} -->`;
330
+ }
331
+ }
332
+ const result = await skillManager.create(
333
+ skillName,
334
+ decision.description || "",
335
+ instructions,
336
+ {
337
+ source: "auto_extracted",
338
+ emoji: decision.emoji,
339
+ keywords: decision.keywords
340
+ }
341
+ );
342
+ if (result) {
343
+ await skillManager.syncToAgentSkills(
344
+ skillName,
345
+ decision.description || "",
346
+ instructions,
347
+ "1.0.0",
348
+ {
349
+ source: "auto_extracted",
350
+ emoji: decision.emoji,
351
+ keywords: decision.keywords,
352
+ sourceSkillId: result.id
353
+ }
354
+ );
355
+ log.info(`Auto-created skill "${skillName}" (validated): ${decision.reason}`);
356
+ }
357
+ break;
358
+ }
359
+ case "update": {
360
+ if (!decision.existing_skill_name || !decision.improved_instructions) {
361
+ log.debug("Skill update skipped: missing skill name or instructions");
362
+ return;
363
+ }
364
+ if (sessionId) {
365
+ const validation = await validateSkill(
366
+ decision.existing_skill_name,
367
+ decision.improved_description || "",
368
+ decision.improved_instructions,
369
+ sessionId,
370
+ model
371
+ );
372
+ if (!validation.valid) {
373
+ log.info(
374
+ `Skill update for "${decision.existing_skill_name}" failed validation. Skipping.`
375
+ );
376
+ return;
377
+ }
378
+ }
379
+ const updated = skillManager.update(
380
+ decision.existing_skill_name,
381
+ decision.improved_instructions,
382
+ decision.improved_description
383
+ );
384
+ if (updated) {
385
+ log.info(`Auto-improved skill "${decision.existing_skill_name}": ${decision.reason}`);
386
+ } else {
387
+ log.debug(`Skill update failed: "${decision.existing_skill_name}" not found`);
388
+ }
389
+ break;
390
+ }
391
+ case "skip":
392
+ log.debug(`Skill evaluation: skip \u2014 ${decision.reason}`);
393
+ break;
394
+ }
395
+ }
396
+
397
+ // src/agent/self-analyzer.ts
398
+ import {
399
+ query as query2
400
+ } from "@anthropic-ai/claude-agent-sdk";
401
+ import { submitFeedback, FeedbackError } from "edsger-feedback";
402
+
403
+ // src/db/analysis-data.ts
404
+ async function getSessionLogs(sessionId, limit = 500, conversationId) {
405
+ try {
406
+ const data = await callMcpHandler("log.list", {
407
+ session_id: sessionId,
408
+ limit,
409
+ ...conversationId ? { conversation_id: conversationId } : {}
410
+ });
411
+ return data || [];
412
+ } catch (err) {
413
+ log.debug(`Failed to fetch session logs: ${err instanceof Error ? err.message : err}`);
414
+ return [];
415
+ }
416
+ }
417
+ async function getMessageEvents(messageId, limit = 500) {
418
+ try {
419
+ const data = await callMcpHandler("event.list", {
420
+ message_id: messageId,
421
+ limit
422
+ });
423
+ return data || [];
424
+ } catch (err) {
425
+ log.debug(`Failed to fetch message events: ${err instanceof Error ? err.message : err}`);
426
+ return [];
427
+ }
428
+ }
429
+ async function getConversationMessages(conversationId, limit = 10) {
430
+ try {
431
+ const data = await callMcpHandler(
432
+ "conversation.list_messages",
433
+ {
434
+ conversation_id: conversationId,
435
+ limit
436
+ }
437
+ );
438
+ return data || [];
439
+ } catch (err) {
440
+ log.debug(`Failed to fetch conversation messages: ${err instanceof Error ? err.message : err}`);
441
+ return [];
442
+ }
443
+ }
444
+
445
+ // src/agent/self-analyzer.ts
446
+ var SELF_ANALYSIS_OUTPUT_FORMAT = {
447
+ type: "json_schema",
448
+ schema: {
449
+ type: "object",
450
+ properties: {
451
+ needsImprovement: { type: "boolean" },
452
+ title: { type: "string" },
453
+ description: { type: "string" }
454
+ },
455
+ required: ["needsImprovement", "title", "description"]
456
+ }
457
+ };
458
+ var SELF_ANALYSIS_PROMPT = `You just completed a task as the AssistMe agent. Now critically analyze AssistMe's own implementation \u2014 NOT the user's task itself, but how well AssistMe (the agent system) performed and whether AssistMe's codebase can be improved.
459
+
460
+ ## Your Role
461
+ You are a perfectionist code reviewer analyzing the AssistMe agent system itself. Be critical, thorough, and constructive. Focus on:
462
+
463
+ 1. **Task Completion Quality**: Did AssistMe handle the task optimally? Were there unnecessary steps, missed edge cases, or suboptimal tool usage?
464
+ 2. **Agent Architecture**: Based on what you observed during execution, are there architectural improvements to AssistMe's code (processor, event hooks, MCP servers, skill system, etc.)?
465
+ 3. **Data & Observability**: Evaluate the quality of the context data provided (session logs, message events, conversation messages). What information is missing that would help diagnose issues or improve the system?
466
+ 4. **Error Handling & Resilience**: Were there any failures or retries? Could the error handling be improved?
467
+ 5. **Performance & Efficiency**: Were there unnecessary API calls, redundant operations, or opportunities to optimize?
468
+ 6. **User Experience**: Could the interaction flow be smoother? Is the feedback to the user adequate?
469
+
470
+ ## Context Data Provided
471
+ Below you will find:
472
+ - **Session Logs**: stdout/stderr/status logs from the agent session
473
+ - **Message Events**: Real-time events emitted during task execution (tool calls, results, status changes, thinking blocks)
474
+ - **Conversation Messages**: User interaction history for this conversation
475
+ - **Tool Call Records**: Summary of all tool calls made during the task
476
+ - **Tool Failures**: Any tool calls that failed during execution
477
+
478
+ ## Instructions
479
+ Analyze all provided data critically. Respond with a JSON object containing:
480
+ - "needsImprovement": set to false ONLY if the task was handled perfectly with zero improvements, true otherwise
481
+ - "title": a short summary under 100 chars (empty string if needsImprovement is false)
482
+ - "description": a detailed markdown report (empty string if needsImprovement is false) that includes:
483
+ - **Summary**: overall assessment of how AssistMe performed
484
+ - **Task Completion Quality**: score (1-10) and assessment
485
+ - **Improvements**: numbered list, each with severity (critical/major/minor/suggestion), area, description, and suggestion
486
+ - **Data Quality Gaps**: any gaps in session logs, message events, or conversation context that limited your analysis`;
487
+ function truncateToChars(text, maxChars) {
488
+ if (text.length <= maxChars) return text;
489
+ return text.slice(0, maxChars) + "\n... [truncated]";
490
+ }
491
+ var EVENT_RESULT_SUMMARY_CHARS = 150;
492
+ function filterAndAggregateEvents(events) {
493
+ const lines = [];
494
+ let pendingTextChunks = [];
495
+ const flushTextDelta = () => {
496
+ if (pendingTextChunks.length > 0) {
497
+ const merged = pendingTextChunks.join("");
498
+ const summary = merged.length > 200 ? merged.slice(0, 200) + "..." : merged;
499
+ lines.push(`[text_delta x${pendingTextChunks.length}] ${summary}`);
500
+ pendingTextChunks = [];
501
+ }
502
+ };
503
+ for (const e of events) {
504
+ switch (e.event_type) {
505
+ case "text_delta": {
506
+ const text = e.event_data.text || "";
507
+ pendingTextChunks.push(text);
508
+ break;
509
+ }
510
+ case "thinking":
511
+ flushTextDelta();
512
+ break;
513
+ case "tool_use_input":
514
+ flushTextDelta();
515
+ break;
516
+ case "tool_result": {
517
+ flushTextDelta();
518
+ const name = e.event_data.name || "unknown";
519
+ const result = (e.event_data.result || "").slice(0, EVENT_RESULT_SUMMARY_CHARS);
520
+ lines.push(`[tool_result] ${name}: ${result}`);
521
+ break;
522
+ }
523
+ case "tool_failure":
524
+ case "status_change":
525
+ case "error": {
526
+ flushTextDelta();
527
+ lines.push(`[${e.event_type}] ${JSON.stringify(e.event_data)}`);
528
+ break;
529
+ }
530
+ default: {
531
+ flushTextDelta();
532
+ const dataStr = JSON.stringify(e.event_data);
533
+ lines.push(`[${e.event_type}] ${dataStr.slice(0, 200)}`);
534
+ break;
535
+ }
536
+ }
537
+ }
538
+ flushTextDelta();
539
+ return lines;
540
+ }
541
+ async function buildAnalysisContext(ctx) {
542
+ const [sessionLogs, messageEvents, conversationMessages] = await Promise.all([
543
+ getSessionLogs(ctx.sessionId, SELF_ANALYSIS_MAX_SESSION_LOGS, ctx.conversationId),
544
+ getMessageEvents(ctx.taskId, SELF_ANALYSIS_MAX_MESSAGE_EVENTS),
545
+ getConversationMessages(ctx.conversationId, SELF_ANALYSIS_MAX_CONVERSATION_MESSAGES)
546
+ ]);
547
+ let context = "";
548
+ if (sessionLogs.length > 0) {
549
+ const logText = sessionLogs.map((l) => `[${l.log_type}] ${l.message}`).join("\n");
550
+ context += `
551
+ ## Session Logs (${sessionLogs.length} entries)
552
+ `;
553
+ context += truncateToChars(logText, SELF_ANALYSIS_LOG_CONTEXT_CHARS);
554
+ } else {
555
+ context += "\n## Session Logs\n(No session logs available \u2014 this is itself a data gap to note)\n";
556
+ }
557
+ if (messageEvents.length > 0) {
558
+ const filteredLines = filterAndAggregateEvents(messageEvents);
559
+ const eventText = filteredLines.join("\n");
560
+ context += `
561
+ ## Message Events (${messageEvents.length} raw \u2192 ${filteredLines.length} aggregated)
562
+ `;
563
+ context += truncateToChars(eventText, SELF_ANALYSIS_EVENT_CONTEXT_CHARS);
564
+ } else {
565
+ context += "\n## Message Events\n(No message events available \u2014 this is itself a data gap to note)\n";
566
+ }
567
+ if (conversationMessages.length > 0) {
568
+ context += `
569
+ ## Conversation Messages (${conversationMessages.length} entries)
570
+ `;
571
+ for (const msg of conversationMessages) {
572
+ const role = msg.role || "unknown";
573
+ const content = (msg.content || "").slice(0, 500);
574
+ const status = msg.status || "";
575
+ context += `[${role}${status ? ` (${status})` : ""}] ${content}
576
+ `;
577
+ }
578
+ } else {
579
+ context += "\n## Conversation Messages\n(No conversation messages available)\n";
580
+ }
581
+ if (ctx.toolCallRecords.length > 0) {
582
+ context += `
583
+ ## Tool Call Records (${ctx.toolCallRecords.length} calls)
584
+ `;
585
+ for (const tc of ctx.toolCallRecords) {
586
+ const inputStr = JSON.stringify(tc.input).slice(0, 200);
587
+ context += `- ${tc.name}: ${inputStr} \u2192 ${tc.result.slice(0, 100)}
588
+ `;
589
+ }
590
+ }
591
+ if (ctx.toolFailures.length > 0) {
592
+ context += `
593
+ ## Tool Failures (${ctx.toolFailures.length} failures)
594
+ `;
595
+ for (const tf of ctx.toolFailures) {
596
+ context += `- ${tf.toolName}: ${tf.error}
597
+ `;
598
+ }
599
+ }
600
+ if (ctx.tokenUsage) {
601
+ context += `
602
+ ## Token Usage
603
+ `;
604
+ context += `Input: ${ctx.tokenUsage.input_tokens}, Output: ${ctx.tokenUsage.output_tokens}
605
+ `;
606
+ }
607
+ context += `
608
+ ## Task
609
+ `;
610
+ context += `Prompt: ${ctx.taskPrompt.slice(0, 500)}
611
+ `;
612
+ context += `Response length: ${ctx.taskResponse.length} chars
613
+ `;
614
+ return context;
615
+ }
616
+ async function submitSelfAnalysisFeedback(title, description) {
617
+ if (description.length > 4900) {
618
+ description = description.slice(0, 4900) + "\n...[truncated]";
619
+ }
620
+ try {
621
+ const result = await submitFeedback({
622
+ slug: EDSGER_PRODUCT_SLUG,
623
+ title: title.slice(0, 200),
624
+ description,
625
+ category: "improvement"
626
+ });
627
+ log.info(`Self-analysis feedback submitted: ${result.id}`);
628
+ } catch (err) {
629
+ if (err instanceof FeedbackError) {
630
+ log.debug(`Feedback submission failed (${err.statusCode}): ${err.message}`);
631
+ } else {
632
+ log.debug(`Feedback submission failed: ${errorMessage(err)}`);
633
+ }
634
+ }
635
+ }
636
+ async function runAnalysisQuery(model, prompt) {
637
+ let structuredOutput;
638
+ for await (const message of query2({
639
+ prompt,
640
+ options: {
641
+ model,
642
+ allowedTools: [],
643
+ effort: "medium",
644
+ outputFormat: SELF_ANALYSIS_OUTPUT_FORMAT
645
+ }
646
+ })) {
647
+ if (message.type === "result") {
648
+ const resultMsg = message;
649
+ if (resultMsg.subtype === "success") {
650
+ const successMsg = resultMsg;
651
+ structuredOutput = successMsg.structured_output;
652
+ log.debug(
653
+ `Self-analysis cost: $${successMsg.total_cost_usd.toFixed(4)}`
654
+ );
655
+ if (!structuredOutput) {
656
+ log.warn(
657
+ `Self-analysis: success but no structured_output. result text: ${String(successMsg.result ?? "").slice(0, 500)}`
658
+ );
659
+ }
660
+ } else {
661
+ log.warn(
662
+ `Self-analysis: query returned subtype="${resultMsg.subtype}".`
663
+ );
664
+ }
665
+ }
666
+ }
667
+ if (!structuredOutput || typeof structuredOutput !== "object") return null;
668
+ const output = structuredOutput;
669
+ return {
670
+ needsImprovement: Boolean(output.needsImprovement),
671
+ title: String(output.title || ""),
672
+ description: String(output.description || "")
673
+ };
674
+ }
675
+ async function analyzeSelfPostTask(opts) {
676
+ const {
677
+ model,
678
+ taskId,
679
+ conversationId,
680
+ taskPrompt,
681
+ taskResponse,
682
+ toolCallRecords,
683
+ toolFailures,
684
+ tokenUsage,
685
+ sessionId
686
+ } = opts;
687
+ try {
688
+ log.info(`Self-analysis starting for task ${taskId}`);
689
+ const analysisContext = await buildAnalysisContext({
690
+ taskId,
691
+ conversationId,
692
+ sessionId,
693
+ taskPrompt,
694
+ taskResponse,
695
+ toolCallRecords,
696
+ toolFailures,
697
+ tokenUsage
698
+ });
699
+ const prompt = `${SELF_ANALYSIS_PROMPT}
700
+ ${analysisContext}`;
701
+ const analysisPromise = runAnalysisQuery(model, prompt);
702
+ const timeoutPromise = new Promise(
703
+ (_, reject) => setTimeout(() => reject(new Error(`Self-analysis timed out after ${SELF_ANALYSIS_TIMEOUT_MS / 1e3}s`)), SELF_ANALYSIS_TIMEOUT_MS)
704
+ );
705
+ const result = await Promise.race([analysisPromise, timeoutPromise]);
706
+ if (!result) {
707
+ log.warn("Self-analysis: no result from query");
708
+ return;
709
+ }
710
+ if (!result.needsImprovement) {
711
+ log.info("Self-analysis complete: no improvements needed");
712
+ return;
713
+ }
714
+ log.info(`Self-analysis complete: improvements found \u2014 ${result.title}`);
715
+ await submitSelfAnalysisFeedback(result.title, result.description);
716
+ } catch (err) {
717
+ log.warn(`Self-analysis error: ${errorMessage(err)}`);
718
+ }
719
+ }
720
+
721
+ // src/utils/retry.ts
722
+ async function withRetry(fn, opts = {}) {
723
+ const {
724
+ maxRetries = 3,
725
+ baseDelayMs = 500,
726
+ maxDelayMs = 1e4,
727
+ backoffFactor = 2,
728
+ retryIf,
729
+ label
730
+ } = opts;
731
+ let lastError;
732
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
733
+ try {
734
+ return await fn();
735
+ } catch (err) {
736
+ lastError = err;
737
+ if (retryIf && !retryIf(err)) {
738
+ throw err;
739
+ }
740
+ if (attempt < maxRetries) {
741
+ const delay = Math.min(
742
+ baseDelayMs * Math.pow(backoffFactor, attempt),
743
+ maxDelayMs
744
+ );
745
+ if (label) {
746
+ log.debug(`${label}: attempt ${attempt + 1} failed, retrying in ${delay}ms`);
747
+ }
748
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
749
+ }
750
+ }
751
+ }
752
+ throw lastError;
753
+ }
754
+
755
+ // src/mcp/browser-server.ts
756
+ import {
757
+ createSdkMcpServer,
758
+ tool
759
+ } from "@anthropic-ai/claude-agent-sdk";
760
+ import { z } from "zod/v4";
761
+
762
+ // src/tools/filesystem.ts
763
+ import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
764
+ import { resolve, relative, join, sep } from "path";
765
+ import { glob } from "glob";
766
+ function assertWithinWorkspace(filePath) {
767
+ const config = getConfig();
768
+ const resolved = resolve(config.workspacePath, filePath);
769
+ const rel = relative(config.workspacePath, resolved);
770
+ if (rel.startsWith("..") || rel.startsWith(sep + sep)) {
771
+ throw new AppError(
772
+ `Access denied: path "${filePath}" is outside workspace "${config.workspacePath}"`,
773
+ "PATH_TRAVERSAL"
774
+ );
775
+ }
776
+ return resolved;
777
+ }
778
+ async function readFileContent(path, offset, limit) {
779
+ const resolved = assertWithinWorkspace(path);
780
+ const content = await readFile(resolved, "utf-8");
781
+ const lines = content.split("\n");
782
+ const start = offset || 0;
783
+ const end = limit ? start + limit : lines.length;
784
+ const selected = lines.slice(start, end);
785
+ return selected.map((line, i) => `${String(start + i + 1).padStart(5)} | ${line}`).join("\n");
786
+ }
787
+ async function writeFileContent(path, content) {
788
+ const resolved = assertWithinWorkspace(path);
789
+ const dir = resolve(resolved, "..");
790
+ await mkdir(dir, { recursive: true });
791
+ await writeFile(resolved, content, "utf-8");
792
+ return `File written: ${path} (${content.length} bytes)`;
793
+ }
794
+ async function searchFiles(pattern, directory) {
795
+ const config = getConfig();
796
+ const cwd = directory ? assertWithinWorkspace(directory) : config.workspacePath;
797
+ const matches = await glob(pattern, {
798
+ cwd,
799
+ nodir: false,
800
+ ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
801
+ });
802
+ if (matches.length === 0) return "No files found matching the pattern.";
803
+ return matches.slice(0, MAX_FILE_SEARCH_RESULTS).map((m) => relative(config.workspacePath, join(cwd, m))).join("\n");
804
+ }
805
+ async function listDirectory(path) {
806
+ const config = getConfig();
807
+ const resolved = path ? assertWithinWorkspace(path) : config.workspacePath;
808
+ const entries = await readdir(resolved, { withFileTypes: true });
809
+ const results = [];
810
+ for (const entry of entries) {
811
+ if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
812
+ const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
813
+ const info = entry.isFile() ? await stat(join(resolved, entry.name)).then((s) => ` (${formatSize(s.size)})`) : "";
814
+ results.push(`${icon} ${entry.name}${info}`);
815
+ }
816
+ return results.join("\n") || "Empty directory.";
817
+ }
818
+ function formatSize(bytes) {
819
+ if (bytes < 1024) return `${bytes}B`;
820
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
821
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
822
+ }
823
+ async function searchContent(pattern, fileGlob, directory) {
824
+ const config = getConfig();
825
+ const cwd = directory ? assertWithinWorkspace(directory) : config.workspacePath;
826
+ const files = await glob(fileGlob || "**/*", {
827
+ cwd,
828
+ nodir: true,
829
+ ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
830
+ });
831
+ let regex;
832
+ try {
833
+ regex = new RegExp(pattern, "gi");
834
+ } catch {
835
+ regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
836
+ }
837
+ const results = [];
838
+ for (const file of files.slice(0, MAX_CONTENT_SEARCH_FILES)) {
839
+ try {
840
+ const content = await readFile(join(cwd, file), "utf-8");
841
+ const lines = content.split("\n");
842
+ for (let i = 0; i < lines.length; i++) {
843
+ if (regex.test(lines[i])) {
844
+ const relPath = relative(config.workspacePath, join(cwd, file));
845
+ results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
846
+ regex.lastIndex = 0;
847
+ if (results.length >= MAX_CONTENT_SEARCH_RESULTS) break;
848
+ }
849
+ }
850
+ if (results.length >= MAX_CONTENT_SEARCH_RESULTS) break;
851
+ } catch {
852
+ }
853
+ }
854
+ return results.length > 0 ? results.join("\n") : "No matches found.";
855
+ }
856
+
857
+ // src/tools/index.ts
858
+ async function detectAndHandleLogin(browser) {
859
+ const detection = await browser.detectLoginPage();
860
+ if (!detection.isLoginPage) return null;
861
+ const pageInfo = await browser.getPageInfo();
862
+ let siteName;
863
+ try {
864
+ siteName = new URL(pageInfo.url).hostname;
865
+ } catch {
866
+ siteName = pageInfo.url;
867
+ }
868
+ console.log("\n");
869
+ console.log("\u2501".repeat(60));
870
+ console.log(" \u{1F510} LOGIN REQUIRED");
871
+ console.log("\u2501".repeat(60));
872
+ console.log(` ${detection.reason}`);
873
+ console.log(` Site: ${siteName}`);
874
+ console.log(` Please log in using the browser window.`);
875
+ console.log(` Your session will be saved for future use.`);
876
+ console.log(` (Waiting up to 120s for you to complete login)`);
877
+ console.log("\u2501".repeat(60));
878
+ console.log("\n");
879
+ const loginUrl = pageInfo.url;
880
+ const deadline = Date.now() + 12e4;
881
+ while (Date.now() < deadline) {
882
+ await new Promise((r) => setTimeout(r, 3e3));
883
+ try {
884
+ const currentInfo = await browser.getPageInfo();
885
+ if (currentInfo.url !== loginUrl) {
886
+ await new Promise((r) => setTimeout(r, 1500));
887
+ const newPage = await browser.readPage();
888
+ return `Login completed. Redirected to: ${currentInfo.url}
889
+ Session saved in assistme browser profile for future use.
890
+
891
+ Current page:
892
+ ${newPage.slice(0, 3e3)}`;
893
+ }
894
+ const stillLogin = await browser.detectLoginPage();
895
+ if (!stillLogin.isLoginPage) {
896
+ const newPage = await browser.readPage();
897
+ return `Login completed on ${siteName}.
898
+ Session saved in assistme browser profile for future use.
899
+
900
+ Current page:
901
+ ${newPage.slice(0, 3e3)}`;
902
+ }
903
+ } catch {
904
+ try {
905
+ await browser.connect();
906
+ } catch {
907
+ }
908
+ }
909
+ }
910
+ return `Login wait timed out after 120s. The user may still need to log in at ${siteName}.`;
911
+ }
912
+ async function ensureConnected(browser, tabIndex) {
913
+ if (browser.isConnected() && tabIndex === void 0) return;
914
+ if (!await browser.isAvailable()) {
915
+ const result = await ensureBrowserAvailable();
916
+ if (!result.success) {
917
+ throw new Error(
918
+ `Chrome is not available (${result.action}). ${result.detail || "Please ensure Google Chrome is installed."}`
919
+ );
920
+ }
921
+ }
922
+ await browser.connect(tabIndex);
923
+ }
924
+ async function executeTool(name, input) {
925
+ const browser = getBrowser();
926
+ switch (name) {
927
+ // ── Filesystem ──────────────────────────────────────────
928
+ case "read_file":
929
+ return readFileContent(
930
+ input.path,
931
+ input.offset,
932
+ input.limit
933
+ );
934
+ case "write_file":
935
+ return writeFileContent(input.path, input.content);
936
+ case "search_files":
937
+ return searchFiles(input.pattern, input.directory);
938
+ case "search_content":
939
+ return searchContent(
940
+ input.pattern,
941
+ input.file_glob,
942
+ input.directory
943
+ );
944
+ case "list_directory":
945
+ return listDirectory(input.path);
946
+ case "execute_command":
947
+ return executeShell(input.command, input.cwd);
948
+ // ── Browser (CDP) ───────────────────────────────────────
949
+ case "browser_connect": {
950
+ await ensureConnected(browser, input.tab_index);
951
+ return browser.isConnected() ? "Connected to browser." : "Failed to connect.";
952
+ }
953
+ case "browser_navigate": {
954
+ await ensureConnected(browser);
955
+ const navResult = await browser.navigate(input.url);
956
+ const loginResult = await detectAndHandleLogin(browser);
957
+ if (loginResult) {
958
+ return navResult + "\n\n" + loginResult;
959
+ }
960
+ return navResult;
961
+ }
962
+ case "browser_read_page":
963
+ await ensureConnected(browser);
964
+ return browser.readPage();
965
+ case "browser_screenshot":
966
+ await ensureConnected(browser);
967
+ return browser.screenshot();
968
+ case "browser_click":
969
+ await ensureConnected(browser);
970
+ return browser.click(input.selector);
971
+ case "browser_type":
972
+ await ensureConnected(browser);
973
+ return browser.typeText(input.selector, input.text);
974
+ case "browser_press_key":
975
+ await ensureConnected(browser);
976
+ return browser.pressKey(input.key);
977
+ case "browser_scroll":
978
+ await ensureConnected(browser);
979
+ return input.direction === "up" ? browser.scrollUp() : browser.scrollDown();
980
+ case "browser_select":
981
+ await ensureConnected(browser);
982
+ return browser.selectOption(input.selector, input.option);
983
+ case "browser_evaluate":
984
+ await ensureConnected(browser);
985
+ return browser.evaluate(input.expression);
986
+ case "browser_snapshot": {
987
+ await ensureConnected(browser);
988
+ const snap = await browser.snapshot(input.annotate);
989
+ return BrowserController.formatRefTable(snap) + "\n__SNAPSHOT_IMAGE__:" + snap.image;
990
+ }
991
+ case "browser_act": {
992
+ await ensureConnected(browser);
993
+ const actions = input.actions;
994
+ const wantScreenshot = input.screenshot || false;
995
+ const actResult = await browser.act(actions, wantScreenshot);
996
+ let response = actResult.results.map((r) => `${r.success ? "OK" : "FAIL"}: ${r.result}`).join("\n");
997
+ if (actResult.screenshot) {
998
+ response += "\n__ACT_SCREENSHOT__:" + actResult.screenshot;
999
+ }
1000
+ return response;
1001
+ }
1002
+ case "browser_list_tabs":
1003
+ return browser.listTabs();
1004
+ case "browser_switch_tab":
1005
+ return browser.switchTab(input.index);
1006
+ case "browser_new_tab":
1007
+ return browser.openNewTab(input.url);
1008
+ case "browser_request_user_action": {
1009
+ const message = input.message;
1010
+ const waitSeconds = input.wait_seconds || 60;
1011
+ console.log("\n");
1012
+ console.log("\u2501".repeat(60));
1013
+ console.log(" \u{1F64B} USER ACTION REQUIRED");
1014
+ console.log("\u2501".repeat(60));
1015
+ console.log(` ${message}`);
1016
+ console.log(` (Waiting up to ${waitSeconds}s for you to complete this)`);
1017
+ console.log("\u2501".repeat(60));
1018
+ console.log("\n");
1019
+ let initialUrl = "";
1020
+ try {
1021
+ const info = await browser.getPageInfo();
1022
+ initialUrl = info.url;
1023
+ } catch {
1024
+ }
1025
+ const deadline = Date.now() + waitSeconds * 1e3;
1026
+ while (Date.now() < deadline) {
1027
+ await new Promise((r) => setTimeout(r, 3e3));
1028
+ try {
1029
+ const info = await browser.getPageInfo();
1030
+ if (initialUrl && info.url !== initialUrl) {
1031
+ break;
1032
+ }
1033
+ } catch {
1034
+ }
1035
+ }
1036
+ try {
1037
+ const pageInfo = await browser.readPage();
1038
+ return `User action wait completed. Current page state:
1039
+ ${pageInfo.slice(0, 3e3)}`;
1040
+ } catch {
1041
+ return "User action wait completed. Could not read page state.";
1042
+ }
1043
+ }
1044
+ default:
1045
+ return `Unknown tool: ${name}`;
1046
+ }
1047
+ }
1048
+
1049
+ // src/utils/rate-limiter.ts
1050
+ var RateLimiter = class {
1051
+ tokens;
1052
+ maxTokens;
1053
+ refillRate;
1054
+ // tokens per second
1055
+ lastRefill;
1056
+ maxWaitMs;
1057
+ constructor(opts) {
1058
+ this.maxTokens = opts.maxTokens;
1059
+ this.refillRate = opts.refillRate;
1060
+ this.tokens = opts.maxTokens;
1061
+ this.lastRefill = Date.now();
1062
+ this.maxWaitMs = opts.maxWaitMs ?? 1e4;
1063
+ }
1064
+ refill() {
1065
+ const now = Date.now();
1066
+ const elapsed = (now - this.lastRefill) / 1e3;
1067
+ this.tokens = Math.min(
1068
+ this.maxTokens,
1069
+ this.tokens + elapsed * this.refillRate
1070
+ );
1071
+ this.lastRefill = now;
1072
+ }
1073
+ /**
1074
+ * Acquire a token. Returns immediately if available,
1075
+ * otherwise waits until a token is refilled.
1076
+ * Throws if wait would exceed maxWaitMs.
1077
+ */
1078
+ async acquire() {
1079
+ this.refill();
1080
+ if (this.tokens >= 1) {
1081
+ this.tokens -= 1;
1082
+ return;
1083
+ }
1084
+ const deficit = 1 - this.tokens;
1085
+ const waitMs = deficit / this.refillRate * 1e3;
1086
+ if (waitMs > this.maxWaitMs) {
1087
+ throw new Error(
1088
+ `Rate limit exceeded. Would need to wait ${Math.round(waitMs)}ms (max: ${this.maxWaitMs}ms)`
1089
+ );
1090
+ }
1091
+ await new Promise((resolve2) => setTimeout(resolve2, Math.ceil(waitMs)));
1092
+ this.refill();
1093
+ this.tokens -= 1;
1094
+ }
1095
+ /**
1096
+ * Try to acquire a token without waiting.
1097
+ * Returns true if acquired, false if rate limited.
1098
+ */
1099
+ tryAcquire() {
1100
+ this.refill();
1101
+ if (this.tokens >= 1) {
1102
+ this.tokens -= 1;
1103
+ return true;
1104
+ }
1105
+ return false;
1106
+ }
1107
+ /**
1108
+ * Get remaining tokens (for monitoring).
1109
+ */
1110
+ remaining() {
1111
+ this.refill();
1112
+ return Math.floor(this.tokens);
1113
+ }
1114
+ };
1115
+ var toolRateLimiters = {
1116
+ /** Browser tools: max 10 calls/sec, burst up to 20 */
1117
+ browser: new RateLimiter({ maxTokens: 20, refillRate: 10 }),
1118
+ /** Shell commands: max 5/sec, burst up to 10 */
1119
+ shell: new RateLimiter({ maxTokens: 10, refillRate: 5 }),
1120
+ /** File operations: max 20/sec, burst up to 40 */
1121
+ filesystem: new RateLimiter({ maxTokens: 40, refillRate: 20 })
1122
+ };
1123
+ function getLimiterForTool(toolName) {
1124
+ if (toolName.includes("assistme-browser") || toolName.startsWith("browser_"))
1125
+ return toolRateLimiters.browser;
1126
+ if (toolName === "Bash" || toolName === "execute_command")
1127
+ return toolRateLimiters.shell;
1128
+ if ([
1129
+ "Read",
1130
+ "Write",
1131
+ "Edit",
1132
+ "Glob",
1133
+ "Grep",
1134
+ "read_file",
1135
+ "write_file",
1136
+ "search_files",
1137
+ "search_content",
1138
+ "list_directory"
1139
+ ].includes(toolName))
1140
+ return toolRateLimiters.filesystem;
1141
+ return null;
1142
+ }
1143
+
1144
+ // src/mcp/browser-server.ts
1145
+ async function callTool(name, input) {
1146
+ const limiter = getLimiterForTool(name);
1147
+ if (limiter) await limiter.acquire();
1148
+ const result = await executeTool(name, input);
1149
+ return { content: [{ type: "text", text: result }] };
1150
+ }
1151
+ var BROWSER_TOOL_NAMES = [
1152
+ "browser_connect",
1153
+ "browser_navigate",
1154
+ "browser_read_page",
1155
+ "browser_screenshot",
1156
+ "browser_click",
1157
+ "browser_type",
1158
+ "browser_press_key",
1159
+ "browser_scroll",
1160
+ "browser_select",
1161
+ "browser_snapshot",
1162
+ "browser_act",
1163
+ "browser_evaluate",
1164
+ "browser_list_tabs",
1165
+ "browser_switch_tab",
1166
+ "browser_new_tab",
1167
+ "browser_request_user_action"
1168
+ ];
1169
+ function createBrowserMcpServer() {
1170
+ return createSdkMcpServer({
1171
+ name: "assistme-browser",
1172
+ version: "1.0.0",
1173
+ tools: [
1174
+ tool(
1175
+ "browser_connect",
1176
+ "Connect to the user's real Chrome browser via CDP. Chrome will be auto-launched if not already running.",
1177
+ { tab_index: z.number().optional().describe("Tab index (default: 0)") },
1178
+ async (args) => callTool("browser_connect", args)
1179
+ ),
1180
+ tool(
1181
+ "browser_navigate",
1182
+ "Navigate the user's browser to a URL, using the user's real browser with all their cookies and logins.",
1183
+ { url: z.string().describe("URL to navigate to") },
1184
+ async (args) => callTool("browser_navigate", args)
1185
+ ),
1186
+ tool(
1187
+ "browser_read_page",
1188
+ "Read the text content of the currently open page. Returns page title, URL, and main text content.",
1189
+ {},
1190
+ async () => callTool("browser_read_page", {})
1191
+ ),
1192
+ tool(
1193
+ "browser_screenshot",
1194
+ "Take a screenshot of the current browser page. Returns a base64-encoded PNG image.",
1195
+ {},
1196
+ async () => {
1197
+ const limiter = getLimiterForTool("browser_screenshot");
1198
+ if (limiter) await limiter.acquire();
1199
+ const base64 = await executeTool("browser_screenshot", {});
1200
+ if (base64.length > 100) {
1201
+ return {
1202
+ content: [{ type: "image", data: base64, mimeType: "image/png" }]
1203
+ };
1204
+ }
1205
+ return { content: [{ type: "text", text: base64 }] };
1206
+ }
1207
+ ),
1208
+ tool(
1209
+ "browser_click",
1210
+ "Click on an element in the user's browser using a CSS selector.",
1211
+ { selector: z.string().describe("CSS selector of the element to click") },
1212
+ async (args) => callTool("browser_click", args)
1213
+ ),
1214
+ tool(
1215
+ "browser_type",
1216
+ "Type text into an input field in the user's browser. If the CSS selector fails, automatically falls back to typing into the currently focused element. Works with contenteditable elements (rich text editors) and cross-origin iframes.",
1217
+ {
1218
+ selector: z.string().describe("CSS selector of the input element"),
1219
+ text: z.string().describe("Text to type")
1220
+ },
1221
+ async (args) => callTool("browser_type", args)
1222
+ ),
1223
+ tool(
1224
+ "browser_press_key",
1225
+ "Press a keyboard key in the browser. Supports: Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp.",
1226
+ { key: z.string().describe("Key to press") },
1227
+ async (args) => callTool("browser_press_key", args)
1228
+ ),
1229
+ tool(
1230
+ "browser_scroll",
1231
+ "Scroll the page up or down.",
1232
+ { direction: z.string().describe("'down' or 'up'") },
1233
+ async (args) => callTool("browser_scroll", args)
1234
+ ),
1235
+ tool(
1236
+ "browser_select",
1237
+ "Select an option from a dropdown menu. Handles both native <select> elements and custom dropdowns (Material Design, React, Angular). Use this instead of manually clicking dropdown items.",
1238
+ {
1239
+ selector: z.string().describe(
1240
+ "CSS selector of the dropdown, or its label/placeholder text (e.g. 'Month', 'Gender', '#country')"
1241
+ ),
1242
+ option: z.string().describe("Visible text of the option to select (e.g. 'March', 'Male')")
1243
+ },
1244
+ async (args) => callTool("browser_select", args)
1245
+ ),
1246
+ tool(
1247
+ "browser_snapshot",
1248
+ "Take a screenshot and discover all interactive elements with numbered refs. Returns a screenshot + a compact ref table. PREFERRED way to understand a page. Set annotate=true to overlay red ref badges (useful for simple pages). Use the ref numbers with browser_act to interact with elements.",
1249
+ {
1250
+ annotate: z.boolean().optional().describe(
1251
+ "Overlay ref badges on the screenshot. Default false. Use true for simple pages where visual context helps."
1252
+ )
1253
+ },
1254
+ async (args) => {
1255
+ const limiter = getLimiterForTool("browser_snapshot");
1256
+ if (limiter) await limiter.acquire();
1257
+ const result = await executeTool("browser_snapshot", args);
1258
+ const parts = result.split("\n__SNAPSHOT_IMAGE__:");
1259
+ const refTable = parts[0];
1260
+ const imageData = parts[1] || "";
1261
+ const content = [];
1262
+ if (imageData.length > 100) {
1263
+ content.push({ type: "image", data: imageData, mimeType: "image/png" });
1264
+ }
1265
+ content.push({ type: "text", text: refTable });
1266
+ return { content };
1267
+ }
1268
+ ),
1269
+ tool(
1270
+ "browser_act",
1271
+ "Execute actions using ref numbers from browser_snapshot. Supports: click, type, select, press, scroll, wait. Batch multiple actions in one call to reduce round-trips. Set screenshot=true to see the result.",
1272
+ {
1273
+ actions: z.array(
1274
+ z.object({
1275
+ action: z.enum(["click", "type", "select", "press", "scroll", "wait"]).describe("Action type"),
1276
+ ref: z.number().optional().describe("Ref number from browser_snapshot"),
1277
+ text: z.string().optional().describe("Text to type (for 'type' action)"),
1278
+ option: z.string().optional().describe("Option to select (for 'select' action)"),
1279
+ key: z.string().optional().describe("Key to press (for 'press' action)"),
1280
+ direction: z.string().optional().describe("'up' or 'down' (for 'scroll')"),
1281
+ ms: z.number().optional().describe("Wait duration in ms (for 'wait', max 5000)")
1282
+ })
1283
+ ).describe("Actions to execute sequentially"),
1284
+ screenshot: z.boolean().optional().describe("Take screenshot after actions (default: false)")
1285
+ },
1286
+ async (args) => {
1287
+ const limiter = getLimiterForTool("browser_act");
1288
+ if (limiter) await limiter.acquire();
1289
+ const result = await executeTool("browser_act", {
1290
+ actions: args.actions,
1291
+ screenshot: args.screenshot
1292
+ });
1293
+ const parts = result.split("\n__ACT_SCREENSHOT__:");
1294
+ const actionText = parts[0];
1295
+ const screenshotData = parts[1] || "";
1296
+ const content = [];
1297
+ content.push({ type: "text", text: actionText });
1298
+ if (screenshotData.length > 100) {
1299
+ content.push({ type: "image", data: screenshotData, mimeType: "image/png" });
1300
+ }
1301
+ return { content };
1302
+ }
1303
+ ),
1304
+ tool(
1305
+ "browser_evaluate",
1306
+ "Execute JavaScript in the browser page context. Use as a last resort when browser_snapshot + browser_act cannot handle the interaction.",
1307
+ { expression: z.string().describe("JavaScript expression to evaluate") },
1308
+ async (args) => callTool("browser_evaluate", args)
1309
+ ),
1310
+ tool(
1311
+ "browser_list_tabs",
1312
+ "List all open tabs in the user's browser.",
1313
+ {},
1314
+ async () => callTool("browser_list_tabs", {})
1315
+ ),
1316
+ tool(
1317
+ "browser_switch_tab",
1318
+ "Switch to a different browser tab by index.",
1319
+ { index: z.number().describe("Tab index") },
1320
+ async (args) => callTool("browser_switch_tab", args)
1321
+ ),
1322
+ tool(
1323
+ "browser_new_tab",
1324
+ "Open a new tab in the user's browser, optionally navigating to a URL.",
1325
+ { url: z.string().optional().describe("URL to open (default: blank)") },
1326
+ async (args) => callTool("browser_new_tab", args)
1327
+ ),
1328
+ tool(
1329
+ "browser_request_user_action",
1330
+ "Request the user to perform an action in their browser (login, CAPTCHA, 2FA, etc.).",
1331
+ {
1332
+ message: z.string().describe("Clear description of what the user needs to do"),
1333
+ wait_seconds: z.number().optional().describe("How long to wait (default: 60)")
1334
+ },
1335
+ async (args) => callTool("browser_request_user_action", args)
1336
+ )
1337
+ ]
1338
+ });
1339
+ }
1340
+
1341
+ // src/mcp/agent-tools-server.ts
1342
+ import {
1343
+ createSdkMcpServer as createSdkMcpServer2,
1344
+ tool as tool2
1345
+ } from "@anthropic-ai/claude-agent-sdk";
1346
+ import { z as z2 } from "zod/v4";
1347
+
1348
+ // src/credentials/program-store.ts
1349
+ import { randomUUID } from "crypto";
1350
+ function rowToMeta(row) {
1351
+ return {
1352
+ id: row.id,
1353
+ name: row.name,
1354
+ description: row.description,
1355
+ language: row.language,
1356
+ directory: row.directory,
1357
+ skillName: row.skill_name,
1358
+ tags: JSON.parse(row.tags),
1359
+ status: row.status,
1360
+ metadata: JSON.parse(row.metadata),
1361
+ createdAt: row.created_at,
1362
+ updatedAt: row.updated_at
1363
+ };
1364
+ }
1365
+ var ProgramStore = class {
1366
+ get db() {
1367
+ return getLocalStore().getDb();
1368
+ }
1369
+ save(name, description, directory, opts) {
1370
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1371
+ const existing = this.getByName(name);
1372
+ if (existing) {
1373
+ this.db.prepare(
1374
+ `UPDATE programs
1375
+ SET description = ?, directory = ?, language = ?, skill_name = ?, tags = ?, metadata = ?, updated_at = ?
1376
+ WHERE name = ?`
1377
+ ).run(
1378
+ description,
1379
+ directory,
1380
+ opts?.language ?? existing.language,
1381
+ opts?.skillName ?? existing.skillName,
1382
+ JSON.stringify(opts?.tags ?? existing.tags),
1383
+ JSON.stringify(opts?.metadata ?? existing.metadata),
1384
+ now,
1385
+ name
1386
+ );
1387
+ return this.getByName(name);
1388
+ }
1389
+ const id = randomUUID();
1390
+ this.db.prepare(
1391
+ `INSERT INTO programs (id, name, description, language, directory, skill_name, tags, status, metadata, created_at, updated_at)
1392
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)`
1393
+ ).run(
1394
+ id,
1395
+ name,
1396
+ description,
1397
+ opts?.language ?? "unknown",
1398
+ directory,
1399
+ opts?.skillName ?? null,
1400
+ JSON.stringify(opts?.tags ?? []),
1401
+ JSON.stringify(opts?.metadata ?? {}),
1402
+ now,
1403
+ now
1404
+ );
1405
+ return {
1406
+ id,
1407
+ name,
1408
+ description,
1409
+ language: opts?.language ?? "unknown",
1410
+ directory,
1411
+ skillName: opts?.skillName ?? null,
1412
+ tags: opts?.tags ?? [],
1413
+ status: "active",
1414
+ metadata: opts?.metadata ?? {},
1415
+ createdAt: now,
1416
+ updatedAt: now
1417
+ };
1418
+ }
1419
+ getByName(name) {
1420
+ const row = this.db.prepare("SELECT * FROM programs WHERE name = ?").get(name);
1421
+ return row ? rowToMeta(row) : null;
1422
+ }
1423
+ getById(id) {
1424
+ const row = this.db.prepare("SELECT * FROM programs WHERE id = ?").get(id);
1425
+ return row ? rowToMeta(row) : null;
1426
+ }
1427
+ list(opts) {
1428
+ let query4 = "SELECT * FROM programs WHERE 1=1";
1429
+ const params = [];
1430
+ if (opts?.skillName) {
1431
+ query4 += " AND skill_name = ?";
1432
+ params.push(opts.skillName);
1433
+ }
1434
+ if (opts?.status) {
1435
+ query4 += " AND status = ?";
1436
+ params.push(opts.status);
1437
+ }
1438
+ query4 += " ORDER BY updated_at DESC";
1439
+ const rows = this.db.prepare(query4).all(...params);
1440
+ return rows.map(rowToMeta);
1441
+ }
1442
+ removeByName(name) {
1443
+ const result = this.db.prepare("DELETE FROM programs WHERE name = ?").run(name);
1444
+ return result.changes > 0;
1445
+ }
1446
+ archive(name) {
1447
+ const result = this.db.prepare("UPDATE programs SET status = 'archived', updated_at = ? WHERE name = ?").run((/* @__PURE__ */ new Date()).toISOString(), name);
1448
+ return result.changes > 0;
1449
+ }
1450
+ };
1451
+ var _instance = null;
1452
+ function getProgramStore() {
1453
+ if (!_instance) {
1454
+ _instance = new ProgramStore();
1455
+ }
1456
+ return _instance;
1457
+ }
1458
+
1459
+ // src/mcp/agent-tools-server.ts
1460
+ function createAgentToolsServer(deps) {
1461
+ const { memoryManager, skillManager, taskId, sessionId, heartbeatEngine, onUserWaitStart, onUserWaitEnd } = deps;
1462
+ return createSdkMcpServer2({
1463
+ name: "assistme-agent",
1464
+ version: "1.0.0",
1465
+ tools: [
1466
+ tool2(
1467
+ "memory_store",
1468
+ "Store a memory about the user that persists across conversations. Use when you learn preferences, habits, or standing instructions.",
1469
+ {
1470
+ content: z2.string().describe("What to remember (concise, factual statement)"),
1471
+ category: z2.string().optional().describe("Category: general, preference, instruction, context, skill_learned, fact"),
1472
+ importance: z2.number().optional().describe("Importance 1-10 (default: 5). Use 8+ for instructions"),
1473
+ tags: z2.array(z2.string()).optional().describe("Optional tags for searchability")
1474
+ },
1475
+ async (args) => {
1476
+ if (!memoryManager) {
1477
+ return {
1478
+ content: [{ type: "text", text: "Memory manager not available." }]
1479
+ };
1480
+ }
1481
+ const mem = await memoryManager.remember(
1482
+ args.content,
1483
+ args.category || "general",
1484
+ {
1485
+ importance: args.importance || 5,
1486
+ tags: args.tags || [],
1487
+ sourceMessageId: taskId
1488
+ }
1489
+ );
1490
+ const result = `Memory stored: "${mem.content}" [${mem.category}, importance: ${mem.importance}]`;
1491
+ return { content: [{ type: "text", text: result }] };
1492
+ }
1493
+ ),
1494
+ tool2(
1495
+ "skill_create",
1496
+ "Create a new skill and add it to the user's collection. Returns the skill ID on success.",
1497
+ {
1498
+ name: z2.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
1499
+ description: z2.string().describe("One-line description of what this skill does"),
1500
+ instructions: z2.string().describe("Markdown step-by-step instructions"),
1501
+ emoji: z2.string().optional().describe("Single emoji representing this skill")
1502
+ },
1503
+ async (args) => {
1504
+ const nameError = validateSkillName(args.name);
1505
+ if (nameError) {
1506
+ return {
1507
+ content: [
1508
+ {
1509
+ type: "text",
1510
+ text: `Invalid skill name: ${nameError}. Use lowercase kebab-case like "flight-booking".`
1511
+ }
1512
+ ]
1513
+ };
1514
+ }
1515
+ const existing = skillManager.findSimilar(args.name);
1516
+ if (existing) {
1517
+ return {
1518
+ content: [
1519
+ {
1520
+ type: "text",
1521
+ text: `A similar skill "${existing.name}" already exists in your collection. Use skill_improve to update it instead.`
1522
+ }
1523
+ ]
1524
+ };
1525
+ }
1526
+ const result = await skillManager.create(args.name, args.description, args.instructions, {
1527
+ source: "manual",
1528
+ emoji: args.emoji
1529
+ });
1530
+ if (!result) {
1531
+ return {
1532
+ content: [{ type: "text", text: `Failed to create skill "${args.name}".` }]
1533
+ };
1534
+ }
1535
+ await skillManager.syncToAgentSkills(
1536
+ args.name,
1537
+ args.description,
1538
+ args.instructions,
1539
+ "1.0.0",
1540
+ {
1541
+ source: "manual",
1542
+ emoji: args.emoji,
1543
+ sourceSkillId: result.id
1544
+ }
1545
+ );
1546
+ log.success(`Skill "${args.name}" created and added to collection`);
1547
+ return {
1548
+ content: [
1549
+ {
1550
+ type: "text",
1551
+ text: `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`
1552
+ }
1553
+ ]
1554
+ };
1555
+ }
1556
+ ),
1557
+ tool2(
1558
+ "skill_improve",
1559
+ "Improve an existing skill with better instructions based on what you just learned. Version auto-bumped.",
1560
+ {
1561
+ name: z2.string().describe("Name of the existing skill to improve"),
1562
+ improved_instructions: z2.string().describe("Full updated markdown instructions (not a diff)"),
1563
+ description: z2.string().optional().describe("Updated description (optional)")
1564
+ },
1565
+ async (args) => {
1566
+ const existing = skillManager.get(args.name);
1567
+ if (!existing) {
1568
+ const available = skillManager.getAll().map((s) => s.name).join(", ");
1569
+ return {
1570
+ content: [
1571
+ {
1572
+ type: "text",
1573
+ text: `Skill "${args.name}" not found. Available skills: ${available}`
1574
+ }
1575
+ ]
1576
+ };
1577
+ }
1578
+ const updated = skillManager.update(
1579
+ args.name,
1580
+ args.improved_instructions,
1581
+ args.description
1582
+ );
1583
+ if (updated) {
1584
+ log.success(`Self-improvement: improved skill "${args.name}"`);
1585
+ return {
1586
+ content: [
1587
+ {
1588
+ type: "text",
1589
+ text: `Skill "${args.name}" improved and version bumped.`
1590
+ }
1591
+ ]
1592
+ };
1593
+ }
1594
+ return {
1595
+ content: [
1596
+ {
1597
+ type: "text",
1598
+ text: `Failed to update skill "${args.name}".`
1599
+ }
1600
+ ]
1601
+ };
1602
+ }
1603
+ ),
1604
+ tool2(
1605
+ "skill_invoke",
1606
+ "Load a skill's full instructions when relevant to the current task. Call this when you determine a skill from the Available Skills list matches the user's request.",
1607
+ {
1608
+ name: z2.string().describe("Skill name from the Available Skills list"),
1609
+ arguments: z2.string().optional().describe("Arguments to pass to the skill (replaces $ARGUMENTS placeholders)")
1610
+ },
1611
+ async (args) => {
1612
+ const skill = skillManager.get(args.name);
1613
+ if (!skill) {
1614
+ const available = skillManager.getAll().map((s) => s.name).join(", ");
1615
+ return {
1616
+ content: [
1617
+ {
1618
+ type: "text",
1619
+ text: `Skill "${args.name}" not found. Available skills: ${available}`
1620
+ }
1621
+ ]
1622
+ };
1623
+ }
1624
+ let content = skill.content;
1625
+ if (args.arguments) {
1626
+ content = substituteArguments(content, args.arguments);
1627
+ }
1628
+ content = preprocessDynamicContext(content);
1629
+ let response = `## Skill: ${skill.name}
1630
+ `;
1631
+ if (skill.description) {
1632
+ response += `*${skill.description}*
1633
+ `;
1634
+ }
1635
+ response += `
1636
+ ${content}`;
1637
+ if (skill.allowedTools.length > 0) {
1638
+ response += `
1639
+
1640
+ **Allowed tools for this skill:** ${skill.allowedTools.join(", ")}
1641
+ `;
1642
+ }
1643
+ const credReqs = skill.metadata.credentials;
1644
+ if (credReqs && credReqs.length > 0) {
1645
+ const store = getCredentialStore();
1646
+ const missing = [];
1647
+ for (const req of credReqs) {
1648
+ const cred = store.getByName(req.name);
1649
+ if (cred) {
1650
+ response += `
1651
+
1652
+ **Credential: ${req.name}** (${req.type})
1653
+ `;
1654
+ response += `\`\`\`json
1655
+ ${JSON.stringify(cred.data, null, 2)}
1656
+ \`\`\`
1657
+ `;
1658
+ } else if (req.required) {
1659
+ missing.push(`${req.name} (${req.description})`);
1660
+ }
1661
+ }
1662
+ if (missing.length > 0) {
1663
+ response += `
1664
+
1665
+ **Missing required credentials:**
1666
+ `;
1667
+ for (const m of missing) {
1668
+ response += `- ${m}
1669
+ `;
1670
+ }
1671
+ response += `
1672
+ Use \`ask_user\` to request these from the user, or create them yourself (e.g. register an account), then store with \`credential_set\`.
1673
+ `;
1674
+ }
1675
+ }
1676
+ log.info(`Skill invoked: "${args.name}"`);
1677
+ skillManager.logInvocation(args.name, {
1678
+ messageId: taskId,
1679
+ sessionId,
1680
+ arguments: args.arguments
1681
+ }).catch(() => {
1682
+ });
1683
+ return {
1684
+ content: [{ type: "text", text: response }]
1685
+ };
1686
+ }
1687
+ ),
1688
+ tool2(
1689
+ "skill_search",
1690
+ "Search for skills by keyword. Uses full-text search across skill names, descriptions, and content. Use this to discover relevant skills when the Available Skills list doesn't have an obvious match.",
1691
+ {
1692
+ query: z2.string().describe("Search query (keywords, topic, or task description)"),
1693
+ limit: z2.number().optional().describe("Max results (default: 5)")
1694
+ },
1695
+ async (args) => {
1696
+ const results = await skillManager.searchDb(args.query, args.limit || 5);
1697
+ if (results.length === 0) {
1698
+ return {
1699
+ content: [{ type: "text", text: `No skills found for "${args.query}".` }]
1700
+ };
1701
+ }
1702
+ let response = `## Skills matching "${args.query}"
1703
+
1704
+ `;
1705
+ for (const r of results) {
1706
+ const emoji = r.emoji ? `${r.emoji} ` : "";
1707
+ const usage = r.invocationCount > 0 ? ` (used ${r.invocationCount}x)` : "";
1708
+ response += `- **${emoji}${r.name}**${usage}: ${r.description}
1709
+ `;
1710
+ }
1711
+ response += "\nUse skill_invoke to load any of these skills.";
1712
+ return { content: [{ type: "text", text: response }] };
1713
+ }
1714
+ ),
1715
+ tool2(
1716
+ "skill_generate",
1717
+ "Prepare context for generating skills from a job description. Returns existing skills and job info so you can analyze the job and create skills using skill_create (which auto-adds to user's collection). After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
1718
+ {
1719
+ job_name: z2.string().describe(
1720
+ "Short name for this job/role. Example: '\u7535\u5546\u8FD0\u8425', 'Frontend Dev', 'Data Analyst'"
1721
+ ),
1722
+ job_description: z2.string().describe(
1723
+ "Description of the user's job, role, and daily tasks. Can be in any language. Example: '\u6211\u662F\u7535\u5546\u8FD0\u8425\uFF0C\u6BCF\u5929\u8981\u770B\u7ADE\u54C1\u4EF7\u683C\u3001\u5199\u5546\u54C1\u6587\u6848\u3001\u56DE\u590D\u5BA2\u6237\u8BC4\u8BBA'"
1724
+ )
1725
+ },
1726
+ async (args) => {
1727
+ const existingNames = skillManager.getAll().map((s) => s.name);
1728
+ let response = `## Job: ${args.job_name}
1729
+ `;
1730
+ response += `**Description:** ${args.job_description}
1731
+
1732
+ `;
1733
+ if (existingNames.length > 0) {
1734
+ response += `**Existing skills (do NOT duplicate):** ${existingNames.join(", ")}
1735
+
1736
+ `;
1737
+ }
1738
+ response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.
1739
+
1740
+ `;
1741
+ response += `**IMPORTANT \u2014 You MUST use ask_user before creating skills:**
1742
+ `;
1743
+ response += `1. Analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).
1744
+ `;
1745
+ response += `2. Call \`ask_user\` with the formatted skill list as "question" and these options:
1746
+ `;
1747
+ 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"}]
1748
+ `;
1749
+ response += `3. WAIT for the response. If action_key is "approve_all", create all skills using \`skill_create\`. If "cancel", stop.
1750
+ `;
1751
+ response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling ask_user first.
1752
+
1753
+ `;
1754
+ response += `For each skill, call \`skill_create\` with:
1755
+ `;
1756
+ response += `- name: kebab-case name (e.g. "slack-message-check")
1757
+ `;
1758
+ response += `- description: one-line description
1759
+ `;
1760
+ response += `- instructions: detailed step-by-step markdown instructions the agent can follow
1761
+ `;
1762
+ response += `- emoji: a single emoji representing the skill
1763
+
1764
+ `;
1765
+ response += `skill_create automatically adds the skill to the user's collection \u2014 no need to call skill_add.
1766
+
1767
+ `;
1768
+ 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.
1769
+
1770
+ `;
1771
+ response += `**Guidelines for skill instructions:**
1772
+ `;
1773
+ response += `- Write clear, actionable markdown steps
1774
+ `;
1775
+ response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks
1776
+ `;
1777
+ response += `- Include error handling steps
1778
+ `;
1779
+ response += `- Use placeholders like {query}, {date} for variable inputs
1780
+ `;
1781
+ response += `- Each skill should be a single, well-defined workflow (10-25 steps)
1782
+ `;
1783
+ return { content: [{ type: "text", text: response }] };
1784
+ }
1785
+ ),
1786
+ tool2(
1787
+ "skill_link_job",
1788
+ "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.",
1789
+ {
1790
+ job_name: z2.string().describe("Name of the job to link skills to"),
1791
+ job_description: z2.string().describe("Job description (used if job doesn't exist yet)"),
1792
+ skill_names: z2.array(z2.string()).describe("Names of skills to link to this job")
1793
+ },
1794
+ async (args) => {
1795
+ try {
1796
+ await saveJobToDb(args.job_name, args.job_description, args.skill_names);
1797
+ log.success(
1798
+ `Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`
1799
+ );
1800
+ return {
1801
+ content: [
1802
+ {
1803
+ type: "text",
1804
+ text: `Job "${args.job_name}" linked with ${args.skill_names.length} skills and marked as analyzed.`
1805
+ }
1806
+ ]
1807
+ };
1808
+ } catch (err) {
1809
+ return {
1810
+ content: [
1811
+ {
1812
+ type: "text",
1813
+ text: `Failed to link job: ${err instanceof Error ? err.message : err}`
1814
+ }
1815
+ ]
1816
+ };
1817
+ }
1818
+ }
1819
+ ),
1820
+ tool2(
1821
+ "skill_browse",
1822
+ "Browse the skill marketplace to discover skills published by the community. Search by keyword, filter by category, and sort by popularity or rating.",
1823
+ {
1824
+ query: z2.string().optional().describe("Search keywords"),
1825
+ category: z2.string().optional().describe("Filter by category (e.g. 'productivity', 'ecommerce', 'dev-tools')"),
1826
+ sort: z2.enum(["popular", "recent", "rating"]).optional().describe("Sort order (default: popular)"),
1827
+ limit: z2.number().optional().describe("Max results (default: 10)")
1828
+ },
1829
+ async (args) => {
1830
+ const results = await skillManager.browse({
1831
+ query: args.query,
1832
+ category: args.category,
1833
+ sort: args.sort,
1834
+ limit: args.limit || 10
1835
+ });
1836
+ if (results.length === 0) {
1837
+ const hint = args.query ? ` for "${args.query}"` : "";
1838
+ return {
1839
+ content: [{ type: "text", text: `No skills found in marketplace${hint}.` }]
1840
+ };
1841
+ }
1842
+ let response = "## Skill Marketplace\n\n";
1843
+ for (const s of results) {
1844
+ const emoji = s.emoji ? `${s.emoji} ` : "";
1845
+ const rating = s.avgRating ? ` \u2605${s.avgRating}` : "";
1846
+ const installs = s.installCount > 0 ? ` (${s.installCount} installs)` : "";
1847
+ const author = s.authorName ? ` by ${s.authorName}` : "";
1848
+ response += `- **${emoji}${s.name}** v${s.version}${author}${installs}${rating}
1849
+ `;
1850
+ response += ` ${s.description}
1851
+ `;
1852
+ response += ` ID: \`${s.id}\`
1853
+
1854
+ `;
1855
+ }
1856
+ response += "Use `skill_add` with the skill ID to add any of these to your collection.";
1857
+ return { content: [{ type: "text", text: response }] };
1858
+ }
1859
+ ),
1860
+ tool2(
1861
+ "skill_add",
1862
+ "Add a skill to your personal collection. Works for both marketplace skills and newly created drafts. This is the approval step \u2014 after adding, the skill becomes available for use via skill_invoke.",
1863
+ {
1864
+ skill_id: z2.string().describe("The skill UUID (from skill_browse or skill_create results)")
1865
+ },
1866
+ async (args) => {
1867
+ const added = await skillManager.addSkill(args.skill_id);
1868
+ if (!added) {
1869
+ return {
1870
+ content: [
1871
+ { type: "text", text: `Failed to add skill. Check that the ID is correct.` }
1872
+ ]
1873
+ };
1874
+ }
1875
+ const emoji = added.metadata.emoji ? `${added.metadata.emoji} ` : "";
1876
+ return {
1877
+ content: [
1878
+ {
1879
+ type: "text",
1880
+ text: `Added **${emoji}${added.name}** v${added.version} to your collection. It's now available for use via skill_invoke.`
1881
+ }
1882
+ ]
1883
+ };
1884
+ }
1885
+ ),
1886
+ tool2(
1887
+ "skill_publish",
1888
+ "Publish one of your skills to the marketplace so others can discover and install it.",
1889
+ {
1890
+ name: z2.string().describe("Name of your skill to publish"),
1891
+ category: z2.string().optional().describe("Category (e.g. 'productivity', 'ecommerce', 'dev-tools')"),
1892
+ author_name: z2.string().optional().describe("Your display name as the author")
1893
+ },
1894
+ async (args) => {
1895
+ const skill = skillManager.get(args.name);
1896
+ if (!skill) {
1897
+ return {
1898
+ content: [
1899
+ { type: "text", text: `Skill "${args.name}" not found in your collection.` }
1900
+ ]
1901
+ };
1902
+ }
1903
+ if (skill.source === "external") {
1904
+ return {
1905
+ content: [{ type: "text", text: `Cannot publish external skills.` }]
1906
+ };
1907
+ }
1908
+ const result = await skillManager.publish(args.name, {
1909
+ category: args.category,
1910
+ authorName: args.author_name
1911
+ });
1912
+ if (!result) {
1913
+ return {
1914
+ content: [
1915
+ {
1916
+ type: "text",
1917
+ text: `Failed to publish "${args.name}". The name may already be taken by another author.`
1918
+ }
1919
+ ]
1920
+ };
1921
+ }
1922
+ return {
1923
+ content: [
1924
+ {
1925
+ type: "text",
1926
+ text: `Skill "${args.name}" published to the marketplace! Others can now find and install it.`
1927
+ }
1928
+ ]
1929
+ };
1930
+ }
1931
+ ),
1932
+ // ── User Interaction Tool ───────────────────────────────────
1933
+ tool2(
1934
+ "ask_user",
1935
+ "Ask the user a question via the web UI and wait for their response. Shows a message with optional predefined option buttons PLUS a free-text input field \u2014 the user can either click a suggested option or type a custom answer. ALWAYS provide options when you can suggest likely answers. Do NOT use this for information you can discover yourself (git remote, file contents, etc.).",
1936
+ {
1937
+ question: z2.string().describe(
1938
+ "The question to ask (supports markdown). Be specific about what you need and why."
1939
+ ),
1940
+ options: z2.array(
1941
+ z2.object({
1942
+ label: z2.string().describe("Button label shown to user"),
1943
+ action_key: z2.string().describe("Machine-readable key returned when selected"),
1944
+ description: z2.string().optional().describe("Tooltip/description for this option")
1945
+ })
1946
+ ).optional().describe(
1947
+ "Suggested options shown as buttons. The user can always type a custom answer instead."
1948
+ ),
1949
+ placeholder: z2.string().optional().describe("Placeholder text for the free-text input field"),
1950
+ timeout_seconds: z2.number().optional().describe("How long to wait for response (default: 300)")
1951
+ },
1952
+ async (args) => {
1953
+ const actionId = `ask_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1954
+ const timeout = (args.timeout_seconds || 300) * 1e3;
1955
+ const actionData = {
1956
+ id: actionId,
1957
+ type: "ask_user",
1958
+ message: args.question,
1959
+ options: args.options || [],
1960
+ placeholder: args.placeholder || "",
1961
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1962
+ };
1963
+ try {
1964
+ await setActionRequest(taskId, actionData);
1965
+ log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
1966
+ await emitEvent(taskId, "user_action_request", actionData);
1967
+ await emitEvent(taskId, "status_change", {
1968
+ status: "waiting_for_user",
1969
+ message: args.question
1970
+ });
1971
+ onUserWaitStart?.();
1972
+ const startTime = Date.now();
1973
+ const pollInterval = 2e3;
1974
+ try {
1975
+ while (Date.now() - startTime < timeout) {
1976
+ const response = await pollActionResponse(taskId);
1977
+ if (response && (!response.action_id || response.action_id === actionId)) {
1978
+ const actionKey = response.action_key || "";
1979
+ const text = response.text || "";
1980
+ const label = response.label || actionKey || text;
1981
+ log.info(`User responded: "${label}"`);
1982
+ return {
1983
+ content: [
1984
+ {
1985
+ type: "text",
1986
+ text: JSON.stringify({
1987
+ status: "responded",
1988
+ action_key: actionKey || "custom_input",
1989
+ label,
1990
+ text: text || label
1991
+ })
1992
+ }
1993
+ ]
1994
+ };
1995
+ }
1996
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
1997
+ }
1998
+ log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
1999
+ return {
2000
+ content: [
2001
+ {
2002
+ type: "text",
2003
+ text: JSON.stringify({
2004
+ status: "timeout",
2005
+ message: "User did not respond within the timeout period. Continue the task with a reasonable default or skip the step that required user input."
2006
+ })
2007
+ }
2008
+ ]
2009
+ };
2010
+ } finally {
2011
+ onUserWaitEnd?.();
2012
+ }
2013
+ } catch (err) {
2014
+ log.error(`ask_user failed: ${err}`);
2015
+ onUserWaitEnd?.();
2016
+ return {
2017
+ content: [
2018
+ {
2019
+ type: "text",
2020
+ text: `Failed to ask user: ${err instanceof Error ? err.message : err}`
2021
+ }
2022
+ ]
2023
+ };
2024
+ }
2025
+ }
2026
+ ),
2027
+ // ── Job Automation Tools ──────────────────────────────────────
2028
+ tool2(
2029
+ "job_run",
2030
+ "Run a job by loading its goal and available skills as capabilities. You then decide dynamically which skills to use, in what order, and how to chain them based on what you discover. Use this when the user asks to run their job, or when a scheduled job fires.",
2031
+ {
2032
+ job_name: z2.string().describe("Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')")
2033
+ },
2034
+ async (args) => {
2035
+ const runner = new JobRunner();
2036
+ const job = await runner.loadJob(args.job_name);
2037
+ if (!job) {
2038
+ const jobs = await runner.listJobs();
2039
+ const available = jobs.length > 0 ? `Available jobs: ${jobs.map((j) => `"${j.name}" (${j.skillCount} skills)`).join(", ")}` : "No jobs defined yet. Use skill_generate to create a job from a job description.";
2040
+ return {
2041
+ content: [{ type: "text", text: `Job "${args.job_name}" not found. ${available}` }]
2042
+ };
2043
+ }
2044
+ if (job.skills.length === 0) {
2045
+ return {
2046
+ content: [
2047
+ {
2048
+ type: "text",
2049
+ text: `Job "${args.job_name}" has no linked skills. Use skill_generate to add skills to this job.`
2050
+ }
2051
+ ]
2052
+ };
2053
+ }
2054
+ const runId = await runner.createRun(job.jobId, {
2055
+ sessionId,
2056
+ messageId: taskId,
2057
+ triggerType: "manual"
2058
+ });
2059
+ if (!runId) {
2060
+ log.debug("Failed to create job run record, proceeding without tracking");
2061
+ }
2062
+ const prompt = runner.buildJobPrompt(job, runId || "untracked");
2063
+ log.info(
2064
+ `Job "${args.job_name}" started with ${job.skills.length} skills (run: ${runId?.slice(0, 8) || "untracked"})`
2065
+ );
2066
+ return {
2067
+ content: [{ type: "text", text: prompt }]
2068
+ };
2069
+ }
2070
+ ),
2071
+ tool2(
2072
+ "job_schedule",
2073
+ "Schedule a job to run automatically on a recurring basis using a cron expression. For example, schedule your 'software-engineer' job to run every morning at 9am.",
2074
+ {
2075
+ job_name: z2.string().describe("Name of the job to schedule"),
2076
+ cron: z2.string().describe(
2077
+ "Cron expression: 'minute hour day-of-month month day-of-week'. Examples: '0 9 * * *' (daily 9am), '0 9 * * 1-5' (weekdays 9am), '0 */2 * * *' (every 2 hours)"
2078
+ ),
2079
+ timezone: z2.string().optional().describe("Timezone (default: UTC). Examples: 'UTC', 'America/New_York'"),
2080
+ schedule_name: z2.string().optional().describe("Custom name for this schedule (default: 'Job: <job_name>')")
2081
+ },
2082
+ async (args) => {
2083
+ const runner = new JobRunner();
2084
+ const job = await runner.loadJob(args.job_name);
2085
+ if (!job) {
2086
+ return {
2087
+ content: [
2088
+ {
2089
+ type: "text",
2090
+ text: `Job "${args.job_name}" not found. Create it first with skill_generate.`
2091
+ }
2092
+ ]
2093
+ };
2094
+ }
2095
+ try {
2096
+ getNextRunTime(args.cron, args.timezone || "UTC");
2097
+ } catch {
2098
+ return {
2099
+ content: [
2100
+ {
2101
+ type: "text",
2102
+ text: `Invalid cron expression: "${args.cron}". Use format: "minute hour day-of-month month day-of-week"`
2103
+ }
2104
+ ]
2105
+ };
2106
+ }
2107
+ const name = args.schedule_name || `Job: ${args.job_name}`;
2108
+ const prompt = `[JobRun: ${args.job_name}] Run the "${args.job_name}" job. Use job_run to execute it.`;
2109
+ const tz = args.timezone || "UTC";
2110
+ try {
2111
+ const task = await createScheduledTask(name, prompt, args.cron, tz);
2112
+ await callMcpHandler("schedule.link_job", {
2113
+ task_id: task.id,
2114
+ job_id: job.jobId
2115
+ });
2116
+ const nextRun = task.next_run_at ? new Date(task.next_run_at).toLocaleString() : "calculating...";
2117
+ let response = `## Job Scheduled: ${args.job_name}
2118
+
2119
+ `;
2120
+ response += `- **Schedule:** ${args.cron} (${tz})
2121
+ `;
2122
+ response += `- **Next run:** ${nextRun}
2123
+ `;
2124
+ response += `- **Skills:** ${job.skills.length}
2125
+
2126
+ `;
2127
+ response += `The job "${args.job_name}" will automatically run on this schedule. `;
2128
+ response += `Each run will execute ${job.skills.length} skills in sequence:
2129
+ `;
2130
+ for (const skill of job.skills) {
2131
+ const emoji = skill.skillEmoji ? `${skill.skillEmoji} ` : "";
2132
+ response += ` - ${emoji}${skill.skillName}
2133
+ `;
2134
+ }
2135
+ log.success(`Job "${args.job_name}" scheduled: ${args.cron}`);
2136
+ return { content: [{ type: "text", text: response }] };
2137
+ } catch (err) {
2138
+ return {
2139
+ content: [
2140
+ {
2141
+ type: "text",
2142
+ text: `Failed to schedule job: ${err instanceof Error ? err.message : err}`
2143
+ }
2144
+ ]
2145
+ };
2146
+ }
2147
+ }
2148
+ ),
2149
+ tool2(
2150
+ "job_status",
2151
+ "Check the status and run history of a job. Shows recent executions, success rates, and details.",
2152
+ {
2153
+ job_name: z2.string().optional().describe("Job name to check (omit for all jobs)"),
2154
+ limit: z2.number().optional().describe("Max number of runs to show (default: 5)")
2155
+ },
2156
+ async (args) => {
2157
+ const runner = new JobRunner();
2158
+ if (!args.job_name) {
2159
+ const jobs = await runner.listJobs();
2160
+ if (jobs.length === 0) {
2161
+ return {
2162
+ content: [
2163
+ {
2164
+ type: "text",
2165
+ text: "No jobs defined. Use skill_generate to create a job from your job description."
2166
+ }
2167
+ ]
2168
+ };
2169
+ }
2170
+ let response2 = "## Your Jobs\n\n";
2171
+ for (const job of jobs) {
2172
+ response2 += `- **${job.name}** (${job.skillCount} skills): ${job.description.slice(0, 100)}
2173
+ `;
2174
+ }
2175
+ response2 += "\nUse `job_run` to execute a job, or `job_schedule` to automate it.";
2176
+ return { content: [{ type: "text", text: response2 }] };
2177
+ }
2178
+ const runs = await runner.getRunHistory(args.job_name, args.limit || 5);
2179
+ if (runs.length === 0) {
2180
+ return {
2181
+ content: [
2182
+ {
2183
+ type: "text",
2184
+ text: `No runs found for job "${args.job_name}". Use job_run to execute it.`
2185
+ }
2186
+ ]
2187
+ };
2188
+ }
2189
+ let response = `## Job Status: ${args.job_name}
2190
+
2191
+ `;
2192
+ response += `### Recent Runs
2193
+
2194
+ `;
2195
+ for (const run of runs) {
2196
+ const statusIcon = run.status === "completed" ? "+" : run.status === "failed" ? "x" : run.status === "running" ? "~" : "-";
2197
+ const date = new Date(run.startedAt).toLocaleString();
2198
+ const duration = run.completedAt ? `${Math.round((new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()) / 1e3)}s` : "in progress";
2199
+ response += `[${statusIcon}] ${date} | ${run.triggerType} | ${duration}
2200
+ `;
2201
+ if (run.summary) {
2202
+ response += ` ${run.summary.slice(0, 100)}
2203
+ `;
2204
+ }
2205
+ }
2206
+ const total = runs.length;
2207
+ const completed = runs.filter((r) => r.status === "completed").length;
2208
+ const failed = runs.filter((r) => r.status === "failed").length;
2209
+ response += `
2210
+ **Stats:** ${completed}/${total} successful`;
2211
+ if (failed > 0) response += `, ${failed} failed`;
2212
+ response += "\n";
2213
+ return { content: [{ type: "text", text: response }] };
2214
+ }
2215
+ ),
2216
+ // ── Credential Tools ──────────────────────────────────────────
2217
+ tool2(
2218
+ "credential_get",
2219
+ "Retrieve a locally stored credential by name. Returns the secret data (API keys, tokens, etc.) stored on the user's machine. Use this when a skill needs authentication or API access.",
2220
+ {
2221
+ name: z2.string().describe("Credential name (e.g. 'amazon-login', 'openai-api-key')")
2222
+ },
2223
+ async (args) => {
2224
+ const store = getCredentialStore();
2225
+ const credential = store.getByName(args.name);
2226
+ if (!credential) {
2227
+ const all = store.list();
2228
+ const available = all.length > 0 ? `Available credentials: ${all.map((m) => m.name).join(", ")}` : "No credentials stored yet.";
2229
+ return {
2230
+ content: [
2231
+ {
2232
+ type: "text",
2233
+ text: `Credential "${args.name}" not found. ${available}
2234
+ Use ask_user to request it from the user, or create it yourself (e.g. register an account), then store with credential_set.`
2235
+ }
2236
+ ]
2237
+ };
2238
+ }
2239
+ log.info(`Credential accessed: "${args.name}" (${credential.meta.type})`);
2240
+ return {
2241
+ content: [
2242
+ {
2243
+ type: "text",
2244
+ text: JSON.stringify({
2245
+ name: credential.meta.name,
2246
+ type: credential.meta.type,
2247
+ data: credential.data,
2248
+ skill: credential.meta.skillName || null
2249
+ })
2250
+ }
2251
+ ]
2252
+ };
2253
+ }
2254
+ ),
2255
+ tool2(
2256
+ "credential_set",
2257
+ "Store a credential locally on the user's machine. The credential is encrypted at rest and never sent to any remote server. IMPORTANT: Always use this to persist credentials \u2014 both when receiving them from the user via ask_user AND when you generate new credentials yourself (e.g. registering an account, creating an API key, generating a token). This ensures credentials survive across sessions and don't need to be recreated.",
2258
+ {
2259
+ name: z2.string().describe("Credential name (lowercase kebab-case, e.g. 'amazon-login')"),
2260
+ type: z2.enum(["api_key", "oauth_token", "login", "secret", "custom"]).describe("Credential type"),
2261
+ data: z2.record(z2.string(), z2.string()).describe(
2262
+ 'Key-value pairs (e.g. { "username": "...", "password": "..." } or { "api_key": "..." })'
2263
+ ),
2264
+ skill_name: z2.string().optional().describe("Associate with a specific skill"),
2265
+ tags: z2.array(z2.string()).optional().describe("Tags for searchability")
2266
+ },
2267
+ async (args) => {
2268
+ const store = getCredentialStore();
2269
+ const meta = store.save(args.name, args.type, args.data, {
2270
+ skillName: args.skill_name,
2271
+ tags: args.tags
2272
+ });
2273
+ log.info(`Credential stored: "${args.name}" (${args.type})`);
2274
+ return {
2275
+ content: [
2276
+ {
2277
+ type: "text",
2278
+ text: `Credential "${meta.name}" stored locally (type: ${meta.type}, id: ${meta.id}). It is encrypted and will persist across app restarts.`
2279
+ }
2280
+ ]
2281
+ };
2282
+ }
2283
+ ),
2284
+ tool2(
2285
+ "credential_list",
2286
+ "List all locally stored credentials (metadata only, no secrets). Use this to check what credentials are available before executing a skill.",
2287
+ {
2288
+ skill_name: z2.string().optional().describe("Filter by skill name"),
2289
+ type: z2.string().optional().describe("Filter by credential type")
2290
+ },
2291
+ async (args) => {
2292
+ const store = getCredentialStore();
2293
+ let results = store.list();
2294
+ if (args.skill_name) {
2295
+ results = results.filter((m) => m.skillName === args.skill_name);
2296
+ }
2297
+ if (args.type) {
2298
+ results = results.filter((m) => m.type === args.type);
2299
+ }
2300
+ if (results.length === 0) {
2301
+ const filter = args.skill_name ? ` for skill "${args.skill_name}"` : "";
2302
+ return {
2303
+ content: [{ type: "text", text: `No credentials found${filter}.` }]
2304
+ };
2305
+ }
2306
+ let response = "## Stored Credentials\n\n";
2307
+ for (const m of results) {
2308
+ const skill = m.skillName ? ` [${m.skillName}]` : "";
2309
+ const tags = m.tags.length > 0 ? ` (${m.tags.join(", ")})` : "";
2310
+ response += `- **${m.name}** (${m.type})${skill}${tags}
2311
+ `;
2312
+ }
2313
+ return { content: [{ type: "text", text: response }] };
2314
+ }
2315
+ ),
2316
+ tool2(
2317
+ "credential_remove",
2318
+ "Remove a locally stored credential by name.",
2319
+ {
2320
+ name: z2.string().describe("Credential name to remove")
2321
+ },
2322
+ async (args) => {
2323
+ const store = getCredentialStore();
2324
+ const removed = store.removeByName(args.name);
2325
+ if (!removed) {
2326
+ return {
2327
+ content: [{ type: "text", text: `Credential "${args.name}" not found.` }]
2328
+ };
2329
+ }
2330
+ log.info(`Credential removed: "${args.name}"`);
2331
+ return {
2332
+ content: [
2333
+ { type: "text", text: `Credential "${args.name}" removed from local storage.` }
2334
+ ]
2335
+ };
2336
+ }
2337
+ ),
2338
+ // ── Program Tools ─────────────────────────────────────────────
2339
+ tool2(
2340
+ "program_register",
2341
+ "Register a program (script, CLI tool, local program) that was created to solve a task. This stores metadata about the program so it appears in the desktop app's Programs list and can be reused in future tasks. Call this after you've written the code and verified it works.",
2342
+ {
2343
+ name: z2.string().describe("Program name in kebab-case (e.g. 'github-issues-viewer')"),
2344
+ description: z2.string().describe("What this program does \u2014 one sentence"),
2345
+ directory: z2.string().describe("Absolute path to the program directory"),
2346
+ language: z2.string().optional().describe("Primary language (e.g. 'typescript', 'python', 'shell')"),
2347
+ skill_name: z2.string().optional().describe("Associated skill name if applicable"),
2348
+ tags: z2.array(z2.string()).optional().describe("Tags for discoverability"),
2349
+ metadata: z2.record(z2.string(), z2.string()).optional().describe("Extra metadata (e.g. { entry_point: 'index.ts', run_command: 'npm start' })")
2350
+ },
2351
+ async (args) => {
2352
+ const store = getProgramStore();
2353
+ const meta = store.save(args.name, args.description, args.directory, {
2354
+ language: args.language,
2355
+ skillName: args.skill_name,
2356
+ tags: args.tags,
2357
+ metadata: args.metadata
2358
+ });
2359
+ try {
2360
+ await callMcpHandler("program.register", {
2361
+ id: meta.id,
2362
+ name: meta.name,
2363
+ description: meta.description,
2364
+ language: meta.language,
2365
+ directory: meta.directory,
2366
+ skill_name: meta.skillName,
2367
+ tags: meta.tags,
2368
+ metadata: meta.metadata
2369
+ });
2370
+ } catch {
2371
+ log.debug("Program Supabase sync skipped (edge function may not exist yet)");
2372
+ }
2373
+ log.info(`Program registered: "${args.name}" at ${args.directory}`);
2374
+ return {
2375
+ content: [
2376
+ {
2377
+ type: "text",
2378
+ text: `Program "${meta.name}" registered (id: ${meta.id}, language: ${meta.language}, dir: ${meta.directory}). It will appear in the desktop app's Programs list.`
2379
+ }
2380
+ ]
2381
+ };
2382
+ }
2383
+ ),
2384
+ tool2(
2385
+ "program_list",
2386
+ "List all registered programs. Use this to check if a reusable program already exists before creating a new one for a task.",
2387
+ {
2388
+ skill_name: z2.string().optional().describe("Filter by associated skill"),
2389
+ status: z2.string().optional().describe("Filter by status: 'active' or 'archived'")
2390
+ },
2391
+ async (args) => {
2392
+ const store = getProgramStore();
2393
+ const results = store.list({
2394
+ skillName: args.skill_name,
2395
+ status: args.status
2396
+ });
2397
+ if (results.length === 0) {
2398
+ return {
2399
+ content: [
2400
+ {
2401
+ type: "text",
2402
+ text: "No programs registered yet. Create a program and use program_register to register it."
2403
+ }
2404
+ ]
2405
+ };
2406
+ }
2407
+ let response = "## Registered Programs\n\n";
2408
+ for (const p of results) {
2409
+ const skill = p.skillName ? ` [skill: ${p.skillName}]` : "";
2410
+ const tags = p.tags.length > 0 ? ` (${p.tags.join(", ")})` : "";
2411
+ const runCmd = p.metadata.run_command ? ` | Run: \`${p.metadata.run_command}\`` : "";
2412
+ response += `- **${p.name}** (${p.language})${skill}${tags}
2413
+ `;
2414
+ response += ` ${p.description}
2415
+ `;
2416
+ response += ` Dir: \`${p.directory}\`${runCmd}
2417
+
2418
+ `;
2419
+ }
2420
+ return { content: [{ type: "text", text: response }] };
2421
+ }
2422
+ ),
2423
+ tool2(
2424
+ "program_run",
2425
+ "Execute a registered program. Looks up the program's directory and run command, then executes it. The program must have a 'run_command' in its metadata (set during registration). Use program_list first to find available programs and their run commands.",
2426
+ {
2427
+ name: z2.string().describe("Program name to run"),
2428
+ args: z2.string().optional().describe("Additional CLI arguments to append to the run command")
2429
+ },
2430
+ async (toolArgs) => {
2431
+ const store = getProgramStore();
2432
+ const program = store.getByName(toolArgs.name);
2433
+ if (!program) {
2434
+ const available = store.list({ status: "active" });
2435
+ const names = available.length > 0 ? `Available programs: ${available.map((p) => p.name).join(", ")}` : "No programs registered yet.";
2436
+ return {
2437
+ content: [
2438
+ {
2439
+ type: "text",
2440
+ text: `Program "${toolArgs.name}" not found. ${names}`
2441
+ }
2442
+ ]
2443
+ };
2444
+ }
2445
+ const runCommand = program.metadata.run_command || null;
2446
+ if (!runCommand) {
2447
+ return {
2448
+ content: [
2449
+ {
2450
+ type: "text",
2451
+ text: `Program "${toolArgs.name}" has no run_command in its metadata. Re-register with metadata: { run_command: "npm start" } or similar. Alternatively, use execute_command directly with dir: "${program.directory}".`
2452
+ }
2453
+ ]
2454
+ };
2455
+ }
2456
+ const fullCommand = toolArgs.args ? `${runCommand} ${toolArgs.args}` : runCommand;
2457
+ log.info(`Running program "${toolArgs.name}": ${fullCommand} (in ${program.directory})`);
2458
+ try {
2459
+ const output = await executeShell(fullCommand, program.directory);
2460
+ return {
2461
+ content: [
2462
+ {
2463
+ type: "text",
2464
+ text: `## ${program.name} output
2465
+
2466
+ \`\`\`
2467
+ ${output}
2468
+ \`\`\``
2469
+ }
2470
+ ]
2471
+ };
2472
+ } catch (err) {
2473
+ const message = err instanceof Error ? err.message : String(err);
2474
+ return {
2475
+ content: [
2476
+ {
2477
+ type: "text",
2478
+ text: `Program "${toolArgs.name}" execution failed:
2479
+ ${message}`
2480
+ }
2481
+ ]
2482
+ };
2483
+ }
2484
+ }
2485
+ ),
2486
+ tool2(
2487
+ "program_remove",
2488
+ "Remove a registered program by name. This only removes the registration \u2014 the actual code files on disk are NOT deleted.",
2489
+ {
2490
+ name: z2.string().describe("Program name to remove")
2491
+ },
2492
+ async (args) => {
2493
+ const store = getProgramStore();
2494
+ const removed = store.removeByName(args.name);
2495
+ if (!removed) {
2496
+ return {
2497
+ content: [{ type: "text", text: `Program "${args.name}" not found.` }]
2498
+ };
2499
+ }
2500
+ log.info(`Program removed: "${args.name}"`);
2501
+ return {
2502
+ content: [
2503
+ {
2504
+ type: "text",
2505
+ text: `Program "${args.name}" removed from registry. Code files on disk were NOT deleted.`
2506
+ }
2507
+ ]
2508
+ };
2509
+ }
2510
+ ),
2511
+ // ── Heartbeat Tools ──────────────────────────────────────────
2512
+ tool2(
2513
+ "heartbeat_status",
2514
+ "Get the current status of the heartbeat engine. Shows whether it's enabled, the interval, cycle count, and whether a HEARTBEAT.md checklist exists.",
2515
+ {},
2516
+ async () => {
2517
+ if (!heartbeatEngine) {
2518
+ return {
2519
+ content: [{ type: "text", text: "HeartbeatEngine is not available in this session." }]
2520
+ };
2521
+ }
2522
+ const status = heartbeatEngine.getStatus();
2523
+ const lines = [
2524
+ `Enabled: ${status.enabled ? "yes" : "no"}`,
2525
+ `Interval: ${status.intervalMs / 1e3}s (${(status.intervalMs / 6e4).toFixed(1)} min)`,
2526
+ `Cycles completed: ${status.cycleCount}`,
2527
+ `HEARTBEAT.md: ${status.hasChecklist ? "found" : "not found"}`,
2528
+ `Currently executing: ${status.executing ? "yes" : "no"}`
2529
+ ];
2530
+ return {
2531
+ content: [{ type: "text", text: lines.join("\n") }]
2532
+ };
2533
+ }
2534
+ ),
2535
+ tool2(
2536
+ "heartbeat_enable",
2537
+ "Enable the heartbeat engine. When enabled, the agent periodically reads HEARTBEAT.md and uses LLM to check each item.",
2538
+ {},
2539
+ async () => {
2540
+ if (!heartbeatEngine) {
2541
+ return {
2542
+ content: [{ type: "text", text: "HeartbeatEngine is not available in this session." }]
2543
+ };
2544
+ }
2545
+ heartbeatEngine.setEnabled(true);
2546
+ return {
2547
+ content: [{ type: "text", text: "Heartbeat enabled." }]
2548
+ };
2549
+ }
2550
+ ),
2551
+ tool2(
2552
+ "heartbeat_disable",
2553
+ "Disable the heartbeat engine. Monitoring is paused but the engine keeps running (can be re-enabled).",
2554
+ {},
2555
+ async () => {
2556
+ if (!heartbeatEngine) {
2557
+ return {
2558
+ content: [{ type: "text", text: "HeartbeatEngine is not available in this session." }]
2559
+ };
2560
+ }
2561
+ heartbeatEngine.setEnabled(false);
2562
+ return {
2563
+ content: [{ type: "text", text: "Heartbeat disabled. Monitoring paused." }]
2564
+ };
2565
+ }
2566
+ ),
2567
+ tool2(
2568
+ "heartbeat_interval",
2569
+ "Set the heartbeat interval. Controls how often the agent wakes up to check the HEARTBEAT.md checklist.",
2570
+ {
2571
+ seconds: z2.number().min(30).describe("Interval in seconds (minimum 30)")
2572
+ },
2573
+ async (args) => {
2574
+ if (!heartbeatEngine) {
2575
+ return {
2576
+ content: [{ type: "text", text: "HeartbeatEngine is not available in this session." }]
2577
+ };
2578
+ }
2579
+ heartbeatEngine.setIntervalMs(args.seconds * 1e3);
2580
+ return {
2581
+ content: [
2582
+ {
2583
+ type: "text",
2584
+ text: `Heartbeat interval set to ${args.seconds}s (${(args.seconds / 60).toFixed(1)} min).`
2585
+ }
2586
+ ]
2587
+ };
2588
+ }
2589
+ ),
2590
+ tool2(
2591
+ "heartbeat_log",
2592
+ "Read recent heartbeat log entries to see past cycle outcomes, timing, and costs.",
2593
+ {
2594
+ limit: z2.number().optional().describe("Number of entries to return (default: 20)")
2595
+ },
2596
+ async (args) => {
2597
+ if (!heartbeatEngine) {
2598
+ return {
2599
+ content: [{ type: "text", text: "HeartbeatEngine is not available in this session." }]
2600
+ };
2601
+ }
2602
+ const entries = await heartbeatEngine.readLog(args.limit ?? 20);
2603
+ if (entries.length === 0) {
2604
+ return {
2605
+ content: [{ type: "text", text: "No heartbeat log entries yet." }]
2606
+ };
2607
+ }
2608
+ const lines = entries.map((e) => {
2609
+ const icon = e.outcome === "ok" ? "\u2713" : e.outcome === "action_taken" ? "!" : "\u2717";
2610
+ const cost = e.costUsd ? ` $${e.costUsd.toFixed(4)}` : "";
2611
+ return `${icon} #${e.cycle} ${e.timestamp} (${e.durationMs}ms${cost}) \u2014 ${e.summary.slice(0, 120)}`;
2612
+ });
2613
+ return {
2614
+ content: [{ type: "text", text: lines.join("\n") }]
2615
+ };
2616
+ }
2617
+ )
2618
+ ]
2619
+ });
2620
+ }
2621
+ async function saveJobToDb(jobName, jobDescription, createdSkillNames) {
2622
+ try {
2623
+ const data = await callMcpHandler("job.save_with_skills", {
2624
+ job_name: jobName,
2625
+ job_description: jobDescription,
2626
+ skill_names: createdSkillNames
2627
+ });
2628
+ log.debug(
2629
+ `Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`
2630
+ );
2631
+ } catch (err) {
2632
+ log.debug(`saveJobToDb error: ${err}`);
2633
+ }
2634
+ }
2635
+
2636
+ // src/agent/event-hooks.ts
2637
+ function stripMcpPrefix(toolName) {
2638
+ const match = toolName.match(/^mcp__[^_]+(?:__)?(.+)$/);
2639
+ return match ? match[1] : toolName;
2640
+ }
2641
+ function createEventHooks(taskId, toolCallRecords, toolFailures = []) {
2642
+ const preToolUseHook = async (input) => {
2643
+ if (input.hook_event_name !== "PreToolUse") return { continue: true };
2644
+ const preInput = input;
2645
+ const rawName = preInput.tool_name;
2646
+ const displayName = stripMcpPrefix(rawName);
2647
+ const toolInput = preInput.tool_input;
2648
+ log.tool(displayName, JSON.stringify(toolInput).slice(0, MAX_TOOL_INPUT_LOG_LENGTH));
2649
+ await emitEvent(taskId, "tool_use_start", { name: displayName });
2650
+ await emitEvent(taskId, "tool_use_input", { input: toolInput });
2651
+ if (displayName === "browser_request_user_action") {
2652
+ await emitEvent(taskId, "status_change", {
2653
+ status: "waiting_for_user",
2654
+ message: toolInput?.message
2655
+ });
2656
+ }
2657
+ return { continue: true };
2658
+ };
2659
+ const postToolUseHook = async (input) => {
2660
+ if (input.hook_event_name !== "PostToolUse") return {};
2661
+ const postInput = input;
2662
+ const rawName = postInput.tool_name;
2663
+ const displayName = stripMcpPrefix(rawName);
2664
+ const toolInput = postInput.tool_input;
2665
+ const toolResponse = postInput.tool_response;
2666
+ const resultStr = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
2667
+ log.result(resultStr.slice(0, MAX_TOOL_INPUT_LOG_LENGTH));
2668
+ await emitEvent(taskId, "tool_result", {
2669
+ name: displayName,
2670
+ result: resultStr.slice(0, MAX_TOOL_RESULT_LENGTH)
2671
+ });
2672
+ toolCallRecords.push({
2673
+ name: displayName,
2674
+ input: toolInput || {},
2675
+ result: resultStr.slice(0, MAX_SKILL_RECORD_RESULT_LENGTH)
2676
+ });
2677
+ return {};
2678
+ };
2679
+ const postToolUseFailureHook = async (input) => {
2680
+ if (input.hook_event_name !== "PostToolUseFailure") return {};
2681
+ const failureInput = input;
2682
+ const rawName = failureInput.tool_name;
2683
+ const displayName = stripMcpPrefix(rawName);
2684
+ const errorStr = failureInput.error;
2685
+ toolFailures.push({
2686
+ toolName: displayName,
2687
+ input: failureInput.tool_input || {},
2688
+ error: errorStr.slice(0, 500),
2689
+ timestamp: Date.now()
2690
+ });
2691
+ await emitEvent(taskId, "tool_failure", {
2692
+ name: displayName,
2693
+ error: errorStr.slice(0, 500),
2694
+ failure_count: toolFailures.filter((f) => f.toolName === displayName).length
2695
+ });
2696
+ log.warn(`Tool failure tracked: ${displayName} (total: ${toolFailures.length})`);
2697
+ return {};
2698
+ };
2699
+ return {
2700
+ PreToolUse: [{ hooks: [preToolUseHook] }],
2701
+ PostToolUse: [{ hooks: [postToolUseHook] }],
2702
+ PostToolUseFailure: [{ hooks: [postToolUseFailureHook] }]
2703
+ };
2704
+ }
2705
+
2706
+ // src/agent/system-prompt.ts
2707
+ var BASE_SYSTEM_PROMPT = `You are AssistMe, an AI-powered agentic assistant that operates on the user's computer. You can complete tasks through multiple approaches: writing code and building local programs, using CLI tools, controlling the user's real Chrome browser, and working with files directly.
2708
+
2709
+ KEY PRINCIPLE: You are an agentic agent \u2014 you choose the best approach for each task:
2710
+ - **Code & CLI approach**: Write scripts, install CLI tools (e.g. gh, aws, gcloud), call APIs programmatically. This is often more efficient, reusable, and saves tokens in future runs.
2711
+ - **Browser approach**: Control the user's real Chrome browser (with real cookies, logins, sessions) when a web UI is the only or simplest way.
2712
+ - **Hybrid approach**: Combine both \u2014 e.g., use an API for data retrieval but browser for UI-only operations.
2713
+
2714
+ When a task can be done BOTH via code/API and via browser, PROACTIVELY suggest the code approach:
2715
+ - Use ask_user to present both options, explaining:
2716
+ - Code/API: "Initial setup takes a bit longer (install CLI, configure API key), but subsequent runs are faster, reusable, and save tokens."
2717
+ - Browser: "Quick to start, but slower for repeated tasks and uses more tokens each time."
2718
+ - Tell the user exactly what credentials/keys are needed and how to obtain them.
2719
+ - If the user chooses code, create a local project directory under the workspace, write the code, write a README with setup instructions (including how to get API keys), and register it as a program via program_register.
2720
+ - Store any required API keys/tokens using credential_set.
2721
+
2722
+ Browser capabilities (user's real Chrome via CDP):
2723
+ - The browser has the user's real cookies, logins, and sessions
2724
+ - When you navigate to amazon.com, you see the user's logged-in Amazon
2725
+ - If a site needs login, the browser will auto-detect the login page and prompt the user
2726
+ - After the user logs in, their session is saved in the persistent browser profile (~/.assistme/browser-profile)
2727
+ - Saved sessions persist across assistme restarts \u2014 the user only needs to log in once per site
2728
+ - Chrome is automatically managed \u2014 just call browser_connect and it will auto-launch if needed
2729
+ - NEVER ask the user to manually start Chrome or run any terminal commands for browser setup
2730
+
2731
+ Available capabilities:
2732
+ 1. CODE EXECUTION & LOCAL PROGRAMS:
2733
+ - Write scripts (Node.js, Python, shell, etc.) to solve tasks programmatically
2734
+ - Install and use CLI tools (gh, aws, gcloud, jq, curl, etc.) via Bash
2735
+ - Build reusable local programs that the user can run again later
2736
+ - Register completed programs via program_register so they appear in the desktop app
2737
+ - Store API keys and secrets using credential_set (encrypted on disk)
2738
+ - When creating a program:
2739
+ 1. Create a directory under the workspace (e.g. {workspace_path}/programs/<project-name>/)
2740
+ 2. Write the code, a README.md (with setup instructions, how to get API keys, usage examples)
2741
+ 3. If credentials are needed, use ask_user to tell the user what's needed and how to get them
2742
+ 4. Once credentials are provided, store them via credential_set
2743
+ 5. Register the project via program_register with name, description, directory path, and language
2744
+
2745
+ 2. BROWSER CONTROL (user's real Chrome via CDP):
2746
+ **PREFERRED workflow \u2014 Snapshot + Act (ref-based):**
2747
+ - browser_snapshot \u2192 takes a screenshot and discovers all interactive elements with numbered refs
2748
+ Returns a ref table (text) + screenshot (image). The ref table is your PRIMARY context for element identification.
2749
+ Use annotate=true only on simple pages (few elements) where visual badge overlay helps.
2750
+ - browser_act \u2192 execute actions using ref numbers: click, type, select, press, scroll, wait
2751
+ - This is MORE RELIABLE than CSS selectors because:
2752
+ (a) The ref table gives you role, name, and type for every interactive element \u2014 no guessing
2753
+ (b) Refs use stable semantic resolution (role + accessible name) that survives DOM changes
2754
+ (c) Actions use CDP Input events (real mouse/keyboard) instead of JavaScript \u2014 works with all frameworks
2755
+ (d) You can batch multiple actions in one call \u2014 fewer round-trips
2756
+ - Example workflow:
2757
+ 1. browser_snapshot \u2192 ref table shows [1] button "Next", [2] textbox "Email", [3] combobox "Month"
2758
+ 2. browser_act actions=[{action:"type", ref:2, text:"user@example.com"}, {action:"select", ref:3, option:"March"}, {action:"click", ref:1}] screenshot=true
2759
+ - Refs persist across actions unless the page navigates. Re-snapshot after navigation or major DOM changes.
2760
+
2761
+ **Legacy tools (still available, use when refs don't work):**
2762
+ - browser_click, browser_type, browser_select, browser_screenshot, browser_evaluate
2763
+ - browser_click supports :contains('text') pseudo-selectors
2764
+ - browser_select handles native and custom dropdowns
2765
+
2766
+ **Other browser tools:**
2767
+ - browser_connect, browser_navigate, browser_read_page, browser_list_tabs, browser_switch_tab, browser_new_tab
2768
+ - If auth is needed: use browser_request_user_action to ask the user to log in
2769
+
2770
+ 3. FILE OPERATIONS & SHELL:
2771
+ - Read, Write, Edit tools for file operations
2772
+ - Bash tool for shell commands
2773
+ - Glob and Grep for file search
2774
+
2775
+ 4. MEMORY & CREDENTIALS:
2776
+ - You can remember things about the user using memory_store
2777
+ - Use this when you learn preferences, important facts, or standing instructions
2778
+ - Your stored memories persist across conversations
2779
+ - PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
2780
+ - Before completing a task, consider if anything learned should be remembered for future conversations
2781
+ - CRITICAL \u2014 Credential Storage: When you create, register, or receive any account credentials (username, password, API keys, tokens), you MUST use credential_set to save them locally. NEVER use memory_store for credentials \u2014 memory_store is for preferences and facts, credential_set is for secrets. Examples:
2782
+ * After registering a new email/account \u2192 credential_set with type "login" and data { "username": "...", "password": "...", "email": "..." }
2783
+ * After generating an API key \u2192 credential_set with type "api_key" and data { "api_key": "..." }
2784
+ * Credentials saved via credential_set are encrypted on disk and viewable in the desktop app's Credentials panel
2785
+
2786
+ 5. SKILL-AWARE EXECUTION (CRITICAL \u2014 follow this for EVERY task):
2787
+ Step A \u2014 Search: Before executing ANY task, check if an existing skill matches (use skill_invoke or skill_search).
2788
+ Step B \u2014 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 \u2014 note what changed.
2789
+ Step C \u2014 If NO skill found: BEFORE executing, draft a skill plan following the Agent Skills format:
2790
+ Skill Draft: [kebab-case-name]
2791
+ Description: [what this skill does and when to use it]
2792
+ Steps:
2793
+ 1. [first step]
2794
+ 2. [second step]
2795
+ ...
2796
+ The draft should be a reusable workflow, not specific to this one request. Use generic placeholders where the user provided specific values.
2797
+ Step D \u2014 Execute: Follow the skill draft (or loaded skill) step by step. Refine the draft as you discover better approaches, edge cases, or missing steps.
2798
+ Step E \u2014 After execution: The system will automatically evaluate whether to save the skill. You do NOT need to call skill_create manually.
2799
+
2800
+ Agent Skills format reference (agentskills.io):
2801
+ - name: 1-64 chars, lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens
2802
+ - description: 1-1024 chars, describe what the skill does AND when to use it, include keywords for discoverability
2803
+ - body: markdown step-by-step instructions, examples, edge cases. Keep under 500 lines.
2804
+ - Progressive disclosure: metadata (~100 tokens) \u2192 instructions (<5000 tokens) \u2192 references (on demand)
2805
+
2806
+ 6. JOB AUTOMATION:
2807
+ - When the user describes their job/role/daily work, use skill_generate to decompose it into automatable skills
2808
+ - ALWAYS use ask_user to get user approval before creating skills \u2014 never create skills without approval
2809
+ - Use job_run to start a job \u2014 it gives you the job's goal and available skills as capabilities
2810
+ - When running a job, be AGENTIC: decide dynamically what to do based on what you discover
2811
+ - Do NOT follow a fixed sequence \u2014 if checking Slack reveals a task that needs GitHub, go do GitHub immediately
2812
+ - Chain actions intelligently: one skill's findings should inform your next move
2813
+ - Skip irrelevant skills, use tools directly when skills aren't needed
2814
+ - Use job_schedule for recurring automation (e.g., "run my SE job every weekday morning")
2815
+ - Use job_status to check run history
2816
+
2817
+ 7. HEARTBEAT MONITORING (HEARTBEAT.md):
2818
+ - You have a heartbeat engine that periodically reads {workspace_path}/HEARTBEAT.md
2819
+ - On each heartbeat cycle, the LLM reads the checklist, uses tools to check each item, and decides what needs attention
2820
+ - If everything is fine, the cycle silently responds HEARTBEAT_OK (the user sees nothing)
2821
+ - If something needs attention, the agent takes action and notifies the user
2822
+ - **When the user asks to monitor something** (e.g. "\u5E2E\u6211\u76D1\u63A7\u78C1\u76D8", "watch for new PRs", "alert me if server goes down"):
2823
+ 1. Read the current HEARTBEAT.md (create it if it doesn't exist)
2824
+ 2. Use the Write or Edit tool to add the new monitoring item to the checklist
2825
+ 3. Confirm to the user what you added \u2014 they do NOT need to edit the file themselves
2826
+ 4. The heartbeat engine will automatically pick up the changes on the next cycle
2827
+ - When the user asks to stop monitoring something, edit HEARTBEAT.md to remove that item
2828
+ - The user should NEVER need to manually edit HEARTBEAT.md \u2014 you do it for them via conversation
2829
+ - Use heartbeat_status to check the current heartbeat state
2830
+ - Use heartbeat_enable / heartbeat_disable to toggle monitoring
2831
+ - Use heartbeat_interval to change how often the heartbeat runs (default: 30 minutes)
2832
+ - Use heartbeat_log to see past cycle outcomes, timing, and costs
2833
+ - Heartbeat logs are saved to ~/.assistme/heartbeat-log.jsonl
2834
+
2835
+ 8. SKILL MARKETPLACE:
2836
+ - Use skill_browse to discover community-published skills
2837
+ - Use skill_add to add marketplace skills to the user's collection
2838
+ - Use skill_publish to share the user's skills with the community
2839
+
2840
+ Workflow for tasks that can be done via code/API:
2841
+ 1. Check if a program already exists for this type of task (program_list)
2842
+ 2. If yes: use the existing project (run its scripts, etc.)
2843
+ 3. If no: suggest code approach via ask_user, explaining benefits and required credentials
2844
+ 4. If user agrees: create project directory, write code + README, register via program_register
2845
+ 5. Execute and verify
2846
+
2847
+ Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7\u683C"):
2848
+ 1. browser_connect \u2192 connect to user's Chrome
2849
+ 2. browser_new_tab \u2192 open a new tab
2850
+ 3. browser_navigate \u2192 go to the website (login pages are auto-detected)
2851
+ 4. browser_snapshot \u2192 get ref table + screenshot (use annotate=true for simple pages)
2852
+ 5. browser_act \u2192 interact using refs (type, click, select, etc.), set screenshot=true to see result
2853
+ 6. Repeat 4-5 as needed (re-snapshot after navigation or major page changes)
2854
+ 7. Summarize findings
2855
+
2856
+ Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
2857
+ 1. browser_connect + browser_navigate \u2192 go to the form page
2858
+ 2. browser_snapshot \u2192 see all form fields with ref numbers
2859
+ 3. browser_act \u2192 batch fill multiple fields + click submit in ONE call:
2860
+ actions=[{action:"type", ref:1, text:"John"}, {action:"type", ref:2, text:"Doe"}, {action:"select", ref:3, option:"March"}, {action:"click", ref:7}] screenshot=true
2861
+ 4. Check the screenshot \u2014 if validation errors appear, re-snapshot and fix
2862
+ 5. When a username/email is taken, append a random 4-digit suffix and retry
2863
+
2864
+ 9. FAILURE RECOVERY \u2014 Strategy Switching:
2865
+ If a tool call fails, do NOT repeat the same call. Reflect on why it failed and switch strategy:
2866
+ - CSS selector fails \u2192 use browser_snapshot refs instead
2867
+ - Direct navigation fails \u2192 search for the page first
2868
+ - API/programmatic approach fails \u2192 use browser UI instead
2869
+ - Browser approach too slow \u2192 consider building a script/CLI solution
2870
+ - One data source fails \u2192 try an alternative source
2871
+ - If stuck after 2 failed attempts at the same step, try a fundamentally different approach
2872
+
2873
+ Guidelines:
2874
+ - SELF-VERIFY before finishing: re-read modified files, take a final screenshot after browser actions, or re-check output to confirm correctness. Never assume success without confirming the end state.
2875
+ - Choose the right tool for the job: code/API for repeatable tasks, browser for one-off web interactions
2876
+ - ALWAYS use browser_snapshot as your primary way to understand a page \u2014 the ref table gives actionable refs, the screenshot gives visual context
2877
+ - Use browser_act to batch multiple actions \u2014 fill an entire form in one call instead of individual clicks/types
2878
+ - Only re-snapshot when: (a) the page navigated, (b) significant DOM changes occurred, (c) an action failed with "ref not found"
2879
+ - Refs are semantically stable (resolved by role + name), so they often survive minor DOM updates
2880
+ - Login pages are auto-detected after navigation \u2014 the user is prompted and sessions are saved automatically
2881
+ - If auto-detection misses a login page, use browser_request_user_action manually
2882
+ - Fall back to legacy tools (browser_click, browser_type, browser_evaluate) only when refs don't work
2883
+ - Be thorough: check multiple sources when comparing prices/products
2884
+ - Summarize results clearly at the end
2885
+ - When you learn something about the user (preferences, habits), use memory_store to remember it
2886
+
2887
+ CRITICAL \u2014 Ask before you guess:
2888
+ - Before executing a task, verify you have all required information. If anything is ambiguous or missing, use ask_user to ask.
2889
+ - First try to resolve unknowns yourself: check memories, read workspace files (e.g. git remote, config files), or infer from conversation history.
2890
+ - If you still lack a critical piece of information after self-resolution, ASK the user via ask_user. Do NOT guess, assume defaults, or proceed with incomplete information.
2891
+ - When asking, provide suggested options as buttons whenever possible \u2014 the user can always type a custom answer instead.
2892
+ - 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.
2893
+ - Keep questions specific and actionable. Explain what you already know and what exactly you need.
2894
+ - After receiving the answer, store it with memory_store if it is likely to be useful in future conversations.
2895
+
2896
+ Workspace path: {workspace_path}`;
2897
+
2898
+ // src/agent/processor.ts
2899
+ var TaskTimeout = class {
2900
+ constructor(abortController, timeoutMs) {
2901
+ this.abortController = abortController;
2902
+ this.remainingMs = timeoutMs;
2903
+ this.resumedAt = Date.now();
2904
+ this.schedule();
2905
+ }
2906
+ timeoutId = null;
2907
+ remainingMs;
2908
+ resumedAt;
2909
+ schedule() {
2910
+ this.timeoutId = setTimeout(() => {
2911
+ this.abortController.abort();
2912
+ }, this.remainingMs);
2913
+ }
2914
+ /** Pause the timeout (e.g. while waiting for user). */
2915
+ pause() {
2916
+ if (this.timeoutId) {
2917
+ clearTimeout(this.timeoutId);
2918
+ this.timeoutId = null;
2919
+ const elapsed = Date.now() - this.resumedAt;
2920
+ this.remainingMs = Math.max(0, this.remainingMs - elapsed);
2921
+ }
2922
+ }
2923
+ /** Resume the timeout after user interaction completes. */
2924
+ resume() {
2925
+ this.resumedAt = Date.now();
2926
+ this.schedule();
2927
+ }
2928
+ clear() {
2929
+ if (this.timeoutId) {
2930
+ clearTimeout(this.timeoutId);
2931
+ this.timeoutId = null;
2932
+ }
2933
+ }
2934
+ };
2935
+ var TaskProcessor = class {
2936
+ memoryManager = null;
2937
+ skillManager;
2938
+ sessionId = null;
2939
+ userId = null;
2940
+ heartbeatEngine = null;
2941
+ /** In-memory conversation history, keyed by conversation_id */
2942
+ historyCache = /* @__PURE__ */ new Map();
2943
+ constructor() {
2944
+ this.skillManager = new SkillManager();
2945
+ }
2946
+ /** @deprecated Use setUserId() instead */
2947
+ init(userId) {
2948
+ this.setUserId(userId);
2949
+ }
2950
+ setUserId(userId) {
2951
+ this.userId = userId;
2952
+ this.memoryManager = new MemoryManager();
2953
+ this.skillManager.setUserId(userId);
2954
+ this.skillManager.loadFromDb().catch((err) => {
2955
+ log.debug(`DB skill load deferred: ${err}`);
2956
+ });
2957
+ }
2958
+ setSessionId(sessionId) {
2959
+ this.sessionId = sessionId;
2960
+ }
2961
+ setHeartbeatEngine(engine) {
2962
+ this.heartbeatEngine = engine;
2963
+ }
2964
+ /**
2965
+ * Post-task: resume the same Agent SDK session to evaluate whether
2966
+ * to create/update a skill. The agent already has full context from
2967
+ * the task it just completed — no need to re-describe anything.
2968
+ */
2969
+ async evaluateSkillPostTask(agentSessionId, model) {
2970
+ await evaluateAndMaybeCreateSkill({
2971
+ sessionId: agentSessionId,
2972
+ skillManager: this.skillManager,
2973
+ model
2974
+ });
2975
+ }
2976
+ async processTask(task) {
2977
+ const config = getConfig();
2978
+ resetEventSequence();
2979
+ const taskTimeoutMs = config.taskTimeoutMinutes * 6e4;
2980
+ newCorrelationId();
2981
+ log.info(`Processing task ${task.id.slice(0, 8)}...`);
2982
+ let finalResponse = "";
2983
+ const toolCallRecords = [];
2984
+ const toolFailures = [];
2985
+ let tokenUsage;
2986
+ let agentSessionId;
2987
+ try {
2988
+ await emitEvent(task.id, "status_change", { status: "running" });
2989
+ let systemPrompt = BASE_SYSTEM_PROMPT.replace("{workspace_path}", config.workspacePath);
2990
+ if (this.memoryManager) {
2991
+ try {
2992
+ const memoryPrompt = await this.memoryManager.buildMemoryPrompt();
2993
+ if (memoryPrompt) {
2994
+ systemPrompt += memoryPrompt;
2995
+ }
2996
+ } catch (err) {
2997
+ log.debug(`Memory load failed: ${err}`);
2998
+ }
2999
+ }
3000
+ const skillPrompt = this.skillManager.buildSkillDescriptions(task.prompt);
3001
+ if (skillPrompt) {
3002
+ systemPrompt += skillPrompt;
3003
+ }
3004
+ let history = [];
3005
+ try {
3006
+ history = await getConversationHistory(task.conversation_id, task.id, MAX_HISTORY_ENTRIES);
3007
+ } catch {
3008
+ log.debug("DB conversation history unavailable, using in-memory cache");
3009
+ }
3010
+ if (history.length === 0) {
3011
+ history = this.historyCache.get(task.conversation_id) || [];
3012
+ }
3013
+ if (history.length > 0) {
3014
+ log.info(`Loaded ${history.length} message(s) from conversation history`);
3015
+ let historyPrompt = "\n\n## Conversation History\nThe following is the history of previous messages in this conversation. Use this context to maintain continuity and understand references to earlier tasks.\n\n";
3016
+ for (const entry of history) {
3017
+ historyPrompt += `User: ${entry.prompt}
3018
+ `;
3019
+ const truncated = entry.response.length > MAX_HISTORY_RESPONSE_LENGTH ? entry.response.slice(0, MAX_HISTORY_RESPONSE_LENGTH) + "\u2026" : entry.response;
3020
+ historyPrompt += `Assistant: ${truncated}
3021
+
3022
+ `;
3023
+ }
3024
+ systemPrompt += historyPrompt;
3025
+ }
3026
+ const abortController = new AbortController();
3027
+ const taskTimeout = new TaskTimeout(abortController, taskTimeoutMs);
3028
+ const browserServer = createBrowserMcpServer();
3029
+ const agentToolsServer = createAgentToolsServer({
3030
+ memoryManager: this.memoryManager,
3031
+ skillManager: this.skillManager,
3032
+ taskId: task.id,
3033
+ sessionId: this.sessionId || void 0,
3034
+ heartbeatEngine: this.heartbeatEngine || void 0,
3035
+ onUserWaitStart: () => taskTimeout.pause(),
3036
+ onUserWaitEnd: () => taskTimeout.resume()
3037
+ });
3038
+ const eventHooks = createEventHooks(task.id, toolCallRecords, toolFailures);
3039
+ const allowedTools = [
3040
+ "Read",
3041
+ "Write",
3042
+ "Edit",
3043
+ "Bash",
3044
+ "Glob",
3045
+ "Grep",
3046
+ ...BROWSER_TOOL_NAMES.map((n) => `mcp__assistme-browser__${n}`),
3047
+ "mcp__assistme-agent__memory_store",
3048
+ "mcp__assistme-agent__skill_create",
3049
+ "mcp__assistme-agent__skill_improve",
3050
+ "mcp__assistme-agent__skill_invoke",
3051
+ "mcp__assistme-agent__skill_search",
3052
+ "mcp__assistme-agent__skill_generate",
3053
+ "mcp__assistme-agent__skill_link_job",
3054
+ "mcp__assistme-agent__skill_browse",
3055
+ "mcp__assistme-agent__skill_add",
3056
+ "mcp__assistme-agent__skill_publish",
3057
+ "mcp__assistme-agent__ask_user",
3058
+ "mcp__assistme-agent__job_run",
3059
+ "mcp__assistme-agent__job_schedule",
3060
+ "mcp__assistme-agent__job_status",
3061
+ "mcp__assistme-agent__credential_get",
3062
+ "mcp__assistme-agent__credential_set",
3063
+ "mcp__assistme-agent__credential_list",
3064
+ "mcp__assistme-agent__credential_remove",
3065
+ "mcp__assistme-agent__program_register",
3066
+ "mcp__assistme-agent__program_list",
3067
+ "mcp__assistme-agent__program_run",
3068
+ "mcp__assistme-agent__program_remove",
3069
+ "mcp__assistme-agent__heartbeat_status",
3070
+ "mcp__assistme-agent__heartbeat_enable",
3071
+ "mcp__assistme-agent__heartbeat_disable",
3072
+ "mcp__assistme-agent__heartbeat_interval",
3073
+ "mcp__assistme-agent__heartbeat_log"
3074
+ ];
3075
+ const mcpServers = {
3076
+ "assistme-browser": browserServer,
3077
+ "assistme-agent": agentToolsServer
3078
+ };
3079
+ const options = {
3080
+ model: config.model,
3081
+ systemPrompt,
3082
+ cwd: config.workspacePath,
3083
+ allowedTools,
3084
+ permissionMode: "bypassPermissions",
3085
+ allowDangerouslySkipPermissions: true,
3086
+ mcpServers,
3087
+ hooks: eventHooks,
3088
+ persistSession: true,
3089
+ abortController,
3090
+ thinking: { type: "adaptive" },
3091
+ effort: "high",
3092
+ maxBudgetUsd: MAX_BUDGET_USD
3093
+ };
3094
+ try {
3095
+ for await (const message of query3({ prompt: task.prompt, options })) {
3096
+ switch (message.type) {
3097
+ case "assistant": {
3098
+ const assistantMsg = message;
3099
+ for (const block of assistantMsg.message.content) {
3100
+ if (block.type === "text") {
3101
+ finalResponse += block.text;
3102
+ log.agent(block.text);
3103
+ await emitEvent(task.id, "text_delta", { text: block.text });
3104
+ } else if (block.type === "thinking" && "thinking" in block) {
3105
+ const thinkingBlock = block;
3106
+ log.debug(`Thinking: ${thinkingBlock.thinking.slice(0, 100)}...`);
3107
+ await emitEvent(task.id, "thinking", { text: thinkingBlock.thinking });
3108
+ }
3109
+ }
3110
+ break;
3111
+ }
3112
+ case "result": {
3113
+ const resultMsg = message;
3114
+ tokenUsage = {
3115
+ input_tokens: resultMsg.usage.input_tokens,
3116
+ output_tokens: resultMsg.usage.output_tokens
3117
+ };
3118
+ if (resultMsg.subtype === "success") {
3119
+ const successMsg = resultMsg;
3120
+ if (!finalResponse && successMsg.result) {
3121
+ finalResponse = successMsg.result;
3122
+ }
3123
+ agentSessionId = successMsg.session_id;
3124
+ log.info(
3125
+ `Task cost: $${successMsg.total_cost_usd.toFixed(4)}, turns: ${successMsg.num_turns}`
3126
+ );
3127
+ } else {
3128
+ const errMsg = resultMsg;
3129
+ log.warn(`SDK result: ${errMsg.subtype}`);
3130
+ for (const err of errMsg.errors) {
3131
+ await emitEvent(task.id, "error", { message: err });
3132
+ }
3133
+ }
3134
+ break;
3135
+ }
3136
+ default:
3137
+ if (message.type === "system" && "subtype" in message) {
3138
+ const sysMsg = message;
3139
+ if (sysMsg.subtype === "init" && sysMsg.session_id) {
3140
+ agentSessionId = sysMsg.session_id;
3141
+ }
3142
+ }
3143
+ log.debug(`SDK message type: ${message.type}`);
3144
+ break;
3145
+ }
3146
+ }
3147
+ } finally {
3148
+ taskTimeout.clear();
3149
+ }
3150
+ const truncatedResponse = finalResponse.length > MAX_RESPONSE_CONTENT_LENGTH ? finalResponse.slice(0, MAX_RESPONSE_CONTENT_LENGTH) + "\n\n[Response truncated]" : finalResponse;
3151
+ await withRetry(() => completeTask(task.id, truncatedResponse, tokenUsage), {
3152
+ maxRetries: MAX_COMPLETE_TASK_RETRIES,
3153
+ baseDelayMs: 300,
3154
+ label: "completeTask"
3155
+ });
3156
+ await emitEvent(task.id, "status_change", { status: "completed" });
3157
+ log.success("Task completed.");
3158
+ const convHistory = this.historyCache.get(task.conversation_id) || [];
3159
+ convHistory.push({ prompt: task.prompt, response: finalResponse });
3160
+ if (convHistory.length > MAX_HISTORY_ENTRIES * 2) {
3161
+ convHistory.splice(0, convHistory.length - MAX_HISTORY_ENTRIES * 2);
3162
+ }
3163
+ this.historyCache.set(task.conversation_id, convHistory);
3164
+ if (this.memoryManager) {
3165
+ this.memoryManager.compressIfNeeded().catch(
3166
+ (err) => log.debug(`Memory compression skipped: ${err}`)
3167
+ );
3168
+ }
3169
+ if (agentSessionId) {
3170
+ (async () => {
3171
+ try {
3172
+ await this.evaluateSkillPostTask(agentSessionId, config.model);
3173
+ } catch (err) {
3174
+ log.debug(`Post-task skill evaluation skipped: ${err}`);
3175
+ }
3176
+ try {
3177
+ await analyzeSelfPostTask({
3178
+ model: config.model,
3179
+ taskId: task.id,
3180
+ conversationId: task.conversation_id,
3181
+ taskPrompt: task.prompt,
3182
+ taskResponse: finalResponse,
3183
+ toolCallRecords,
3184
+ toolFailures,
3185
+ tokenUsage,
3186
+ sessionId: this.sessionId || ""
3187
+ });
3188
+ } catch (err) {
3189
+ log.debug(`Post-task self-analysis skipped: ${err}`);
3190
+ }
3191
+ })().catch(() => {
3192
+ });
3193
+ }
3194
+ } catch (err) {
3195
+ const errMsg = errorMessage(err);
3196
+ log.error(`Task failed: ${errMsg}`);
3197
+ await failTask(task.id, errMsg);
3198
+ await emitEvent(task.id, "error", { message: errMsg });
3199
+ await emitEvent(task.id, "status_change", { status: "failed" });
3200
+ } finally {
3201
+ setCorrelationId(null);
3202
+ try {
3203
+ const browser = getBrowser();
3204
+ if (browser.isConnected()) {
3205
+ await browser.disconnect();
3206
+ }
3207
+ } catch {
3208
+ }
3209
+ }
3210
+ }
3211
+ };
3212
+
3213
+ // src/workers/conversation.ts
3214
+ loadEnv();
3215
+ var ConversationHandler = class extends BaseHandler {
3216
+ workerType = "conversation";
3217
+ processor = null;
3218
+ async onInit(config) {
3219
+ this.processor = new TaskProcessor();
3220
+ this.processor.setUserId(config.userId);
3221
+ this.processor.setSessionId(config.sessionId);
3222
+ this.log(
3223
+ "info",
3224
+ `Conversation worker initialized (conversation: ${config.conversationId ?? "any"})`
3225
+ );
3226
+ }
3227
+ async onMessage(message) {
3228
+ switch (message.type) {
3229
+ case "process_task": {
3230
+ const { task } = message;
3231
+ this.log("info", `Processing task ${task.id.slice(0, 8)}...`);
3232
+ try {
3233
+ await this.processor.processTask(task);
3234
+ this.send({ type: "task_completed", taskId: task.id });
3235
+ } catch (err) {
3236
+ this.send({
3237
+ type: "task_failed",
3238
+ taskId: task.id,
3239
+ error: err instanceof Error ? err.message : String(err)
3240
+ });
3241
+ }
3242
+ break;
3243
+ }
3244
+ default:
3245
+ this.log("warn", `Unknown message type: ${message.type}`);
3246
+ }
3247
+ }
3248
+ async onShutdown() {
3249
+ this.log("info", "Conversation worker shutting down");
3250
+ try {
3251
+ const browser = getBrowser();
3252
+ if (browser.isConnected()) {
3253
+ await browser.disconnect();
3254
+ }
3255
+ } catch {
3256
+ }
3257
+ }
3258
+ };
3259
+
3260
+ // src/workers/entry.ts
3261
+ function getWorkerType() {
3262
+ const typeIndex = process.argv.indexOf("--type");
3263
+ if (typeIndex === -1 || !process.argv[typeIndex + 1]) {
3264
+ console.error("Usage: worker --type <conversation>");
3265
+ process.exit(1);
3266
+ }
3267
+ return process.argv[typeIndex + 1];
3268
+ }
3269
+ function createHandler(type2) {
3270
+ switch (type2) {
3271
+ case "conversation":
3272
+ return new ConversationHandler();
3273
+ default:
3274
+ console.error(`Unknown worker type: ${type2}`);
3275
+ process.exit(1);
3276
+ }
3277
+ }
3278
+ var type = getWorkerType();
3279
+ var handler = createHandler(type);
3280
+ handler.start();