assistme 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-KX7ITO55.js → chunk-4YWS463E.js} +174 -22
- package/dist/{chunk-TTEGHE2E.js → chunk-JVA6DHXD.js} +6 -4
- package/dist/{config-PUIS2TQL.js → config-T4357GAE.js} +1 -1
- package/dist/index.js +513 -164
- package/dist/job-runner-JT3JWZBV.js +7 -0
- package/package.json +2 -1
- package/src/agent/event-hooks.ts +16 -8
- package/src/agent/job-runner.ts +52 -40
- package/src/agent/processor.ts +24 -27
- package/src/agent/scheduler.ts +22 -59
- package/src/agent/skill-evaluator.ts +45 -52
- package/src/agent/skills.ts +57 -36
- package/src/browser/controller.ts +434 -13
- package/src/mcp/browser-server.ts +1 -1
- package/src/tools/filesystem.ts +32 -35
- package/src/tools/shell.ts +18 -22
- package/src/utils/config.test.ts +1 -1
- package/src/utils/config.ts +15 -9
- package/src/utils/constants.ts +77 -0
- package/src/utils/errors.ts +37 -0
- package/src/utils/schemas.ts +115 -0
- package/dist/job-runner-P2L6MOOX.js +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistme",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"chalk": "^5.4.1",
|
|
25
25
|
"commander": "^13.1.0",
|
|
26
26
|
"conf": "^13.0.1",
|
|
27
|
+
"croner": "^10.0.1",
|
|
27
28
|
"dotenv": "^16.5.0",
|
|
28
29
|
"glob": "^11.0.1",
|
|
29
30
|
"ora": "^8.2.0",
|
package/src/agent/event-hooks.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
HookCallbackMatcher,
|
|
3
|
+
HookCallback,
|
|
4
|
+
PreToolUseHookInput,
|
|
5
|
+
PostToolUseHookInput,
|
|
6
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
2
7
|
import { emitEvent } from "../db/supabase.js";
|
|
3
8
|
import { log } from "../utils/logger.js";
|
|
4
9
|
import type { ToolCallRecord } from "./skill-extractor.js";
|
|
10
|
+
import {
|
|
11
|
+
MAX_TOOL_INPUT_LOG_LENGTH,
|
|
12
|
+
MAX_TOOL_RESULT_LENGTH,
|
|
13
|
+
MAX_SKILL_RECORD_RESULT_LENGTH,
|
|
14
|
+
} from "../utils/constants.js";
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* Strip MCP server prefix from tool names for web UI compatibility.
|
|
@@ -29,7 +39,7 @@ export function createEventHooks(
|
|
|
29
39
|
const displayName = stripMcpPrefix(rawName);
|
|
30
40
|
const toolInput = preInput.tool_input;
|
|
31
41
|
|
|
32
|
-
log.tool(displayName, JSON.stringify(toolInput).slice(0,
|
|
42
|
+
log.tool(displayName, JSON.stringify(toolInput).slice(0, MAX_TOOL_INPUT_LOG_LENGTH));
|
|
33
43
|
|
|
34
44
|
await emitEvent(taskId, "tool_use_start", { name: displayName });
|
|
35
45
|
await emitEvent(taskId, "tool_use_input", { input: toolInput });
|
|
@@ -55,22 +65,20 @@ export function createEventHooks(
|
|
|
55
65
|
const toolResponse = postInput.tool_response;
|
|
56
66
|
|
|
57
67
|
const resultStr =
|
|
58
|
-
typeof toolResponse === "string"
|
|
59
|
-
? toolResponse
|
|
60
|
-
: JSON.stringify(toolResponse);
|
|
68
|
+
typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
|
|
61
69
|
|
|
62
|
-
log.result(resultStr.slice(0,
|
|
70
|
+
log.result(resultStr.slice(0, MAX_TOOL_INPUT_LOG_LENGTH));
|
|
63
71
|
|
|
64
72
|
await emitEvent(taskId, "tool_result", {
|
|
65
73
|
name: displayName,
|
|
66
|
-
result: resultStr.slice(0,
|
|
74
|
+
result: resultStr.slice(0, MAX_TOOL_RESULT_LENGTH),
|
|
67
75
|
});
|
|
68
76
|
|
|
69
77
|
// Record for post-task skill extraction
|
|
70
78
|
toolCallRecords.push({
|
|
71
79
|
name: displayName,
|
|
72
80
|
input: (toolInput as Record<string, unknown>) || {},
|
|
73
|
-
result: resultStr.slice(0,
|
|
81
|
+
result: resultStr.slice(0, MAX_SKILL_RECORD_RESULT_LENGTH),
|
|
74
82
|
});
|
|
75
83
|
|
|
76
84
|
return {};
|
package/src/agent/job-runner.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { callMcpHandler } from "../db/api-client.js";
|
|
2
2
|
import { log } from "../utils/logger.js";
|
|
3
|
+
import { JobRowSchema, JobListRowSchema, JobRunRowSchema, safeParse } from "../utils/schemas.js";
|
|
4
|
+
import { MAX_JOB_SUMMARY_LENGTH } from "../utils/constants.js";
|
|
5
|
+
import { errorMessage } from "../utils/errors.js";
|
|
3
6
|
|
|
4
7
|
// ── Interfaces ─────────────────────────────────────────────────────
|
|
5
8
|
|
|
@@ -37,32 +40,35 @@ export class JobRunner {
|
|
|
37
40
|
*/
|
|
38
41
|
async loadJob(jobName: string): Promise<JobInfo | null> {
|
|
39
42
|
try {
|
|
40
|
-
const data = await callMcpHandler<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
43
|
+
const data = await callMcpHandler<unknown[]>("job.get_with_skills", {
|
|
44
|
+
job_name: jobName,
|
|
45
|
+
});
|
|
44
46
|
|
|
45
|
-
if (!data || (data
|
|
47
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
46
48
|
return null;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
const rows = data
|
|
50
|
-
|
|
51
|
+
const rows = data.map((row) => safeParse(JobRowSchema, row)).filter(Boolean);
|
|
52
|
+
if (rows.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
const first = rows[0]!;
|
|
51
55
|
|
|
52
56
|
return {
|
|
53
|
-
jobId: first.job_id
|
|
54
|
-
jobName: first.job_name
|
|
55
|
-
jobDescription: first.job_description
|
|
56
|
-
skills: rows
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
jobId: first.job_id,
|
|
58
|
+
jobName: first.job_name,
|
|
59
|
+
jobDescription: first.job_description,
|
|
60
|
+
skills: rows
|
|
61
|
+
.filter((row) => row!.skill_id)
|
|
62
|
+
.map((row) => ({
|
|
63
|
+
skillId: row!.skill_id!,
|
|
64
|
+
skillName: row!.skill_name || "",
|
|
65
|
+
skillDescription: row!.skill_description,
|
|
66
|
+
skillEmoji: row!.skill_emoji,
|
|
67
|
+
skillContent: row!.skill_content,
|
|
68
|
+
})),
|
|
63
69
|
};
|
|
64
70
|
} catch (err) {
|
|
65
|
-
log.debug(`Failed to load job "${jobName}": ${err}`);
|
|
71
|
+
log.debug(`Failed to load job "${jobName}": ${errorMessage(err)}`);
|
|
66
72
|
return null;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
@@ -74,16 +80,19 @@ export class JobRunner {
|
|
|
74
80
|
Array<{ id: string; name: string; description: string; skillCount: number }>
|
|
75
81
|
> {
|
|
76
82
|
try {
|
|
77
|
-
const data = await callMcpHandler<
|
|
78
|
-
|
|
79
|
-
return (data || [])
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const data = await callMcpHandler<unknown[]>("job.list");
|
|
84
|
+
|
|
85
|
+
return (data || [])
|
|
86
|
+
.map((row) => safeParse(JobListRowSchema, row))
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.map((row) => ({
|
|
89
|
+
id: row!.id,
|
|
90
|
+
name: row!.name,
|
|
91
|
+
description: row!.description,
|
|
92
|
+
skillCount: row!.skill_count,
|
|
93
|
+
}));
|
|
85
94
|
} catch (err) {
|
|
86
|
-
log.debug(`Failed to list jobs: ${err}`);
|
|
95
|
+
log.debug(`Failed to list jobs: ${errorMessage(err)}`);
|
|
87
96
|
return [];
|
|
88
97
|
}
|
|
89
98
|
}
|
|
@@ -106,7 +115,7 @@ export class JobRunner {
|
|
|
106
115
|
});
|
|
107
116
|
return data;
|
|
108
117
|
} catch (err) {
|
|
109
|
-
log.debug(`Job run creation error: ${err}`);
|
|
118
|
+
log.debug(`Job run creation error: ${errorMessage(err)}`);
|
|
110
119
|
return null;
|
|
111
120
|
}
|
|
112
121
|
}
|
|
@@ -123,7 +132,7 @@ export class JobRunner {
|
|
|
123
132
|
await callMcpHandler("job.complete_run", {
|
|
124
133
|
run_id: runId,
|
|
125
134
|
status,
|
|
126
|
-
summary: summary?.slice(0,
|
|
135
|
+
summary: summary?.slice(0, MAX_JOB_SUMMARY_LENGTH) || null,
|
|
127
136
|
});
|
|
128
137
|
}
|
|
129
138
|
|
|
@@ -132,22 +141,25 @@ export class JobRunner {
|
|
|
132
141
|
*/
|
|
133
142
|
async getRunHistory(jobName?: string, limit = 10): Promise<JobRunInfo[]> {
|
|
134
143
|
try {
|
|
135
|
-
const data = await callMcpHandler<
|
|
144
|
+
const data = await callMcpHandler<unknown[]>("job.get_runs", {
|
|
136
145
|
job_name: jobName || null,
|
|
137
146
|
limit,
|
|
138
147
|
});
|
|
139
148
|
|
|
140
|
-
return (data || [])
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
return (data || [])
|
|
150
|
+
.map((row) => safeParse(JobRunRowSchema, row))
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.map((row) => ({
|
|
153
|
+
runId: row!.run_id,
|
|
154
|
+
jobName: row!.job_name,
|
|
155
|
+
status: row!.status,
|
|
156
|
+
triggerType: row!.trigger_type,
|
|
157
|
+
startedAt: row!.started_at,
|
|
158
|
+
completedAt: row!.completed_at ?? null,
|
|
159
|
+
summary: row!.summary ?? null,
|
|
160
|
+
}));
|
|
149
161
|
} catch (err) {
|
|
150
|
-
log.debug(`Run history error: ${err}`);
|
|
162
|
+
log.debug(`Run history error: ${errorMessage(err)}`);
|
|
151
163
|
return [];
|
|
152
164
|
}
|
|
153
165
|
}
|
package/src/agent/processor.ts
CHANGED
|
@@ -30,6 +30,13 @@ import {
|
|
|
30
30
|
} from "./mcp-servers.js";
|
|
31
31
|
import { createEventHooks } from "./event-hooks.js";
|
|
32
32
|
import { BASE_SYSTEM_PROMPT } from "./system-prompt.js";
|
|
33
|
+
import {
|
|
34
|
+
MAX_RESPONSE_CONTENT_LENGTH,
|
|
35
|
+
MAX_HISTORY_ENTRIES,
|
|
36
|
+
MAX_HISTORY_RESPONSE_LENGTH,
|
|
37
|
+
MAX_COMPLETE_TASK_RETRIES,
|
|
38
|
+
} from "../utils/constants.js";
|
|
39
|
+
import { errorMessage } from "../utils/errors.js";
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
42
|
* Manages the task wall-clock timeout.
|
|
@@ -80,8 +87,7 @@ class TaskTimeout {
|
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
const MAX_RESPONSE_LENGTH = 1500;
|
|
90
|
+
// Constants are now imported from utils/constants.ts
|
|
85
91
|
|
|
86
92
|
export class TaskProcessor {
|
|
87
93
|
private memoryManager: MemoryManager | null = null;
|
|
@@ -124,10 +130,8 @@ export class TaskProcessor {
|
|
|
124
130
|
const config = getConfig();
|
|
125
131
|
resetEventSequence();
|
|
126
132
|
|
|
127
|
-
// Wall-clock timeout for the entire task
|
|
128
|
-
const taskTimeoutMs =
|
|
129
|
-
(((config as unknown as Record<string, unknown>).taskTimeoutMinutes as number) || 10) *
|
|
130
|
-
60_000;
|
|
133
|
+
// Wall-clock timeout for the entire task
|
|
134
|
+
const taskTimeoutMs = config.taskTimeoutMinutes * 60_000;
|
|
131
135
|
|
|
132
136
|
// Set correlation ID for this task's log messages
|
|
133
137
|
newCorrelationId();
|
|
@@ -184,8 +188,8 @@ export class TaskProcessor {
|
|
|
184
188
|
for (const entry of history) {
|
|
185
189
|
historyPrompt += `User: ${entry.prompt}\n`;
|
|
186
190
|
const truncated =
|
|
187
|
-
entry.response.length >
|
|
188
|
-
? entry.response.slice(0,
|
|
191
|
+
entry.response.length > MAX_HISTORY_RESPONSE_LENGTH
|
|
192
|
+
? entry.response.slice(0, MAX_HISTORY_RESPONSE_LENGTH) + "…"
|
|
189
193
|
: entry.response;
|
|
190
194
|
historyPrompt += `Assistant: ${truncated}\n\n`;
|
|
191
195
|
}
|
|
@@ -274,18 +278,12 @@ export class TaskProcessor {
|
|
|
274
278
|
abortController,
|
|
275
279
|
};
|
|
276
280
|
|
|
277
|
-
const taskStartTime = Date.now();
|
|
278
|
-
|
|
279
281
|
try {
|
|
280
282
|
for await (const message of query({
|
|
281
283
|
prompt: promptMessages(),
|
|
282
284
|
options,
|
|
283
285
|
})) {
|
|
284
|
-
//
|
|
285
|
-
if (Date.now() - taskStartTime > taskTimeoutMs) {
|
|
286
|
-
finalResponse += "\n\n[Task timed out]";
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
286
|
+
// Timeout is handled by TaskTimeout + AbortController
|
|
289
287
|
|
|
290
288
|
switch (message.type) {
|
|
291
289
|
case "assistant": {
|
|
@@ -299,7 +297,8 @@ export class TaskProcessor {
|
|
|
299
297
|
text: block.text,
|
|
300
298
|
});
|
|
301
299
|
} else if (block.type === "thinking" && "thinking" in block) {
|
|
302
|
-
const
|
|
300
|
+
const thinkingBlock = block as { type: "thinking"; thinking: string };
|
|
301
|
+
const thinkingText = thinkingBlock.thinking;
|
|
303
302
|
log.debug(`Thinking: ${thinkingText.slice(0, 100)}...`);
|
|
304
303
|
await emitEvent(task.id, "thinking", {
|
|
305
304
|
text: thinkingText,
|
|
@@ -339,12 +338,11 @@ export class TaskProcessor {
|
|
|
339
338
|
|
|
340
339
|
default:
|
|
341
340
|
// Capture session ID from init message for post-task session resume
|
|
342
|
-
if (
|
|
343
|
-
message
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
agentSessionId = (message as Record<string, unknown>).session_id as string;
|
|
341
|
+
if (message.type === "system" && "subtype" in message) {
|
|
342
|
+
const sysMsg = message as { type: string; subtype?: string; session_id?: string };
|
|
343
|
+
if (sysMsg.subtype === "init" && sysMsg.session_id) {
|
|
344
|
+
agentSessionId = sysMsg.session_id;
|
|
345
|
+
}
|
|
348
346
|
}
|
|
349
347
|
log.debug(`SDK message type: ${message.type}`);
|
|
350
348
|
break;
|
|
@@ -355,15 +353,14 @@ export class TaskProcessor {
|
|
|
355
353
|
}
|
|
356
354
|
|
|
357
355
|
// Truncate finalResponse to avoid edge function payload limits
|
|
358
|
-
const MAX_CONTENT_LENGTH = 50_000;
|
|
359
356
|
const truncatedResponse =
|
|
360
|
-
finalResponse.length >
|
|
361
|
-
? finalResponse.slice(0,
|
|
357
|
+
finalResponse.length > MAX_RESPONSE_CONTENT_LENGTH
|
|
358
|
+
? finalResponse.slice(0, MAX_RESPONSE_CONTENT_LENGTH) + "\n\n[Response truncated]"
|
|
362
359
|
: finalResponse;
|
|
363
360
|
|
|
364
361
|
// Complete the task (with retry for transient DB failures)
|
|
365
362
|
await withRetry(() => completeTask(task.id, truncatedResponse, tokenUsage), {
|
|
366
|
-
maxRetries:
|
|
363
|
+
maxRetries: MAX_COMPLETE_TASK_RETRIES,
|
|
367
364
|
baseDelayMs: 300,
|
|
368
365
|
label: "completeTask",
|
|
369
366
|
});
|
|
@@ -386,7 +383,7 @@ export class TaskProcessor {
|
|
|
386
383
|
);
|
|
387
384
|
}
|
|
388
385
|
} catch (err) {
|
|
389
|
-
const errorMsg =
|
|
386
|
+
const errorMsg = errorMessage(err);
|
|
390
387
|
log.error(`Task failed: ${errorMsg}`);
|
|
391
388
|
|
|
392
389
|
await failTask(task.id, errorMsg);
|
package/src/agent/scheduler.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { Cron } from "croner";
|
|
1
2
|
import { callMcpHandler } from "../db/api-client.js";
|
|
2
3
|
import { log } from "../utils/logger.js";
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import { SCHEDULER_INTERVAL_MS } from "../utils/constants.js";
|
|
5
|
+
import { errorMessage } from "../utils/errors.js";
|
|
5
6
|
|
|
6
7
|
export interface ScheduledTask {
|
|
7
8
|
id: string;
|
|
@@ -22,69 +23,31 @@ export interface ScheduledTask {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Parse a cron expression and calculate the next run time
|
|
26
|
+
* Parse a cron expression and calculate the next run time using `croner`.
|
|
26
27
|
* Supports: minute hour day-of-month month day-of-week
|
|
27
28
|
* Examples: "0 8 * * *" (daily 8am), "0,30 * * * *" (every 30min)
|
|
29
|
+
*
|
|
30
|
+
* Handles edge cases correctly (Feb 29, month boundaries, invalid dates)
|
|
31
|
+
* that the previous hand-rolled parser missed.
|
|
28
32
|
*/
|
|
29
33
|
export function getNextRunTime(cronExpr: string, timezone: string, fromDate?: Date): Date {
|
|
30
34
|
const now = fromDate || new Date();
|
|
31
|
-
const parts = cronExpr.trim().split(/\s+/);
|
|
32
|
-
if (parts.length !== 5) {
|
|
33
|
-
throw new Error(`Invalid cron expression: ${cronExpr}`);
|
|
34
|
-
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const values: number[] = [];
|
|
40
|
-
for (const part of expr.split(",")) {
|
|
41
|
-
if (part === "*") {
|
|
42
|
-
for (let i = min; i <= max; i++) values.push(i);
|
|
43
|
-
} else if (part.startsWith("*/")) {
|
|
44
|
-
const step = parseInt(part.slice(2));
|
|
45
|
-
for (let i = min; i <= max; i += step) values.push(i);
|
|
46
|
-
} else if (part.includes("-")) {
|
|
47
|
-
const [start, end] = part.split("-").map(Number);
|
|
48
|
-
for (let i = start; i <= end; i++) values.push(i);
|
|
49
|
-
} else {
|
|
50
|
-
values.push(parseInt(part));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return values.sort((a, b) => a - b);
|
|
54
|
-
}
|
|
36
|
+
try {
|
|
37
|
+
const job = new Cron(cronExpr, { timezone: timezone || "UTC" });
|
|
38
|
+
const next = job.nextRun(now);
|
|
55
39
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const daysOfMonth = parseField(domExpr, 1, 31);
|
|
59
|
-
const months = parseField(monExpr, 1, 12);
|
|
60
|
-
const daysOfWeek = parseField(dowExpr, 0, 6);
|
|
61
|
-
|
|
62
|
-
const useUTC = timezone === "UTC";
|
|
63
|
-
|
|
64
|
-
const candidate = new Date(now.getTime() + 60_000);
|
|
65
|
-
candidate.setSeconds(0, 0);
|
|
66
|
-
|
|
67
|
-
for (let i = 0; i < 527040; i++) {
|
|
68
|
-
const m = useUTC ? candidate.getUTCMinutes() : candidate.getMinutes();
|
|
69
|
-
const h = useUTC ? candidate.getUTCHours() : candidate.getHours();
|
|
70
|
-
const dom = useUTC ? candidate.getUTCDate() : candidate.getDate();
|
|
71
|
-
const mon = (useUTC ? candidate.getUTCMonth() : candidate.getMonth()) + 1;
|
|
72
|
-
const dow = useUTC ? candidate.getUTCDay() : candidate.getDay();
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
minutes.includes(m) &&
|
|
76
|
-
hours.includes(h) &&
|
|
77
|
-
daysOfMonth.includes(dom) &&
|
|
78
|
-
months.includes(mon) &&
|
|
79
|
-
(dowExpr === "*" || daysOfWeek.includes(dow))
|
|
80
|
-
) {
|
|
81
|
-
return candidate;
|
|
40
|
+
if (!next) {
|
|
41
|
+
throw new Error(`No future run time found for cron expression: ${cronExpr}`);
|
|
82
42
|
}
|
|
83
43
|
|
|
84
|
-
|
|
44
|
+
return next;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err instanceof Error && err.message.includes("No future run time")) {
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Invalid cron expression "${cronExpr}": ${errorMessage(err)}`);
|
|
85
50
|
}
|
|
86
|
-
|
|
87
|
-
return new Date(now.getTime() + 86400_000);
|
|
88
51
|
}
|
|
89
52
|
|
|
90
53
|
export class Scheduler {
|
|
@@ -98,7 +61,7 @@ export class Scheduler {
|
|
|
98
61
|
|
|
99
62
|
await this.initializeNextRuns();
|
|
100
63
|
|
|
101
|
-
this.timer = setInterval(() => this.checkDueTasks(),
|
|
64
|
+
this.timer = setInterval(() => this.checkDueTasks(), SCHEDULER_INTERVAL_MS);
|
|
102
65
|
log.info("Scheduler started (checking every 30s)");
|
|
103
66
|
}
|
|
104
67
|
|
|
@@ -124,7 +87,7 @@ export class Scheduler {
|
|
|
124
87
|
}
|
|
125
88
|
}
|
|
126
89
|
} catch (err) {
|
|
127
|
-
log.debug(`Scheduler init: ${err}`);
|
|
90
|
+
log.debug(`Scheduler init: ${errorMessage(err)}`);
|
|
128
91
|
}
|
|
129
92
|
}
|
|
130
93
|
|
|
@@ -158,7 +121,7 @@ export class Scheduler {
|
|
|
158
121
|
last_error: null,
|
|
159
122
|
});
|
|
160
123
|
} catch (err) {
|
|
161
|
-
const errMsg =
|
|
124
|
+
const errMsg = errorMessage(err);
|
|
162
125
|
await callMcpHandler("schedule.update", {
|
|
163
126
|
task_id: task.id,
|
|
164
127
|
last_error: errMsg,
|
|
@@ -166,7 +129,7 @@ export class Scheduler {
|
|
|
166
129
|
log.error(`Scheduled task "${task.name}" failed: ${errMsg}`);
|
|
167
130
|
}
|
|
168
131
|
} catch (err) {
|
|
169
|
-
log.debug(`Scheduler check error: ${err}`);
|
|
132
|
+
log.debug(`Scheduler check error: ${errorMessage(err)}`);
|
|
170
133
|
}
|
|
171
134
|
}
|
|
172
135
|
}
|
|
@@ -6,24 +6,8 @@ import {
|
|
|
6
6
|
import { log } from "../utils/logger.js";
|
|
7
7
|
import type { SkillManager } from "./skills.js";
|
|
8
8
|
import { validateSkillName, normalizeSkillName } from "./skills.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
interface SkillDecision {
|
|
13
|
-
action: "create" | "update" | "skip";
|
|
14
|
-
// For "create"
|
|
15
|
-
name?: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
instructions?: string;
|
|
18
|
-
emoji?: string;
|
|
19
|
-
keywords?: string[];
|
|
20
|
-
// For "update"
|
|
21
|
-
existing_skill_name?: string;
|
|
22
|
-
improved_instructions?: string;
|
|
23
|
-
improved_description?: string;
|
|
24
|
-
// Always present
|
|
25
|
-
reason: string;
|
|
26
|
-
}
|
|
9
|
+
import { SkillDecisionSchema, type SkillDecision, safeParse } from "../utils/schemas.js";
|
|
10
|
+
import { errorMessage } from "../utils/errors.js";
|
|
27
11
|
|
|
28
12
|
// ── Agent Skills format spec (agentskills.io) ───────────────────────
|
|
29
13
|
|
|
@@ -78,9 +62,10 @@ export async function evaluateAndMaybeCreateSkill(opts: {
|
|
|
78
62
|
|
|
79
63
|
// Build existing skills context so the agent knows what already exists
|
|
80
64
|
const existingSkills = skillManager.getAll();
|
|
81
|
-
const existingList =
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
const existingList =
|
|
66
|
+
existingSkills.length > 0
|
|
67
|
+
? existingSkills.map((s) => `- ${s.name}: ${s.description}`).join("\n")
|
|
68
|
+
: "(no existing skills)";
|
|
84
69
|
|
|
85
70
|
const prompt = `${SKILL_EVALUATION_PROMPT}
|
|
86
71
|
|
|
@@ -111,7 +96,9 @@ Respond with a JSON object now.`;
|
|
|
111
96
|
} else if (message.type === "result") {
|
|
112
97
|
const resultMsg = message as SDKResultMessage;
|
|
113
98
|
if (resultMsg.subtype === "success" && "total_cost_usd" in resultMsg) {
|
|
114
|
-
log.debug(
|
|
99
|
+
log.debug(
|
|
100
|
+
`Skill evaluation cost: $${(resultMsg as { total_cost_usd: number }).total_cost_usd.toFixed(4)}`
|
|
101
|
+
);
|
|
115
102
|
}
|
|
116
103
|
}
|
|
117
104
|
}
|
|
@@ -123,15 +110,12 @@ Respond with a JSON object now.`;
|
|
|
123
110
|
return;
|
|
124
111
|
}
|
|
125
112
|
|
|
126
|
-
|
|
127
|
-
log.debug("Skill evaluation: invalid action");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
113
|
+
// Action is already validated by Zod schema
|
|
130
114
|
|
|
131
115
|
// Execute the decision
|
|
132
116
|
await executeSkillDecision(decision, skillManager);
|
|
133
117
|
} catch (err) {
|
|
134
|
-
log.debug(`Skill evaluation error: ${err}`);
|
|
118
|
+
log.debug(`Skill evaluation error: ${errorMessage(err)}`);
|
|
135
119
|
}
|
|
136
120
|
}
|
|
137
121
|
|
|
@@ -149,14 +133,18 @@ async function executeSkillDecision(
|
|
|
149
133
|
return;
|
|
150
134
|
}
|
|
151
135
|
|
|
152
|
-
//
|
|
153
|
-
let skillName = decision.name;
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
136
|
+
// Always normalize, then validate once
|
|
137
|
+
let skillName = normalizeSkillName(decision.name);
|
|
138
|
+
if (!skillName) {
|
|
139
|
+
log.debug(`Skill create skipped: name "${decision.name}" cannot be normalized`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const validationError = validateSkillName(skillName);
|
|
143
|
+
if (validationError) {
|
|
144
|
+
log.debug(`Skill create skipped: ${validationError}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (skillName !== decision.name) {
|
|
160
148
|
log.debug(`Normalized skill name: "${decision.name}" → "${skillName}"`);
|
|
161
149
|
}
|
|
162
150
|
|
|
@@ -228,31 +216,36 @@ async function executeSkillDecision(
|
|
|
228
216
|
* Attempt to parse a SkillDecision from the model's response text.
|
|
229
217
|
* Tries the full text first (model returned pure JSON), then falls
|
|
230
218
|
* back to extracting the outermost balanced `{…}` block.
|
|
219
|
+
* Validates the parsed result against the Zod schema.
|
|
231
220
|
*/
|
|
232
221
|
function parseJsonResponse(text: string): SkillDecision | null {
|
|
233
222
|
const trimmed = text.trim();
|
|
234
223
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
const parsed = JSON.parse(trimmed) as SkillDecision;
|
|
238
|
-
if (parsed.action) return parsed;
|
|
239
|
-
} catch { /* not pure JSON */ }
|
|
224
|
+
// Try to extract JSON — full text first, then balanced `{…}` block
|
|
225
|
+
const candidates: string[] = [trimmed];
|
|
240
226
|
|
|
241
|
-
// Fallback: find the first balanced `{…}` block
|
|
242
227
|
const start = trimmed.indexOf("{");
|
|
243
|
-
if (start
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return JSON.parse(trimmed.slice(start, i + 1)) as SkillDecision;
|
|
252
|
-
} catch {
|
|
253
|
-
return null;
|
|
228
|
+
if (start !== -1) {
|
|
229
|
+
let depth = 0;
|
|
230
|
+
for (let i = start; i < trimmed.length; i++) {
|
|
231
|
+
if (trimmed[i] === "{") depth++;
|
|
232
|
+
else if (trimmed[i] === "}") depth--;
|
|
233
|
+
if (depth === 0) {
|
|
234
|
+
candidates.push(trimmed.slice(start, i + 1));
|
|
235
|
+
break;
|
|
254
236
|
}
|
|
255
237
|
}
|
|
256
238
|
}
|
|
239
|
+
|
|
240
|
+
for (const candidate of candidates) {
|
|
241
|
+
try {
|
|
242
|
+
const parsed = JSON.parse(candidate);
|
|
243
|
+
const validated = safeParse(SkillDecisionSchema, parsed);
|
|
244
|
+
if (validated) return validated;
|
|
245
|
+
} catch {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
257
250
|
return null;
|
|
258
251
|
}
|