assistme 0.3.5 → 0.4.0
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-4SBIN27G.js} +184 -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 +434 -221
- package/dist/job-runner-CJ7HM4GZ.js +7 -0
- package/package.json +2 -1
- package/src/agent/event-hooks.ts +59 -10
- package/src/agent/job-runner.ts +52 -40
- package/src/agent/memory.ts +124 -0
- package/src/agent/processor.ts +62 -87
- package/src/agent/scheduler.ts +22 -59
- package/src/agent/skill-evaluator.ts +192 -87
- package/src/agent/skills.ts +57 -36
- package/src/agent/system-prompt.ts +9 -0
- package/src/browser/controller.ts +16 -5
- package/src/db/types.ts +3 -1
- package/src/tools/filesystem.ts +32 -35
- package/src/tools/shell.ts +18 -22
- package/src/utils/config.ts +15 -9
- package/src/utils/constants.ts +98 -0
- package/src/utils/errors.ts +37 -0
- package/src/utils/schemas.ts +148 -0
- package/{src → tests}/agent/event-hooks.test.ts +121 -33
- package/{src → tests}/agent/mcp-servers.test.ts +43 -29
- package/{src → tests}/agent/memory.test.ts +71 -3
- package/{src → tests}/agent/processor.test.ts +59 -55
- package/{src → tests}/agent/scheduler.test.ts +1 -1
- package/{src → tests}/agent/session.test.ts +20 -10
- package/{src → tests}/agent/skills.test.ts +51 -29
- package/{src → tests}/credentials/credential-store.test.ts +23 -8
- package/{src → tests}/credentials/encryption.test.ts +1 -1
- package/{src → tests}/db/supabase.test.ts +4 -4
- package/{src → tests}/tools/filesystem.test.ts +6 -15
- package/{src → tests}/tools/shell.test.ts +1 -1
- package/{src → tests}/utils/config.test.ts +3 -2
- package/{src → tests}/utils/rate-limiter.test.ts +1 -1
- package/{src → tests}/utils/retry.test.ts +6 -12
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -1
- package/dist/job-runner-P2L6MOOX.js +0 -7
package/dist/index.js
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
AppError,
|
|
4
|
+
BrowseSkillRowSchema,
|
|
5
|
+
CDP_COMMAND_TIMEOUT_MS,
|
|
6
|
+
FRAME_CONTEXTS_MAX_SIZE,
|
|
3
7
|
JobRunner,
|
|
8
|
+
MAX_BUDGET_USD,
|
|
9
|
+
MAX_COMPLETE_TASK_RETRIES,
|
|
10
|
+
MAX_CONTENT_SEARCH_FILES,
|
|
11
|
+
MAX_CONTENT_SEARCH_RESULTS,
|
|
12
|
+
MAX_FILE_SEARCH_RESULTS,
|
|
13
|
+
MAX_HISTORY_ENTRIES,
|
|
14
|
+
MAX_HISTORY_RESPONSE_LENGTH,
|
|
15
|
+
MAX_RESPONSE_CONTENT_LENGTH,
|
|
16
|
+
MAX_SKILL_RECORD_RESULT_LENGTH,
|
|
17
|
+
MAX_TOOL_INPUT_LOG_LENGTH,
|
|
18
|
+
MAX_TOOL_RESULT_LENGTH,
|
|
19
|
+
MEMORY_COMPRESSION_TARGET,
|
|
20
|
+
MEMORY_COMPRESSION_THRESHOLD,
|
|
21
|
+
MEMORY_DEDUP_SIMILARITY_THRESHOLD,
|
|
22
|
+
SCHEDULER_INTERVAL_MS,
|
|
23
|
+
SHELL_MAX_OUTPUT,
|
|
24
|
+
SHELL_TIMEOUT_MS,
|
|
25
|
+
SKILL_DESCRIPTION_BUDGET_CHARS,
|
|
26
|
+
SKILL_VALIDATION_MAX_TURNS,
|
|
27
|
+
SkillCreateResultSchema,
|
|
28
|
+
SkillDecisionSchema,
|
|
29
|
+
SkillRowSchema,
|
|
30
|
+
WS_CONNECT_TIMEOUT_MS,
|
|
4
31
|
callMcpHandler,
|
|
32
|
+
errorMessage,
|
|
5
33
|
log,
|
|
6
34
|
newCorrelationId,
|
|
7
35
|
readAuthStore,
|
|
36
|
+
safeParse,
|
|
8
37
|
setCorrelationId,
|
|
9
38
|
setLogLevel,
|
|
10
39
|
writeAuthStore
|
|
11
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-4SBIN27G.js";
|
|
12
41
|
import {
|
|
13
42
|
clearConfig,
|
|
14
43
|
getConfig,
|
|
15
44
|
getConfigPath,
|
|
16
45
|
setConfig
|
|
17
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-JVA6DHXD.js";
|
|
18
47
|
|
|
19
48
|
// src/index.ts
|
|
20
49
|
import { Command } from "commander";
|
|
@@ -54,7 +83,7 @@ async function logout() {
|
|
|
54
83
|
|
|
55
84
|
// src/db/session.ts
|
|
56
85
|
async function createSession(sessionName, workspacePath, version2) {
|
|
57
|
-
const { getConfig: getConfig2 } = await import("./config-
|
|
86
|
+
const { getConfig: getConfig2 } = await import("./config-T4357GAE.js");
|
|
58
87
|
const data = await callMcpHandler("session.create", {
|
|
59
88
|
session_name: sessionName,
|
|
60
89
|
workspace_path: workspacePath,
|
|
@@ -135,11 +164,11 @@ async function completeTask(messageId, resultSummary, tokenUsage) {
|
|
|
135
164
|
token_usage: tokenUsage || null
|
|
136
165
|
});
|
|
137
166
|
}
|
|
138
|
-
async function failTask(messageId,
|
|
167
|
+
async function failTask(messageId, errorMessage2) {
|
|
139
168
|
try {
|
|
140
169
|
await callMcpHandler("task.fail", {
|
|
141
170
|
message_id: messageId,
|
|
142
|
-
error:
|
|
171
|
+
error: errorMessage2
|
|
143
172
|
});
|
|
144
173
|
} catch (err) {
|
|
145
174
|
log.error(`Failed to update task status: ${err instanceof Error ? err.message : err}`);
|
|
@@ -412,9 +441,9 @@ var BrowserController = class {
|
|
|
412
441
|
if (!settled) {
|
|
413
442
|
settled = true;
|
|
414
443
|
this.ws?.close();
|
|
415
|
-
reject(new Error(
|
|
444
|
+
reject(new Error(`Connection timeout (${WS_CONNECT_TIMEOUT_MS}ms)`));
|
|
416
445
|
}
|
|
417
|
-
},
|
|
446
|
+
}, WS_CONNECT_TIMEOUT_MS);
|
|
418
447
|
this.ws.on("open", () => {
|
|
419
448
|
if (settled) return;
|
|
420
449
|
settled = true;
|
|
@@ -462,6 +491,8 @@ var BrowserController = class {
|
|
|
462
491
|
this.ws = null;
|
|
463
492
|
this.connected = false;
|
|
464
493
|
}
|
|
494
|
+
this.refCache.clear();
|
|
495
|
+
this.frameContexts.clear();
|
|
465
496
|
return "Disconnected from browser.";
|
|
466
497
|
}
|
|
467
498
|
// ── CDP Protocol ────────────────────────────────────────────────
|
|
@@ -481,7 +512,7 @@ var BrowserController = class {
|
|
|
481
512
|
const timeout = setTimeout(() => {
|
|
482
513
|
this.callbacks.delete(id);
|
|
483
514
|
reject(new Error(`CDP command timed out: ${method}`));
|
|
484
|
-
},
|
|
515
|
+
}, CDP_COMMAND_TIMEOUT_MS);
|
|
485
516
|
this.callbacks.set(id, (response) => {
|
|
486
517
|
clearTimeout(timeout);
|
|
487
518
|
if (response.error) {
|
|
@@ -1213,7 +1244,9 @@ Refs:
|
|
|
1213
1244
|
height: r.box.height
|
|
1214
1245
|
}
|
|
1215
1246
|
});
|
|
1216
|
-
this.frameContexts.
|
|
1247
|
+
if (this.frameContexts.size < FRAME_CONTEXTS_MAX_SIZE) {
|
|
1248
|
+
this.frameContexts.set(r.id, contextId);
|
|
1249
|
+
}
|
|
1217
1250
|
}
|
|
1218
1251
|
} catch {
|
|
1219
1252
|
}
|
|
@@ -2328,51 +2361,22 @@ import ora3 from "ora";
|
|
|
2328
2361
|
import { createInterface as createInterface2 } from "readline";
|
|
2329
2362
|
|
|
2330
2363
|
// src/agent/scheduler.ts
|
|
2331
|
-
|
|
2364
|
+
import { Cron } from "croner";
|
|
2332
2365
|
function getNextRunTime(cronExpr, timezone, fromDate) {
|
|
2333
2366
|
const now = fromDate || /* @__PURE__ */ new Date();
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
} else if (part.startsWith("*/")) {
|
|
2345
|
-
const step = parseInt(part.slice(2));
|
|
2346
|
-
for (let i = min; i <= max; i += step) values.push(i);
|
|
2347
|
-
} else if (part.includes("-")) {
|
|
2348
|
-
const [start, end] = part.split("-").map(Number);
|
|
2349
|
-
for (let i = start; i <= end; i++) values.push(i);
|
|
2350
|
-
} else {
|
|
2351
|
-
values.push(parseInt(part));
|
|
2352
|
-
}
|
|
2367
|
+
try {
|
|
2368
|
+
const job = new Cron(cronExpr, { timezone: timezone || "UTC" });
|
|
2369
|
+
const next = job.nextRun(now);
|
|
2370
|
+
if (!next) {
|
|
2371
|
+
throw new Error(`No future run time found for cron expression: ${cronExpr}`);
|
|
2372
|
+
}
|
|
2373
|
+
return next;
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
if (err instanceof Error && err.message.includes("No future run time")) {
|
|
2376
|
+
throw err;
|
|
2353
2377
|
}
|
|
2354
|
-
|
|
2355
|
-
}
|
|
2356
|
-
const minutes = parseField(minExpr, 0, 59);
|
|
2357
|
-
const hours = parseField(hourExpr, 0, 23);
|
|
2358
|
-
const daysOfMonth = parseField(domExpr, 1, 31);
|
|
2359
|
-
const months = parseField(monExpr, 1, 12);
|
|
2360
|
-
const daysOfWeek = parseField(dowExpr, 0, 6);
|
|
2361
|
-
const useUTC = timezone === "UTC";
|
|
2362
|
-
const candidate = new Date(now.getTime() + 6e4);
|
|
2363
|
-
candidate.setSeconds(0, 0);
|
|
2364
|
-
for (let i = 0; i < 527040; i++) {
|
|
2365
|
-
const m = useUTC ? candidate.getUTCMinutes() : candidate.getMinutes();
|
|
2366
|
-
const h = useUTC ? candidate.getUTCHours() : candidate.getHours();
|
|
2367
|
-
const dom = useUTC ? candidate.getUTCDate() : candidate.getDate();
|
|
2368
|
-
const mon = (useUTC ? candidate.getUTCMonth() : candidate.getMonth()) + 1;
|
|
2369
|
-
const dow = useUTC ? candidate.getUTCDay() : candidate.getDay();
|
|
2370
|
-
if (minutes.includes(m) && hours.includes(h) && daysOfMonth.includes(dom) && months.includes(mon) && (dowExpr === "*" || daysOfWeek.includes(dow))) {
|
|
2371
|
-
return candidate;
|
|
2372
|
-
}
|
|
2373
|
-
candidate.setTime(candidate.getTime() + 6e4);
|
|
2374
|
-
}
|
|
2375
|
-
return new Date(now.getTime() + 864e5);
|
|
2378
|
+
throw new Error(`Invalid cron expression "${cronExpr}": ${errorMessage(err)}`);
|
|
2379
|
+
}
|
|
2376
2380
|
}
|
|
2377
2381
|
var Scheduler = class {
|
|
2378
2382
|
timer = null;
|
|
@@ -2382,7 +2386,7 @@ var Scheduler = class {
|
|
|
2382
2386
|
this.onScheduledTask = onScheduledTask;
|
|
2383
2387
|
this.running = true;
|
|
2384
2388
|
await this.initializeNextRuns();
|
|
2385
|
-
this.timer = setInterval(() => this.checkDueTasks(),
|
|
2389
|
+
this.timer = setInterval(() => this.checkDueTasks(), SCHEDULER_INTERVAL_MS);
|
|
2386
2390
|
log.info("Scheduler started (checking every 30s)");
|
|
2387
2391
|
}
|
|
2388
2392
|
stop() {
|
|
@@ -2405,7 +2409,7 @@ var Scheduler = class {
|
|
|
2405
2409
|
}
|
|
2406
2410
|
}
|
|
2407
2411
|
} catch (err) {
|
|
2408
|
-
log.debug(`Scheduler init: ${err}`);
|
|
2412
|
+
log.debug(`Scheduler init: ${errorMessage(err)}`);
|
|
2409
2413
|
}
|
|
2410
2414
|
}
|
|
2411
2415
|
async checkDueTasks() {
|
|
@@ -2429,7 +2433,7 @@ var Scheduler = class {
|
|
|
2429
2433
|
last_error: null
|
|
2430
2434
|
});
|
|
2431
2435
|
} catch (err) {
|
|
2432
|
-
const errMsg =
|
|
2436
|
+
const errMsg = errorMessage(err);
|
|
2433
2437
|
await callMcpHandler("schedule.update", {
|
|
2434
2438
|
task_id: task.id,
|
|
2435
2439
|
last_error: errMsg
|
|
@@ -2437,7 +2441,7 @@ var Scheduler = class {
|
|
|
2437
2441
|
log.error(`Scheduled task "${task.name}" failed: ${errMsg}`);
|
|
2438
2442
|
}
|
|
2439
2443
|
} catch (err) {
|
|
2440
|
-
log.debug(`Scheduler check error: ${err}`);
|
|
2444
|
+
log.debug(`Scheduler check error: ${errorMessage(err)}`);
|
|
2441
2445
|
}
|
|
2442
2446
|
}
|
|
2443
2447
|
};
|
|
@@ -2808,7 +2812,92 @@ var MemoryManager = class {
|
|
|
2808
2812
|
});
|
|
2809
2813
|
return result.count;
|
|
2810
2814
|
}
|
|
2815
|
+
// ── Compression & Deduplication ──────────────────────────────────
|
|
2816
|
+
/**
|
|
2817
|
+
* Check if memory count exceeds threshold and compress if needed.
|
|
2818
|
+
* Called automatically after task completion.
|
|
2819
|
+
*/
|
|
2820
|
+
async compressIfNeeded() {
|
|
2821
|
+
try {
|
|
2822
|
+
const all = await this.list(void 0, 200);
|
|
2823
|
+
if (all.length < MEMORY_COMPRESSION_THRESHOLD) {
|
|
2824
|
+
return 0;
|
|
2825
|
+
}
|
|
2826
|
+
log.info(`Memory compression triggered: ${all.length} memories (threshold: ${MEMORY_COMPRESSION_THRESHOLD})`);
|
|
2827
|
+
let removed = 0;
|
|
2828
|
+
const now = Date.now();
|
|
2829
|
+
for (const m of all) {
|
|
2830
|
+
if (m.expires_at && new Date(m.expires_at).getTime() < now) {
|
|
2831
|
+
await this.remove(m.id);
|
|
2832
|
+
removed++;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
const remaining = all.filter(
|
|
2836
|
+
(m) => !m.expires_at || new Date(m.expires_at).getTime() >= now
|
|
2837
|
+
);
|
|
2838
|
+
const duplicateIds = this.findDuplicates(remaining);
|
|
2839
|
+
for (const id of duplicateIds) {
|
|
2840
|
+
await this.remove(id);
|
|
2841
|
+
removed++;
|
|
2842
|
+
}
|
|
2843
|
+
const afterDedup = remaining.filter((m) => !duplicateIds.has(m.id));
|
|
2844
|
+
if (afterDedup.length > MEMORY_COMPRESSION_TARGET) {
|
|
2845
|
+
const toRemove = afterDedup.sort((a, b) => {
|
|
2846
|
+
if (a.importance !== b.importance) return a.importance - b.importance;
|
|
2847
|
+
if (a.access_count !== b.access_count) return a.access_count - b.access_count;
|
|
2848
|
+
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
2849
|
+
}).slice(0, afterDedup.length - MEMORY_COMPRESSION_TARGET);
|
|
2850
|
+
for (const m of toRemove) {
|
|
2851
|
+
if (m.category === "instruction" && m.importance >= 8) continue;
|
|
2852
|
+
await this.remove(m.id);
|
|
2853
|
+
removed++;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
if (removed > 0) {
|
|
2857
|
+
log.info(`Memory compression complete: removed ${removed} memories`);
|
|
2858
|
+
}
|
|
2859
|
+
return removed;
|
|
2860
|
+
} catch (err) {
|
|
2861
|
+
log.warn(`Memory compression error: ${err instanceof Error ? err.message : err}`);
|
|
2862
|
+
return 0;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Find duplicate memories based on content similarity.
|
|
2867
|
+
* Returns the IDs of memories that should be removed (keeps the higher-importance duplicate).
|
|
2868
|
+
*/
|
|
2869
|
+
findDuplicates(memories) {
|
|
2870
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
2871
|
+
for (let i = 0; i < memories.length; i++) {
|
|
2872
|
+
if (toRemove.has(memories[i].id)) continue;
|
|
2873
|
+
for (let j = i + 1; j < memories.length; j++) {
|
|
2874
|
+
if (toRemove.has(memories[j].id)) continue;
|
|
2875
|
+
if (memories[i].category !== memories[j].category) continue;
|
|
2876
|
+
const similarity = computeWordOverlap(memories[i].content, memories[j].content);
|
|
2877
|
+
if (similarity >= MEMORY_DEDUP_SIMILARITY_THRESHOLD) {
|
|
2878
|
+
if (memories[i].importance > memories[j].importance || memories[i].importance === memories[j].importance && new Date(memories[i].created_at) > new Date(memories[j].created_at)) {
|
|
2879
|
+
toRemove.add(memories[j].id);
|
|
2880
|
+
} else {
|
|
2881
|
+
toRemove.add(memories[i].id);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
return toRemove;
|
|
2887
|
+
}
|
|
2811
2888
|
};
|
|
2889
|
+
function computeWordOverlap(a, b) {
|
|
2890
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
2891
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
2892
|
+
if (wordsA.size === 0 && wordsB.size === 0) return 1;
|
|
2893
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
2894
|
+
let intersection = 0;
|
|
2895
|
+
for (const w of wordsA) {
|
|
2896
|
+
if (wordsB.has(w)) intersection++;
|
|
2897
|
+
}
|
|
2898
|
+
const union = wordsA.size + wordsB.size - intersection;
|
|
2899
|
+
return union === 0 ? 0 : intersection / union;
|
|
2900
|
+
}
|
|
2812
2901
|
|
|
2813
2902
|
// src/agent/skills.ts
|
|
2814
2903
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -2957,7 +3046,7 @@ var SkillManager = class {
|
|
|
2957
3046
|
userId = null;
|
|
2958
3047
|
/** Cache for findRelevant() — keyed by prompt, invalidated on skill changes */
|
|
2959
3048
|
relevanceCache = /* @__PURE__ */ new Map();
|
|
2960
|
-
DESCRIPTION_BUDGET_CHARS =
|
|
3049
|
+
DESCRIPTION_BUDGET_CHARS = SKILL_DESCRIPTION_BUDGET_CHARS;
|
|
2961
3050
|
setUserId(userId) {
|
|
2962
3051
|
this.userId = userId;
|
|
2963
3052
|
}
|
|
@@ -2966,7 +3055,9 @@ var SkillManager = class {
|
|
|
2966
3055
|
try {
|
|
2967
3056
|
const data = await callMcpHandler("skill.load");
|
|
2968
3057
|
this.skills.clear();
|
|
2969
|
-
for (const
|
|
3058
|
+
for (const raw of data || []) {
|
|
3059
|
+
const row = safeParse(SkillRowSchema, raw);
|
|
3060
|
+
if (!row) continue;
|
|
2970
3061
|
const skill = this.rowToSkill(row);
|
|
2971
3062
|
this.skills.set(skill.name, skill);
|
|
2972
3063
|
}
|
|
@@ -2980,22 +3071,22 @@ var SkillManager = class {
|
|
|
2980
3071
|
}
|
|
2981
3072
|
rowToSkill(row) {
|
|
2982
3073
|
return {
|
|
2983
|
-
name: row.name,
|
|
2984
|
-
description: row.description
|
|
2985
|
-
version: row.version
|
|
3074
|
+
name: String(row.name),
|
|
3075
|
+
description: String(row.description ?? ""),
|
|
3076
|
+
version: String(row.version ?? "1.0.0"),
|
|
2986
3077
|
userInvocable: row.user_invocable !== false,
|
|
2987
3078
|
disableModelInvocation: row.disable_model_invocation === true,
|
|
2988
|
-
keywords: row.keywords
|
|
2989
|
-
allowedTools: row.allowed_tools
|
|
2990
|
-
argumentHint: row.argument_hint
|
|
3079
|
+
keywords: Array.isArray(row.keywords) ? row.keywords : [],
|
|
3080
|
+
allowedTools: Array.isArray(row.allowed_tools) ? row.allowed_tools : [],
|
|
3081
|
+
argumentHint: String(row.argument_hint ?? ""),
|
|
2991
3082
|
metadata: parseDbMetadata(row.metadata),
|
|
2992
|
-
homepage: row.homepage
|
|
2993
|
-
content: row.content,
|
|
3083
|
+
homepage: String(row.homepage ?? ""),
|
|
3084
|
+
content: String(row.content ?? ""),
|
|
2994
3085
|
filePath: "",
|
|
2995
3086
|
source: row.source || "manual",
|
|
2996
|
-
dbId: row.id,
|
|
2997
|
-
sourceSkillId: row.source_skill_id
|
|
2998
|
-
invocationCount: row.invocation_count
|
|
3087
|
+
dbId: row.id != null ? String(row.id) : void 0,
|
|
3088
|
+
sourceSkillId: row.source_skill_id != null ? String(row.source_skill_id) : void 0,
|
|
3089
|
+
invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0
|
|
2999
3090
|
};
|
|
3000
3091
|
}
|
|
3001
3092
|
/** Invalidate caches when skills change (create, add, update, remove). */
|
|
@@ -3125,13 +3216,14 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
3125
3216
|
metadata
|
|
3126
3217
|
}
|
|
3127
3218
|
);
|
|
3128
|
-
const
|
|
3219
|
+
const raw = Array.isArray(data) ? data[0] : data;
|
|
3220
|
+
const row = safeParse(SkillCreateResultSchema, raw);
|
|
3129
3221
|
if (!row) {
|
|
3130
|
-
log.debug(`Skill create returned
|
|
3222
|
+
log.debug(`Skill create returned invalid data for "${name}"`);
|
|
3131
3223
|
return null;
|
|
3132
3224
|
}
|
|
3133
3225
|
const id = row.out_id || row.id;
|
|
3134
|
-
const skillName = row.out_name || row.name;
|
|
3226
|
+
const skillName = row.out_name || row.name || name;
|
|
3135
3227
|
this.skills.set(skillName, {
|
|
3136
3228
|
name: skillName,
|
|
3137
3229
|
description,
|
|
@@ -3301,11 +3393,11 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
3301
3393
|
});
|
|
3302
3394
|
if (data) {
|
|
3303
3395
|
return data.map((row) => ({
|
|
3304
|
-
name: row.name,
|
|
3305
|
-
description: row.description
|
|
3306
|
-
emoji: row.emoji
|
|
3307
|
-
source: row.source
|
|
3308
|
-
invocationCount: row.invocation_count
|
|
3396
|
+
name: String(row.name),
|
|
3397
|
+
description: String(row.description ?? ""),
|
|
3398
|
+
emoji: String(row.emoji ?? ""),
|
|
3399
|
+
source: String(row.source ?? "manual"),
|
|
3400
|
+
invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0
|
|
3309
3401
|
}));
|
|
3310
3402
|
}
|
|
3311
3403
|
} catch {
|
|
@@ -3368,17 +3460,17 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
3368
3460
|
limit: options?.limit || 20,
|
|
3369
3461
|
offset: options?.offset || 0
|
|
3370
3462
|
});
|
|
3371
|
-
return (data || []).map((r) => ({
|
|
3463
|
+
return (data || []).map((r) => safeParse(BrowseSkillRowSchema, r)).filter(Boolean).map((r) => ({
|
|
3372
3464
|
id: r.id,
|
|
3373
3465
|
name: r.name,
|
|
3374
|
-
description: r.description
|
|
3375
|
-
emoji: r.emoji
|
|
3376
|
-
version: r.version
|
|
3377
|
-
authorName: r.author_name
|
|
3378
|
-
category: r.category
|
|
3379
|
-
installCount: r.install_count
|
|
3380
|
-
avgRating: r.avg_rating
|
|
3381
|
-
ratingCount: r.rating_count
|
|
3466
|
+
description: r.description,
|
|
3467
|
+
emoji: r.emoji,
|
|
3468
|
+
version: r.version,
|
|
3469
|
+
authorName: r.author_name,
|
|
3470
|
+
category: r.category,
|
|
3471
|
+
installCount: r.install_count,
|
|
3472
|
+
avgRating: r.avg_rating ?? null,
|
|
3473
|
+
ratingCount: r.rating_count
|
|
3382
3474
|
}));
|
|
3383
3475
|
} catch {
|
|
3384
3476
|
return [];
|
|
@@ -3403,8 +3495,12 @@ function substituteArguments(content, args) {
|
|
|
3403
3495
|
content = content.replace(/\$(\d+)(?!\w)/g, (_, i) => parts[parseInt(i)] || "");
|
|
3404
3496
|
return content;
|
|
3405
3497
|
}
|
|
3498
|
+
var SAFE_DYNAMIC_COMMANDS = /^(date|whoami|hostname|uname|pwd|echo|node\s+--version|npm\s+--version|git\s+(branch|rev-parse|log\s+--oneline)|cat\s+)/;
|
|
3406
3499
|
function preprocessDynamicContext(content, cwd) {
|
|
3407
3500
|
return content.replace(/!`([^`]+)`/g, (_, cmd) => {
|
|
3501
|
+
if (!SAFE_DYNAMIC_COMMANDS.test(cmd.trim())) {
|
|
3502
|
+
return `[command blocked: ${cmd}]`;
|
|
3503
|
+
}
|
|
3408
3504
|
try {
|
|
3409
3505
|
return execSync2(cmd, { timeout: 1e4, encoding: "utf-8", cwd }).trim();
|
|
3410
3506
|
} catch {
|
|
@@ -3417,6 +3513,36 @@ function preprocessDynamicContext(content, cwd) {
|
|
|
3417
3513
|
import {
|
|
3418
3514
|
query
|
|
3419
3515
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
3516
|
+
var SKILL_DECISION_OUTPUT_FORMAT = {
|
|
3517
|
+
type: "json_schema",
|
|
3518
|
+
schema: {
|
|
3519
|
+
type: "object",
|
|
3520
|
+
properties: {
|
|
3521
|
+
action: { type: "string", enum: ["create", "update", "skip"] },
|
|
3522
|
+
name: { type: "string" },
|
|
3523
|
+
description: { type: "string" },
|
|
3524
|
+
instructions: { type: "string" },
|
|
3525
|
+
emoji: { type: "string" },
|
|
3526
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
3527
|
+
existing_skill_name: { type: "string" },
|
|
3528
|
+
improved_instructions: { type: "string" },
|
|
3529
|
+
improved_description: { type: "string" },
|
|
3530
|
+
reason: { type: "string" }
|
|
3531
|
+
},
|
|
3532
|
+
required: ["action", "reason"]
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
var SKILL_VALIDATION_OUTPUT_FORMAT = {
|
|
3536
|
+
type: "json_schema",
|
|
3537
|
+
schema: {
|
|
3538
|
+
type: "object",
|
|
3539
|
+
properties: {
|
|
3540
|
+
valid: { type: "boolean" },
|
|
3541
|
+
improvements: { type: "string" }
|
|
3542
|
+
},
|
|
3543
|
+
required: ["valid"]
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3420
3546
|
var SKILL_EVALUATION_PROMPT = `You just completed a task. Now evaluate whether it should be saved as a reusable Agent Skill.
|
|
3421
3547
|
|
|
3422
3548
|
## Agent Skills Format (agentskills.io)
|
|
@@ -3444,6 +3570,22 @@ Respond with ONLY a JSON object (no markdown, no explanation outside the JSON).
|
|
|
3444
3570
|
Always include "reason" explaining your decision.
|
|
3445
3571
|
|
|
3446
3572
|
Use your judgment \u2014 no rigid rules. Consider: Is this repeatable? Can it be generalized? Would it save time next time?`;
|
|
3573
|
+
var SKILL_VALIDATION_PROMPT = `Validate this auto-generated skill before it becomes active.
|
|
3574
|
+
|
|
3575
|
+
Check:
|
|
3576
|
+
1. Are the instructions clear, complete, and actionable?
|
|
3577
|
+
2. Do they use generic placeholders (not hardcoded values)?
|
|
3578
|
+
3. Are error handling steps included?
|
|
3579
|
+
4. Is the description accurate and searchable?
|
|
3580
|
+
5. Would this actually work if followed step-by-step?
|
|
3581
|
+
|
|
3582
|
+
Respond with ONLY a JSON object:
|
|
3583
|
+
- {"valid": true, "improvements": null}
|
|
3584
|
+
- {"valid": false, "improvements": "Specific improvements needed"}
|
|
3585
|
+
- {"valid": true, "improvements": "Optional minor improvements"}
|
|
3586
|
+
|
|
3587
|
+
Skill to validate:
|
|
3588
|
+
`;
|
|
3447
3589
|
async function evaluateAndMaybeCreateSkill(opts) {
|
|
3448
3590
|
const { sessionId, skillManager, model } = opts;
|
|
3449
3591
|
if (!sessionId) {
|
|
@@ -3459,58 +3601,93 @@ ${existingList}
|
|
|
3459
3601
|
|
|
3460
3602
|
Respond with a JSON object now.`;
|
|
3461
3603
|
try {
|
|
3462
|
-
let
|
|
3604
|
+
let structuredOutput;
|
|
3463
3605
|
for await (const message of query({
|
|
3464
3606
|
prompt,
|
|
3465
3607
|
options: {
|
|
3466
3608
|
resume: sessionId,
|
|
3467
3609
|
model,
|
|
3468
3610
|
maxTurns: 1,
|
|
3469
|
-
allowedTools: []
|
|
3611
|
+
allowedTools: [],
|
|
3612
|
+
effort: "low",
|
|
3613
|
+
outputFormat: SKILL_DECISION_OUTPUT_FORMAT
|
|
3470
3614
|
}
|
|
3471
3615
|
})) {
|
|
3472
|
-
if (message.type === "
|
|
3473
|
-
const assistantMsg = message;
|
|
3474
|
-
for (const block of assistantMsg.message.content) {
|
|
3475
|
-
if (block.type === "text") {
|
|
3476
|
-
responseText += block.text;
|
|
3477
|
-
}
|
|
3478
|
-
}
|
|
3479
|
-
} else if (message.type === "result") {
|
|
3616
|
+
if (message.type === "result") {
|
|
3480
3617
|
const resultMsg = message;
|
|
3481
|
-
if (resultMsg.subtype === "success"
|
|
3482
|
-
|
|
3618
|
+
if (resultMsg.subtype === "success") {
|
|
3619
|
+
const successMsg = resultMsg;
|
|
3620
|
+
structuredOutput = successMsg.structured_output;
|
|
3621
|
+
log.debug(
|
|
3622
|
+
`Skill evaluation cost: $${successMsg.total_cost_usd.toFixed(4)}`
|
|
3623
|
+
);
|
|
3483
3624
|
}
|
|
3484
3625
|
}
|
|
3485
3626
|
}
|
|
3486
|
-
const decision =
|
|
3627
|
+
const decision = structuredOutput ? safeParse(SkillDecisionSchema, structuredOutput) : null;
|
|
3487
3628
|
if (!decision) {
|
|
3488
3629
|
log.debug("Skill evaluation: no valid JSON in response");
|
|
3489
3630
|
return;
|
|
3490
3631
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3632
|
+
await executeSkillDecision(decision, skillManager, sessionId, model);
|
|
3633
|
+
} catch (err) {
|
|
3634
|
+
log.debug(`Skill evaluation error: ${errorMessage(err)}`);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
async function validateSkill(name, description, instructions, sessionId, model) {
|
|
3638
|
+
try {
|
|
3639
|
+
const skillDoc = `Name: ${name}
|
|
3640
|
+
Description: ${description}
|
|
3641
|
+
|
|
3642
|
+
Instructions:
|
|
3643
|
+
${instructions}`;
|
|
3644
|
+
let structuredOutput;
|
|
3645
|
+
for await (const message of query({
|
|
3646
|
+
prompt: SKILL_VALIDATION_PROMPT + skillDoc,
|
|
3647
|
+
options: {
|
|
3648
|
+
resume: sessionId,
|
|
3649
|
+
model,
|
|
3650
|
+
maxTurns: SKILL_VALIDATION_MAX_TURNS,
|
|
3651
|
+
allowedTools: [],
|
|
3652
|
+
effort: "low",
|
|
3653
|
+
outputFormat: SKILL_VALIDATION_OUTPUT_FORMAT
|
|
3654
|
+
}
|
|
3655
|
+
})) {
|
|
3656
|
+
if (message.type === "result") {
|
|
3657
|
+
const resultMsg = message;
|
|
3658
|
+
if (resultMsg.subtype === "success") {
|
|
3659
|
+
structuredOutput = resultMsg.structured_output;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
const parsed = structuredOutput;
|
|
3664
|
+
if (parsed) {
|
|
3665
|
+
return { valid: parsed.valid, improvements: parsed.improvements || void 0 };
|
|
3494
3666
|
}
|
|
3495
|
-
|
|
3667
|
+
return { valid: true };
|
|
3496
3668
|
} catch (err) {
|
|
3497
|
-
log.debug(`Skill
|
|
3669
|
+
log.debug(`Skill validation error: ${errorMessage(err)}`);
|
|
3670
|
+
return { valid: true };
|
|
3498
3671
|
}
|
|
3499
3672
|
}
|
|
3500
|
-
async function executeSkillDecision(decision, skillManager) {
|
|
3673
|
+
async function executeSkillDecision(decision, skillManager, sessionId, model) {
|
|
3501
3674
|
switch (decision.action) {
|
|
3502
3675
|
case "create": {
|
|
3503
3676
|
if (!decision.name || !decision.instructions) {
|
|
3504
3677
|
log.debug("Skill create skipped: missing name or instructions");
|
|
3505
3678
|
return;
|
|
3506
3679
|
}
|
|
3507
|
-
|
|
3508
|
-
if (
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3680
|
+
const skillName = normalizeSkillName(decision.name);
|
|
3681
|
+
if (!skillName) {
|
|
3682
|
+
log.debug(`Skill create skipped: name "${decision.name}" cannot be normalized`);
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
const validationError = validateSkillName(skillName);
|
|
3686
|
+
if (validationError) {
|
|
3687
|
+
log.debug(`Skill create skipped: ${validationError}`);
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
if (skillName !== decision.name) {
|
|
3514
3691
|
log.debug(`Normalized skill name: "${decision.name}" \u2192 "${skillName}"`);
|
|
3515
3692
|
}
|
|
3516
3693
|
const existing = skillManager.findSimilar(skillName);
|
|
@@ -3518,10 +3695,33 @@ async function executeSkillDecision(decision, skillManager) {
|
|
|
3518
3695
|
log.debug(`Skill create skipped: similar skill "${existing.name}" exists`);
|
|
3519
3696
|
return;
|
|
3520
3697
|
}
|
|
3698
|
+
let instructions = decision.instructions;
|
|
3699
|
+
if (sessionId) {
|
|
3700
|
+
log.debug(`Validating skill "${skillName}" before activation...`);
|
|
3701
|
+
const validation = await validateSkill(
|
|
3702
|
+
skillName,
|
|
3703
|
+
decision.description || "",
|
|
3704
|
+
instructions,
|
|
3705
|
+
sessionId,
|
|
3706
|
+
model
|
|
3707
|
+
);
|
|
3708
|
+
if (!validation.valid) {
|
|
3709
|
+
log.info(
|
|
3710
|
+
`Skill "${skillName}" failed validation: ${validation.improvements}. Skipping creation.`
|
|
3711
|
+
);
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
if (validation.improvements) {
|
|
3715
|
+
log.debug(`Skill "${skillName}" validated with suggestions: ${validation.improvements}`);
|
|
3716
|
+
instructions += `
|
|
3717
|
+
|
|
3718
|
+
<!-- Validation notes: ${validation.improvements} -->`;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3521
3721
|
const result = await skillManager.create(
|
|
3522
3722
|
skillName,
|
|
3523
3723
|
decision.description || "",
|
|
3524
|
-
|
|
3724
|
+
instructions,
|
|
3525
3725
|
{
|
|
3526
3726
|
source: "auto_extracted",
|
|
3527
3727
|
emoji: decision.emoji,
|
|
@@ -3532,7 +3732,7 @@ async function executeSkillDecision(decision, skillManager) {
|
|
|
3532
3732
|
await skillManager.syncToAgentSkills(
|
|
3533
3733
|
skillName,
|
|
3534
3734
|
decision.description || "",
|
|
3535
|
-
|
|
3735
|
+
instructions,
|
|
3536
3736
|
"1.0.0",
|
|
3537
3737
|
{
|
|
3538
3738
|
source: "auto_extracted",
|
|
@@ -3541,7 +3741,7 @@ async function executeSkillDecision(decision, skillManager) {
|
|
|
3541
3741
|
sourceSkillId: result.id
|
|
3542
3742
|
}
|
|
3543
3743
|
);
|
|
3544
|
-
log.info(`Auto-created skill "${skillName}": ${decision.reason}`);
|
|
3744
|
+
log.info(`Auto-created skill "${skillName}" (validated): ${decision.reason}`);
|
|
3545
3745
|
}
|
|
3546
3746
|
break;
|
|
3547
3747
|
}
|
|
@@ -3550,6 +3750,21 @@ async function executeSkillDecision(decision, skillManager) {
|
|
|
3550
3750
|
log.debug("Skill update skipped: missing skill name or instructions");
|
|
3551
3751
|
return;
|
|
3552
3752
|
}
|
|
3753
|
+
if (sessionId) {
|
|
3754
|
+
const validation = await validateSkill(
|
|
3755
|
+
decision.existing_skill_name,
|
|
3756
|
+
decision.improved_description || "",
|
|
3757
|
+
decision.improved_instructions,
|
|
3758
|
+
sessionId,
|
|
3759
|
+
model
|
|
3760
|
+
);
|
|
3761
|
+
if (!validation.valid) {
|
|
3762
|
+
log.info(
|
|
3763
|
+
`Skill update for "${decision.existing_skill_name}" failed validation. Skipping.`
|
|
3764
|
+
);
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3553
3768
|
const updated = skillManager.update(
|
|
3554
3769
|
decision.existing_skill_name,
|
|
3555
3770
|
decision.improved_instructions,
|
|
@@ -3567,29 +3782,6 @@ async function executeSkillDecision(decision, skillManager) {
|
|
|
3567
3782
|
break;
|
|
3568
3783
|
}
|
|
3569
3784
|
}
|
|
3570
|
-
function parseJsonResponse(text) {
|
|
3571
|
-
const trimmed = text.trim();
|
|
3572
|
-
try {
|
|
3573
|
-
const parsed = JSON.parse(trimmed);
|
|
3574
|
-
if (parsed.action) return parsed;
|
|
3575
|
-
} catch {
|
|
3576
|
-
}
|
|
3577
|
-
const start = trimmed.indexOf("{");
|
|
3578
|
-
if (start === -1) return null;
|
|
3579
|
-
let depth = 0;
|
|
3580
|
-
for (let i = start; i < trimmed.length; i++) {
|
|
3581
|
-
if (trimmed[i] === "{") depth++;
|
|
3582
|
-
else if (trimmed[i] === "}") depth--;
|
|
3583
|
-
if (depth === 0) {
|
|
3584
|
-
try {
|
|
3585
|
-
return JSON.parse(trimmed.slice(start, i + 1));
|
|
3586
|
-
} catch {
|
|
3587
|
-
return null;
|
|
3588
|
-
}
|
|
3589
|
-
}
|
|
3590
|
-
}
|
|
3591
|
-
return null;
|
|
3592
|
-
}
|
|
3593
3785
|
|
|
3594
3786
|
// src/utils/retry.ts
|
|
3595
3787
|
async function withRetry(fn, opts = {}) {
|
|
@@ -3634,14 +3826,16 @@ import { z } from "zod/v4";
|
|
|
3634
3826
|
|
|
3635
3827
|
// src/tools/filesystem.ts
|
|
3636
3828
|
import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
|
|
3637
|
-
import { resolve, relative, join as join2 } from "path";
|
|
3829
|
+
import { resolve, relative, join as join2, sep } from "path";
|
|
3638
3830
|
import { glob } from "glob";
|
|
3639
3831
|
function assertWithinWorkspace(filePath) {
|
|
3640
3832
|
const config = getConfig();
|
|
3641
3833
|
const resolved = resolve(config.workspacePath, filePath);
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3834
|
+
const rel = relative(config.workspacePath, resolved);
|
|
3835
|
+
if (rel.startsWith("..") || rel.startsWith(sep + sep)) {
|
|
3836
|
+
throw new AppError(
|
|
3837
|
+
`Access denied: path "${filePath}" is outside workspace "${config.workspacePath}"`,
|
|
3838
|
+
"PATH_TRAVERSAL"
|
|
3645
3839
|
);
|
|
3646
3840
|
}
|
|
3647
3841
|
return resolved;
|
|
@@ -3671,7 +3865,7 @@ async function searchFiles(pattern, directory) {
|
|
|
3671
3865
|
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
3672
3866
|
});
|
|
3673
3867
|
if (matches.length === 0) return "No files found matching the pattern.";
|
|
3674
|
-
return matches.slice(0,
|
|
3868
|
+
return matches.slice(0, MAX_FILE_SEARCH_RESULTS).map((m) => relative(config.workspacePath, join2(cwd, m))).join("\n");
|
|
3675
3869
|
}
|
|
3676
3870
|
async function listDirectory(path) {
|
|
3677
3871
|
const config = getConfig();
|
|
@@ -3681,9 +3875,7 @@ async function listDirectory(path) {
|
|
|
3681
3875
|
for (const entry of entries) {
|
|
3682
3876
|
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
3683
3877
|
const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
3684
|
-
const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then(
|
|
3685
|
-
(s) => ` (${formatSize(s.size)})`
|
|
3686
|
-
) : "";
|
|
3878
|
+
const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then((s) => ` (${formatSize(s.size)})`) : "";
|
|
3687
3879
|
results.push(`${icon} ${entry.name}${info}`);
|
|
3688
3880
|
}
|
|
3689
3881
|
return results.join("\n") || "Empty directory.";
|
|
@@ -3701,9 +3893,14 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
3701
3893
|
nodir: true,
|
|
3702
3894
|
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
3703
3895
|
});
|
|
3704
|
-
|
|
3896
|
+
let regex;
|
|
3897
|
+
try {
|
|
3898
|
+
regex = new RegExp(pattern, "gi");
|
|
3899
|
+
} catch {
|
|
3900
|
+
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
3901
|
+
}
|
|
3705
3902
|
const results = [];
|
|
3706
|
-
for (const file of files.slice(0,
|
|
3903
|
+
for (const file of files.slice(0, MAX_CONTENT_SEARCH_FILES)) {
|
|
3707
3904
|
try {
|
|
3708
3905
|
const content = await readFile(join2(cwd, file), "utf-8");
|
|
3709
3906
|
const lines = content.split("\n");
|
|
@@ -3712,10 +3909,10 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
3712
3909
|
const relPath = relative(config.workspacePath, join2(cwd, file));
|
|
3713
3910
|
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
3714
3911
|
regex.lastIndex = 0;
|
|
3715
|
-
if (results.length >=
|
|
3912
|
+
if (results.length >= MAX_CONTENT_SEARCH_RESULTS) break;
|
|
3716
3913
|
}
|
|
3717
3914
|
}
|
|
3718
|
-
if (results.length >=
|
|
3915
|
+
if (results.length >= MAX_CONTENT_SEARCH_RESULTS) break;
|
|
3719
3916
|
} catch {
|
|
3720
3917
|
}
|
|
3721
3918
|
}
|
|
@@ -3724,8 +3921,6 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
3724
3921
|
|
|
3725
3922
|
// src/tools/shell.ts
|
|
3726
3923
|
import { exec } from "child_process";
|
|
3727
|
-
var TIMEOUT_MS = 3e4;
|
|
3728
|
-
var MAX_OUTPUT = 5e4;
|
|
3729
3924
|
var BLOCKED_PATTERNS = [
|
|
3730
3925
|
/rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i,
|
|
3731
3926
|
// rm -rf /, rm -fr /, etc.
|
|
@@ -3753,7 +3948,7 @@ function isBlocked(command) {
|
|
|
3753
3948
|
}
|
|
3754
3949
|
async function executeShell(command, cwd) {
|
|
3755
3950
|
if (isBlocked(command)) {
|
|
3756
|
-
throw new
|
|
3951
|
+
throw new AppError(`Command blocked for safety: "${command}"`, "COMMAND_BLOCKED");
|
|
3757
3952
|
}
|
|
3758
3953
|
const config = getConfig();
|
|
3759
3954
|
const workDir = cwd || config.workspacePath;
|
|
@@ -3762,7 +3957,7 @@ async function executeShell(command, cwd) {
|
|
|
3762
3957
|
command,
|
|
3763
3958
|
{
|
|
3764
3959
|
cwd: workDir,
|
|
3765
|
-
timeout:
|
|
3960
|
+
timeout: SHELL_TIMEOUT_MS,
|
|
3766
3961
|
maxBuffer: 1024 * 1024,
|
|
3767
3962
|
// 1MB buffer
|
|
3768
3963
|
env: { ...process.env, TERM: "dumb" }
|
|
@@ -3780,10 +3975,10 @@ ${stderr}` : "";
|
|
|
3780
3975
|
if (error && !stdout && !stderr) {
|
|
3781
3976
|
output = `Error: ${error.message}`;
|
|
3782
3977
|
}
|
|
3783
|
-
if (output.length >
|
|
3784
|
-
output = output.slice(0,
|
|
3978
|
+
if (output.length > SHELL_MAX_OUTPUT) {
|
|
3979
|
+
output = output.slice(0, SHELL_MAX_OUTPUT) + `
|
|
3785
3980
|
|
|
3786
|
-
[Output truncated at ${
|
|
3981
|
+
[Output truncated at ${SHELL_MAX_OUTPUT} bytes]`;
|
|
3787
3982
|
}
|
|
3788
3983
|
resolve2(output || "(no output)");
|
|
3789
3984
|
}
|
|
@@ -5436,14 +5631,14 @@ function stripMcpPrefix(toolName) {
|
|
|
5436
5631
|
const match = toolName.match(/^mcp__[^_]+(?:__)?(.+)$/);
|
|
5437
5632
|
return match ? match[1] : toolName;
|
|
5438
5633
|
}
|
|
5439
|
-
function createEventHooks(taskId, toolCallRecords) {
|
|
5634
|
+
function createEventHooks(taskId, toolCallRecords, toolFailures = []) {
|
|
5440
5635
|
const preToolUseHook = async (input) => {
|
|
5441
5636
|
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
5442
5637
|
const preInput = input;
|
|
5443
5638
|
const rawName = preInput.tool_name;
|
|
5444
5639
|
const displayName = stripMcpPrefix(rawName);
|
|
5445
5640
|
const toolInput = preInput.tool_input;
|
|
5446
|
-
log.tool(displayName, JSON.stringify(toolInput).slice(0,
|
|
5641
|
+
log.tool(displayName, JSON.stringify(toolInput).slice(0, MAX_TOOL_INPUT_LOG_LENGTH));
|
|
5447
5642
|
await emitEvent(taskId, "tool_use_start", { name: displayName });
|
|
5448
5643
|
await emitEvent(taskId, "tool_use_input", { input: toolInput });
|
|
5449
5644
|
if (displayName === "browser_request_user_action") {
|
|
@@ -5462,21 +5657,42 @@ function createEventHooks(taskId, toolCallRecords) {
|
|
|
5462
5657
|
const toolInput = postInput.tool_input;
|
|
5463
5658
|
const toolResponse = postInput.tool_response;
|
|
5464
5659
|
const resultStr = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
|
|
5465
|
-
log.result(resultStr.slice(0,
|
|
5660
|
+
log.result(resultStr.slice(0, MAX_TOOL_INPUT_LOG_LENGTH));
|
|
5466
5661
|
await emitEvent(taskId, "tool_result", {
|
|
5467
5662
|
name: displayName,
|
|
5468
|
-
result: resultStr.slice(0,
|
|
5663
|
+
result: resultStr.slice(0, MAX_TOOL_RESULT_LENGTH)
|
|
5469
5664
|
});
|
|
5470
5665
|
toolCallRecords.push({
|
|
5471
5666
|
name: displayName,
|
|
5472
5667
|
input: toolInput || {},
|
|
5473
|
-
result: resultStr.slice(0,
|
|
5668
|
+
result: resultStr.slice(0, MAX_SKILL_RECORD_RESULT_LENGTH)
|
|
5474
5669
|
});
|
|
5475
5670
|
return {};
|
|
5476
5671
|
};
|
|
5672
|
+
const postToolUseFailureHook = async (input) => {
|
|
5673
|
+
if (input.hook_event_name !== "PostToolUseFailure") return {};
|
|
5674
|
+
const failureInput = input;
|
|
5675
|
+
const rawName = failureInput.tool_name;
|
|
5676
|
+
const displayName = stripMcpPrefix(rawName);
|
|
5677
|
+
const errorStr = failureInput.error;
|
|
5678
|
+
toolFailures.push({
|
|
5679
|
+
toolName: displayName,
|
|
5680
|
+
input: failureInput.tool_input || {},
|
|
5681
|
+
error: errorStr.slice(0, 500),
|
|
5682
|
+
timestamp: Date.now()
|
|
5683
|
+
});
|
|
5684
|
+
await emitEvent(taskId, "tool_failure", {
|
|
5685
|
+
name: displayName,
|
|
5686
|
+
error: errorStr.slice(0, 500),
|
|
5687
|
+
failure_count: toolFailures.filter((f) => f.toolName === displayName).length
|
|
5688
|
+
});
|
|
5689
|
+
log.warn(`Tool failure tracked: ${displayName} (total: ${toolFailures.length})`);
|
|
5690
|
+
return {};
|
|
5691
|
+
};
|
|
5477
5692
|
return {
|
|
5478
5693
|
PreToolUse: [{ hooks: [preToolUseHook] }],
|
|
5479
|
-
PostToolUse: [{ hooks: [postToolUseHook] }]
|
|
5694
|
+
PostToolUse: [{ hooks: [postToolUseHook] }],
|
|
5695
|
+
PostToolUseFailure: [{ hooks: [postToolUseFailureHook] }]
|
|
5480
5696
|
};
|
|
5481
5697
|
}
|
|
5482
5698
|
|
|
@@ -5588,7 +5804,16 @@ Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
|
|
|
5588
5804
|
4. Check the screenshot \u2014 if validation errors appear, re-snapshot and fix
|
|
5589
5805
|
5. When a username/email is taken, append a random 4-digit suffix and retry
|
|
5590
5806
|
|
|
5807
|
+
7. FAILURE RECOVERY \u2014 Strategy Switching:
|
|
5808
|
+
If a tool call fails, do NOT repeat the same call. Reflect on why it failed and switch strategy:
|
|
5809
|
+
- CSS selector fails \u2192 use browser_snapshot refs instead
|
|
5810
|
+
- Direct navigation fails \u2192 search for the page first
|
|
5811
|
+
- API/programmatic approach fails \u2192 use browser UI instead
|
|
5812
|
+
- One data source fails \u2192 try an alternative source
|
|
5813
|
+
- If stuck after 2 failed attempts at the same step, try a fundamentally different approach
|
|
5814
|
+
|
|
5591
5815
|
Guidelines:
|
|
5816
|
+
- 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.
|
|
5592
5817
|
- Always use the real browser for web tasks, never try to fetch URLs programmatically
|
|
5593
5818
|
- ALWAYS use browser_snapshot as your primary way to understand a page \u2014 the ref table gives actionable refs, the screenshot gives visual context
|
|
5594
5819
|
- Use browser_act to batch multiple actions \u2014 fill an entire form in one call instead of individual clicks/types
|
|
@@ -5649,18 +5874,22 @@ var TaskTimeout = class {
|
|
|
5649
5874
|
}
|
|
5650
5875
|
}
|
|
5651
5876
|
};
|
|
5652
|
-
var MAX_HISTORY_ENTRIES = 10;
|
|
5653
|
-
var MAX_RESPONSE_LENGTH = 1500;
|
|
5654
5877
|
var TaskProcessor = class {
|
|
5655
5878
|
memoryManager = null;
|
|
5656
5879
|
skillManager;
|
|
5657
5880
|
sessionId = null;
|
|
5881
|
+
userId = null;
|
|
5658
5882
|
/** In-memory conversation history, keyed by conversation_id */
|
|
5659
5883
|
historyCache = /* @__PURE__ */ new Map();
|
|
5660
5884
|
constructor() {
|
|
5661
5885
|
this.skillManager = new SkillManager();
|
|
5662
5886
|
}
|
|
5887
|
+
/** @deprecated Use setUserId() instead */
|
|
5663
5888
|
init(userId) {
|
|
5889
|
+
this.setUserId(userId);
|
|
5890
|
+
}
|
|
5891
|
+
setUserId(userId) {
|
|
5892
|
+
this.userId = userId;
|
|
5664
5893
|
this.memoryManager = new MemoryManager();
|
|
5665
5894
|
this.skillManager.setUserId(userId);
|
|
5666
5895
|
this.skillManager.loadFromDb().catch((err) => {
|
|
@@ -5685,11 +5914,12 @@ var TaskProcessor = class {
|
|
|
5685
5914
|
async processTask(task) {
|
|
5686
5915
|
const config = getConfig();
|
|
5687
5916
|
resetEventSequence();
|
|
5688
|
-
const taskTimeoutMs =
|
|
5917
|
+
const taskTimeoutMs = config.taskTimeoutMinutes * 6e4;
|
|
5689
5918
|
newCorrelationId();
|
|
5690
5919
|
log.info(`Processing task ${task.id.slice(0, 8)}...`);
|
|
5691
5920
|
let finalResponse = "";
|
|
5692
5921
|
const toolCallRecords = [];
|
|
5922
|
+
const toolFailures = [];
|
|
5693
5923
|
let tokenUsage;
|
|
5694
5924
|
let agentSessionId;
|
|
5695
5925
|
try {
|
|
@@ -5724,7 +5954,7 @@ var TaskProcessor = class {
|
|
|
5724
5954
|
for (const entry of history) {
|
|
5725
5955
|
historyPrompt += `User: ${entry.prompt}
|
|
5726
5956
|
`;
|
|
5727
|
-
const truncated = entry.response.length >
|
|
5957
|
+
const truncated = entry.response.length > MAX_HISTORY_RESPONSE_LENGTH ? entry.response.slice(0, MAX_HISTORY_RESPONSE_LENGTH) + "\u2026" : entry.response;
|
|
5728
5958
|
historyPrompt += `Assistant: ${truncated}
|
|
5729
5959
|
|
|
5730
5960
|
`;
|
|
@@ -5742,18 +5972,15 @@ var TaskProcessor = class {
|
|
|
5742
5972
|
onUserWaitStart: () => taskTimeout.pause(),
|
|
5743
5973
|
onUserWaitEnd: () => taskTimeout.resume()
|
|
5744
5974
|
});
|
|
5745
|
-
const eventHooks = createEventHooks(task.id, toolCallRecords);
|
|
5975
|
+
const eventHooks = createEventHooks(task.id, toolCallRecords, toolFailures);
|
|
5746
5976
|
const allowedTools = [
|
|
5747
|
-
// SDK built-in tools
|
|
5748
5977
|
"Read",
|
|
5749
5978
|
"Write",
|
|
5750
5979
|
"Edit",
|
|
5751
5980
|
"Bash",
|
|
5752
5981
|
"Glob",
|
|
5753
5982
|
"Grep",
|
|
5754
|
-
// Browser MCP tools
|
|
5755
5983
|
...BROWSER_TOOL_NAMES.map((n) => `mcp__assistme-browser__${n}`),
|
|
5756
|
-
// Agent MCP tools (memory, skills)
|
|
5757
5984
|
"mcp__assistme-agent__memory_store",
|
|
5758
5985
|
"mcp__assistme-agent__skill_create",
|
|
5759
5986
|
"mcp__assistme-agent__skill_improve",
|
|
@@ -5764,29 +5991,19 @@ var TaskProcessor = class {
|
|
|
5764
5991
|
"mcp__assistme-agent__skill_browse",
|
|
5765
5992
|
"mcp__assistme-agent__skill_add",
|
|
5766
5993
|
"mcp__assistme-agent__skill_publish",
|
|
5767
|
-
// User interaction
|
|
5768
5994
|
"mcp__assistme-agent__ask_user",
|
|
5769
|
-
// Job automation tools
|
|
5770
5995
|
"mcp__assistme-agent__job_run",
|
|
5771
5996
|
"mcp__assistme-agent__job_schedule",
|
|
5772
5997
|
"mcp__assistme-agent__job_status",
|
|
5773
|
-
// Credential tools (local storage)
|
|
5774
5998
|
"mcp__assistme-agent__credential_get",
|
|
5775
5999
|
"mcp__assistme-agent__credential_set",
|
|
5776
6000
|
"mcp__assistme-agent__credential_list",
|
|
5777
6001
|
"mcp__assistme-agent__credential_remove"
|
|
5778
6002
|
];
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
role: "user",
|
|
5784
|
-
content: task.prompt
|
|
5785
|
-
},
|
|
5786
|
-
parent_tool_use_id: null,
|
|
5787
|
-
session_id: ""
|
|
5788
|
-
};
|
|
5789
|
-
}
|
|
6003
|
+
const mcpServers = {
|
|
6004
|
+
"assistme-browser": browserServer,
|
|
6005
|
+
"assistme-agent": agentToolsServer
|
|
6006
|
+
};
|
|
5790
6007
|
const options = {
|
|
5791
6008
|
model: config.model,
|
|
5792
6009
|
systemPrompt,
|
|
@@ -5795,24 +6012,16 @@ var TaskProcessor = class {
|
|
|
5795
6012
|
allowedTools,
|
|
5796
6013
|
permissionMode: "bypassPermissions",
|
|
5797
6014
|
allowDangerouslySkipPermissions: true,
|
|
5798
|
-
mcpServers
|
|
5799
|
-
"assistme-browser": browserServer,
|
|
5800
|
-
"assistme-agent": agentToolsServer
|
|
5801
|
-
},
|
|
6015
|
+
mcpServers,
|
|
5802
6016
|
hooks: eventHooks,
|
|
5803
6017
|
persistSession: true,
|
|
5804
|
-
abortController
|
|
6018
|
+
abortController,
|
|
6019
|
+
thinking: { type: "adaptive" },
|
|
6020
|
+
effort: "high",
|
|
6021
|
+
maxBudgetUsd: MAX_BUDGET_USD
|
|
5805
6022
|
};
|
|
5806
|
-
const taskStartTime = Date.now();
|
|
5807
6023
|
try {
|
|
5808
|
-
for await (const message of query2({
|
|
5809
|
-
prompt: promptMessages(),
|
|
5810
|
-
options
|
|
5811
|
-
})) {
|
|
5812
|
-
if (Date.now() - taskStartTime > taskTimeoutMs) {
|
|
5813
|
-
finalResponse += "\n\n[Task timed out]";
|
|
5814
|
-
break;
|
|
5815
|
-
}
|
|
6024
|
+
for await (const message of query2({ prompt: task.prompt, options })) {
|
|
5816
6025
|
switch (message.type) {
|
|
5817
6026
|
case "assistant": {
|
|
5818
6027
|
const assistantMsg = message;
|
|
@@ -5820,15 +6029,11 @@ var TaskProcessor = class {
|
|
|
5820
6029
|
if (block.type === "text") {
|
|
5821
6030
|
finalResponse += block.text;
|
|
5822
6031
|
log.agent(block.text);
|
|
5823
|
-
await emitEvent(task.id, "text_delta", {
|
|
5824
|
-
text: block.text
|
|
5825
|
-
});
|
|
6032
|
+
await emitEvent(task.id, "text_delta", { text: block.text });
|
|
5826
6033
|
} else if (block.type === "thinking" && "thinking" in block) {
|
|
5827
|
-
const
|
|
5828
|
-
log.debug(`Thinking: ${
|
|
5829
|
-
await emitEvent(task.id, "thinking", {
|
|
5830
|
-
text: thinkingText
|
|
5831
|
-
});
|
|
6034
|
+
const thinkingBlock = block;
|
|
6035
|
+
log.debug(`Thinking: ${thinkingBlock.thinking.slice(0, 100)}...`);
|
|
6036
|
+
await emitEvent(task.id, "thinking", { text: thinkingBlock.thinking });
|
|
5832
6037
|
}
|
|
5833
6038
|
}
|
|
5834
6039
|
break;
|
|
@@ -5844,21 +6049,25 @@ var TaskProcessor = class {
|
|
|
5844
6049
|
if (!finalResponse && successMsg.result) {
|
|
5845
6050
|
finalResponse = successMsg.result;
|
|
5846
6051
|
}
|
|
6052
|
+
agentSessionId = successMsg.session_id;
|
|
5847
6053
|
log.info(
|
|
5848
6054
|
`Task cost: $${successMsg.total_cost_usd.toFixed(4)}, turns: ${successMsg.num_turns}`
|
|
5849
6055
|
);
|
|
5850
6056
|
} else {
|
|
5851
|
-
const
|
|
5852
|
-
log.warn(`SDK result: ${
|
|
5853
|
-
for (const err of
|
|
6057
|
+
const errMsg = resultMsg;
|
|
6058
|
+
log.warn(`SDK result: ${errMsg.subtype}`);
|
|
6059
|
+
for (const err of errMsg.errors) {
|
|
5854
6060
|
await emitEvent(task.id, "error", { message: err });
|
|
5855
6061
|
}
|
|
5856
6062
|
}
|
|
5857
6063
|
break;
|
|
5858
6064
|
}
|
|
5859
6065
|
default:
|
|
5860
|
-
if (message.type === "system" && "subtype" in message
|
|
5861
|
-
|
|
6066
|
+
if (message.type === "system" && "subtype" in message) {
|
|
6067
|
+
const sysMsg = message;
|
|
6068
|
+
if (sysMsg.subtype === "init" && sysMsg.session_id) {
|
|
6069
|
+
agentSessionId = sysMsg.session_id;
|
|
6070
|
+
}
|
|
5862
6071
|
}
|
|
5863
6072
|
log.debug(`SDK message type: ${message.type}`);
|
|
5864
6073
|
break;
|
|
@@ -5867,10 +6076,9 @@ var TaskProcessor = class {
|
|
|
5867
6076
|
} finally {
|
|
5868
6077
|
taskTimeout.clear();
|
|
5869
6078
|
}
|
|
5870
|
-
const
|
|
5871
|
-
const truncatedResponse = finalResponse.length > MAX_CONTENT_LENGTH ? finalResponse.slice(0, MAX_CONTENT_LENGTH) + "\n\n[Response truncated]" : finalResponse;
|
|
6079
|
+
const truncatedResponse = finalResponse.length > MAX_RESPONSE_CONTENT_LENGTH ? finalResponse.slice(0, MAX_RESPONSE_CONTENT_LENGTH) + "\n\n[Response truncated]" : finalResponse;
|
|
5872
6080
|
await withRetry(() => completeTask(task.id, truncatedResponse, tokenUsage), {
|
|
5873
|
-
maxRetries:
|
|
6081
|
+
maxRetries: MAX_COMPLETE_TASK_RETRIES,
|
|
5874
6082
|
baseDelayMs: 300,
|
|
5875
6083
|
label: "completeTask"
|
|
5876
6084
|
});
|
|
@@ -5882,16 +6090,21 @@ var TaskProcessor = class {
|
|
|
5882
6090
|
convHistory.splice(0, convHistory.length - MAX_HISTORY_ENTRIES * 2);
|
|
5883
6091
|
}
|
|
5884
6092
|
this.historyCache.set(task.conversation_id, convHistory);
|
|
6093
|
+
if (this.memoryManager) {
|
|
6094
|
+
this.memoryManager.compressIfNeeded().catch(
|
|
6095
|
+
(err) => log.debug(`Memory compression skipped: ${err}`)
|
|
6096
|
+
);
|
|
6097
|
+
}
|
|
5885
6098
|
if (agentSessionId) {
|
|
5886
6099
|
this.evaluateSkillPostTask(agentSessionId, config.model).catch(
|
|
5887
6100
|
(err) => log.debug(`Post-task skill evaluation skipped: ${err}`)
|
|
5888
6101
|
);
|
|
5889
6102
|
}
|
|
5890
6103
|
} catch (err) {
|
|
5891
|
-
const
|
|
5892
|
-
log.error(`Task failed: ${
|
|
5893
|
-
await failTask(task.id,
|
|
5894
|
-
await emitEvent(task.id, "error", { message:
|
|
6104
|
+
const errMsg = errorMessage(err);
|
|
6105
|
+
log.error(`Task failed: ${errMsg}`);
|
|
6106
|
+
await failTask(task.id, errMsg);
|
|
6107
|
+
await emitEvent(task.id, "error", { message: errMsg });
|
|
5895
6108
|
await emitEvent(task.id, "status_change", { status: "failed" });
|
|
5896
6109
|
} finally {
|
|
5897
6110
|
setCorrelationId(null);
|
|
@@ -6392,7 +6605,7 @@ function registerJobCommands(program2) {
|
|
|
6392
6605
|
jobCmd.command("list").description("List your defined jobs").action(async () => {
|
|
6393
6606
|
try {
|
|
6394
6607
|
const userId = await getCurrentUserId();
|
|
6395
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
6608
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-CJ7HM4GZ.js");
|
|
6396
6609
|
const runner = new JobRunner2();
|
|
6397
6610
|
const jobs = await runner.listJobs();
|
|
6398
6611
|
if (jobs.length === 0) {
|
|
@@ -6416,7 +6629,7 @@ function registerJobCommands(program2) {
|
|
|
6416
6629
|
jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
|
|
6417
6630
|
try {
|
|
6418
6631
|
const userId = await getCurrentUserId();
|
|
6419
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
6632
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-CJ7HM4GZ.js");
|
|
6420
6633
|
const runner = new JobRunner2();
|
|
6421
6634
|
const runs = await runner.getRunHistory(name, parseInt(opts.limit || "5"));
|
|
6422
6635
|
if (runs.length === 0) {
|
|
@@ -6455,7 +6668,7 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`));
|
|
|
6455
6668
|
process.exit(1);
|
|
6456
6669
|
}
|
|
6457
6670
|
const userId = await getCurrentUserId();
|
|
6458
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
6671
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-CJ7HM4GZ.js");
|
|
6459
6672
|
const runner = new JobRunner2();
|
|
6460
6673
|
const job = await runner.loadJob(name);
|
|
6461
6674
|
if (!job) {
|