assistme 0.1.5 → 0.1.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/index.js +913 -906
- package/package.json +3 -2
- package/src/agent/event-hooks.test.ts +172 -0
- package/src/agent/event-hooks.ts +81 -0
- package/src/agent/mcp-servers.test.ts +128 -0
- package/src/agent/mcp-servers.ts +367 -0
- package/src/agent/processor.test.ts +175 -54
- package/src/agent/processor.ts +168 -324
- package/src/agent/session.test.ts +23 -1
- package/src/agent/session.ts +37 -19
- package/src/index.ts +1 -13
- package/src/utils/rate-limiter.ts +11 -5
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
claimTask,
|
|
3
4
|
clearConfig,
|
|
4
5
|
completeTask,
|
|
5
6
|
createSession,
|
|
@@ -248,26 +249,11 @@ var SessionManager = class {
|
|
|
248
249
|
if (!this.session || !this.userId || !this.conversationId) return;
|
|
249
250
|
log.info(`Running scheduled task: "${scheduledTask.name}"`);
|
|
250
251
|
try {
|
|
251
|
-
|
|
252
|
-
this.conversationId,
|
|
253
|
-
this.userId,
|
|
254
|
-
this.session.id,
|
|
252
|
+
await this.submitTask(
|
|
255
253
|
`[Scheduled: ${scheduledTask.name}] ${scheduledTask.prompt}`
|
|
256
254
|
);
|
|
257
|
-
if (this.onTask) {
|
|
258
|
-
this.processing = true;
|
|
259
|
-
await setSessionBusy(this.session.id, true);
|
|
260
|
-
try {
|
|
261
|
-
await this.onTask(task);
|
|
262
|
-
} catch (err) {
|
|
263
|
-
log.error(`Scheduled task error: ${err}`);
|
|
264
|
-
} finally {
|
|
265
|
-
await setSessionBusy(this.session.id, false);
|
|
266
|
-
this.processing = false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
255
|
} catch (err) {
|
|
270
|
-
log.error(`
|
|
256
|
+
log.error(`Scheduled task error: ${err}`);
|
|
271
257
|
}
|
|
272
258
|
}
|
|
273
259
|
async pollForTasks() {
|
|
@@ -330,6 +316,34 @@ var SessionManager = class {
|
|
|
330
316
|
log.debug(`Stale session cleanup error: ${err}`);
|
|
331
317
|
}
|
|
332
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Submit a task from the interactive prompt or scheduled job.
|
|
321
|
+
* Sets processing=true BEFORE creating the task so the poll loop
|
|
322
|
+
* never races to pick it up.
|
|
323
|
+
*/
|
|
324
|
+
async submitTask(prompt) {
|
|
325
|
+
if (!this.session || !this.userId || !this.conversationId || !this.onTask) {
|
|
326
|
+
throw new Error("Session not started");
|
|
327
|
+
}
|
|
328
|
+
this.processing = true;
|
|
329
|
+
await setSessionBusy(this.session.id, true);
|
|
330
|
+
try {
|
|
331
|
+
const task = await createTask(
|
|
332
|
+
this.conversationId,
|
|
333
|
+
this.userId,
|
|
334
|
+
this.session.id,
|
|
335
|
+
prompt
|
|
336
|
+
);
|
|
337
|
+
await claimTask(task.id);
|
|
338
|
+
await this.onTask(task);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
log.error(`Task error: ${err}`);
|
|
341
|
+
throw err;
|
|
342
|
+
} finally {
|
|
343
|
+
await setSessionBusy(this.session.id, false);
|
|
344
|
+
this.processing = false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
333
347
|
/**
|
|
334
348
|
* Stop the session with a safety timeout to prevent hanging on shutdown.
|
|
335
349
|
*/
|
|
@@ -811,554 +825,82 @@ function getBrowser(port = 9222) {
|
|
|
811
825
|
return browserInstance;
|
|
812
826
|
}
|
|
813
827
|
|
|
814
|
-
// src/
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const config = getConfig();
|
|
820
|
-
const resolved = resolve(config.workspacePath, filePath);
|
|
821
|
-
if (!resolved.startsWith(config.workspacePath)) {
|
|
822
|
-
throw new Error(
|
|
823
|
-
`Access denied: path "${filePath}" is outside workspace "${config.workspacePath}"`
|
|
824
|
-
);
|
|
828
|
+
// src/agent/memory.ts
|
|
829
|
+
var MemoryManager = class {
|
|
830
|
+
userId;
|
|
831
|
+
constructor(userId) {
|
|
832
|
+
this.userId = userId;
|
|
825
833
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const matches = await glob(pattern, {
|
|
848
|
-
cwd,
|
|
849
|
-
nodir: false,
|
|
850
|
-
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
851
|
-
});
|
|
852
|
-
if (matches.length === 0) return "No files found matching the pattern.";
|
|
853
|
-
return matches.slice(0, 50).map((m) => relative(config.workspacePath, join(cwd, m))).join("\n");
|
|
854
|
-
}
|
|
855
|
-
async function listDirectory(path) {
|
|
856
|
-
const config = getConfig();
|
|
857
|
-
const resolved = path ? assertWithinWorkspace(path) : config.workspacePath;
|
|
858
|
-
const entries = await readdir(resolved, { withFileTypes: true });
|
|
859
|
-
const results = [];
|
|
860
|
-
for (const entry of entries) {
|
|
861
|
-
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
862
|
-
const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
863
|
-
const info = entry.isFile() ? await stat(join(resolved, entry.name)).then(
|
|
864
|
-
(s) => ` (${formatSize(s.size)})`
|
|
865
|
-
) : "";
|
|
866
|
-
results.push(`${icon} ${entry.name}${info}`);
|
|
834
|
+
/**
|
|
835
|
+
* Store a new memory. Called by the agent after completing tasks
|
|
836
|
+
* to remember important facts about the user.
|
|
837
|
+
*/
|
|
838
|
+
async remember(content, category = "general", options) {
|
|
839
|
+
const sb = getSupabase();
|
|
840
|
+
const expiresAt = options?.expiresInDays ? new Date(
|
|
841
|
+
Date.now() + options.expiresInDays * 864e5
|
|
842
|
+
).toISOString() : null;
|
|
843
|
+
const { data, error } = await sb.from("agent_memories").insert({
|
|
844
|
+
user_id: this.userId,
|
|
845
|
+
category,
|
|
846
|
+
content,
|
|
847
|
+
importance: options?.importance ?? 5,
|
|
848
|
+
tags: options?.tags ?? [],
|
|
849
|
+
source_message_id: options?.sourceMessageId ?? null,
|
|
850
|
+
expires_at: expiresAt
|
|
851
|
+
}).select().single();
|
|
852
|
+
if (error) throw new Error(`Failed to store memory: ${error.message}`);
|
|
853
|
+
log.debug(`Memory stored: [${category}] ${content.slice(0, 80)}...`);
|
|
854
|
+
return data;
|
|
867
855
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const relPath = relative(config.workspacePath, join(cwd, file));
|
|
892
|
-
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
893
|
-
regex.lastIndex = 0;
|
|
894
|
-
if (results.length >= 30) break;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
if (results.length >= 30) break;
|
|
898
|
-
} catch {
|
|
856
|
+
/**
|
|
857
|
+
* Search memories by query text. Uses ILIKE + tag containment.
|
|
858
|
+
*/
|
|
859
|
+
async search(query2, limit = 10) {
|
|
860
|
+
const sb = getSupabase();
|
|
861
|
+
const sanitized = query2.replace(/[%_]/g, "\\$&");
|
|
862
|
+
const { data, error } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).or(
|
|
863
|
+
`content.ilike.%${sanitized}%,tags.cs.{${sanitized}}`
|
|
864
|
+
).order("importance", { ascending: false }).limit(limit);
|
|
865
|
+
if (error) {
|
|
866
|
+
log.warn(`Memory search failed: ${error.message}`);
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
if (data && data.length > 0) {
|
|
870
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
871
|
+
await Promise.all(
|
|
872
|
+
data.map(
|
|
873
|
+
(m) => sb.from("agent_memories").update({
|
|
874
|
+
access_count: m.access_count + 1,
|
|
875
|
+
last_accessed_at: now
|
|
876
|
+
}).eq("id", m.id)
|
|
877
|
+
)
|
|
878
|
+
);
|
|
899
879
|
}
|
|
880
|
+
return data || [];
|
|
900
881
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
/\bshutdown\b/i,
|
|
924
|
-
// shutdown
|
|
925
|
-
/\breboot\b/i,
|
|
926
|
-
// reboot
|
|
927
|
-
/\bsystemctl\s+(start|stop|disable|mask)\b/i
|
|
928
|
-
// dangerous systemctl ops
|
|
929
|
-
];
|
|
930
|
-
function isBlocked(command) {
|
|
931
|
-
return BLOCKED_PATTERNS.some((pattern) => pattern.test(command));
|
|
932
|
-
}
|
|
933
|
-
async function executeShell(command, cwd) {
|
|
934
|
-
if (isBlocked(command)) {
|
|
935
|
-
throw new Error(`Command blocked for safety: "${command}"`);
|
|
936
|
-
}
|
|
937
|
-
const config = getConfig();
|
|
938
|
-
const workDir = cwd || config.workspacePath;
|
|
939
|
-
return new Promise((resolve2) => {
|
|
940
|
-
exec(
|
|
941
|
-
command,
|
|
942
|
-
{
|
|
943
|
-
cwd: workDir,
|
|
944
|
-
timeout: TIMEOUT_MS,
|
|
945
|
-
maxBuffer: 1024 * 1024,
|
|
946
|
-
// 1MB buffer
|
|
947
|
-
env: { ...process.env, TERM: "dumb" }
|
|
948
|
-
},
|
|
949
|
-
(error, stdout, stderr) => {
|
|
950
|
-
let output = "";
|
|
951
|
-
if (stdout) {
|
|
952
|
-
output += stdout;
|
|
953
|
-
}
|
|
954
|
-
if (stderr) {
|
|
955
|
-
output += stderr ? `
|
|
956
|
-
[stderr]
|
|
957
|
-
${stderr}` : "";
|
|
958
|
-
}
|
|
959
|
-
if (error && !stdout && !stderr) {
|
|
960
|
-
output = `Error: ${error.message}`;
|
|
961
|
-
}
|
|
962
|
-
if (output.length > MAX_OUTPUT) {
|
|
963
|
-
output = output.slice(0, MAX_OUTPUT) + `
|
|
964
|
-
|
|
965
|
-
[Output truncated at ${MAX_OUTPUT} bytes]`;
|
|
966
|
-
}
|
|
967
|
-
resolve2(output || "(no output)");
|
|
968
|
-
}
|
|
969
|
-
);
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// src/tools/index.ts
|
|
974
|
-
function getToolDefinitions() {
|
|
975
|
-
return [
|
|
976
|
-
// ── File System Tools ───────────────────────────────────────
|
|
977
|
-
{
|
|
978
|
-
name: "read_file",
|
|
979
|
-
description: "Read the contents of a file. Returns line-numbered content. Use offset/limit for large files.",
|
|
980
|
-
input_schema: {
|
|
981
|
-
type: "object",
|
|
982
|
-
properties: {
|
|
983
|
-
path: { type: "string", description: "File path relative to workspace" },
|
|
984
|
-
offset: { type: "number", description: "Start line (0-indexed)" },
|
|
985
|
-
limit: { type: "number", description: "Max lines to read" }
|
|
986
|
-
},
|
|
987
|
-
required: ["path"]
|
|
988
|
-
}
|
|
989
|
-
},
|
|
990
|
-
{
|
|
991
|
-
name: "write_file",
|
|
992
|
-
description: "Write content to a file. Creates parent directories if needed.",
|
|
993
|
-
input_schema: {
|
|
994
|
-
type: "object",
|
|
995
|
-
properties: {
|
|
996
|
-
path: { type: "string", description: "File path relative to workspace" },
|
|
997
|
-
content: { type: "string", description: "Content to write" }
|
|
998
|
-
},
|
|
999
|
-
required: ["path", "content"]
|
|
1000
|
-
}
|
|
1001
|
-
},
|
|
1002
|
-
{
|
|
1003
|
-
name: "search_files",
|
|
1004
|
-
description: "Search for files matching a glob pattern (e.g. '**/*.ts').",
|
|
1005
|
-
input_schema: {
|
|
1006
|
-
type: "object",
|
|
1007
|
-
properties: {
|
|
1008
|
-
pattern: { type: "string", description: "Glob pattern" },
|
|
1009
|
-
directory: { type: "string", description: "Directory (relative to workspace)" }
|
|
1010
|
-
},
|
|
1011
|
-
required: ["pattern"]
|
|
1012
|
-
}
|
|
1013
|
-
},
|
|
1014
|
-
{
|
|
1015
|
-
name: "search_content",
|
|
1016
|
-
description: "Search file contents for a regex pattern. Returns matching lines.",
|
|
1017
|
-
input_schema: {
|
|
1018
|
-
type: "object",
|
|
1019
|
-
properties: {
|
|
1020
|
-
pattern: { type: "string", description: "Regex pattern" },
|
|
1021
|
-
file_glob: { type: "string", description: "File glob filter (default: **/*)" },
|
|
1022
|
-
directory: { type: "string", description: "Directory (relative to workspace)" }
|
|
1023
|
-
},
|
|
1024
|
-
required: ["pattern"]
|
|
1025
|
-
}
|
|
1026
|
-
},
|
|
1027
|
-
{
|
|
1028
|
-
name: "list_directory",
|
|
1029
|
-
description: "List files and directories in a path.",
|
|
1030
|
-
input_schema: {
|
|
1031
|
-
type: "object",
|
|
1032
|
-
properties: {
|
|
1033
|
-
path: { type: "string", description: "Directory path (default: workspace root)" }
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
},
|
|
1037
|
-
{
|
|
1038
|
-
name: "execute_command",
|
|
1039
|
-
description: "Execute a shell command in the workspace directory.",
|
|
1040
|
-
input_schema: {
|
|
1041
|
-
type: "object",
|
|
1042
|
-
properties: {
|
|
1043
|
-
command: { type: "string", description: "Shell command" },
|
|
1044
|
-
cwd: { type: "string", description: "Working directory (relative)" }
|
|
1045
|
-
},
|
|
1046
|
-
required: ["command"]
|
|
1047
|
-
}
|
|
1048
|
-
},
|
|
1049
|
-
// ── Browser Tools (CDP - controls user's real Chrome) ───────
|
|
1050
|
-
{
|
|
1051
|
-
name: "browser_connect",
|
|
1052
|
-
description: "Connect to the user's real Chrome browser via CDP. The user must have Chrome running with --remote-debugging-port=9222. This shares the user's actual browser session including all logins and cookies.",
|
|
1053
|
-
input_schema: {
|
|
1054
|
-
type: "object",
|
|
1055
|
-
properties: {
|
|
1056
|
-
tab_index: {
|
|
1057
|
-
type: "number",
|
|
1058
|
-
description: "Tab index to connect to (default: 0, the first tab). Use browser_list_tabs to see available tabs."
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
},
|
|
1063
|
-
{
|
|
1064
|
-
name: "browser_navigate",
|
|
1065
|
-
description: "Navigate the user's browser to a URL. Opens the page in the currently connected tab, using the user's real browser with all their cookies and logins.",
|
|
1066
|
-
input_schema: {
|
|
1067
|
-
type: "object",
|
|
1068
|
-
properties: {
|
|
1069
|
-
url: { type: "string", description: "URL to navigate to" }
|
|
1070
|
-
},
|
|
1071
|
-
required: ["url"]
|
|
1072
|
-
}
|
|
1073
|
-
},
|
|
1074
|
-
{
|
|
1075
|
-
name: "browser_read_page",
|
|
1076
|
-
description: "Read the text content of the currently open page in the user's browser. Returns the page title, URL, and main text content. Use this to understand what's on the page.",
|
|
1077
|
-
input_schema: { type: "object", properties: {} }
|
|
1078
|
-
},
|
|
1079
|
-
{
|
|
1080
|
-
name: "browser_screenshot",
|
|
1081
|
-
description: "Take a screenshot of the current browser page. Returns a base64-encoded PNG image. Use this when you need to visually understand the page layout, verify an action worked, or see dynamic content.",
|
|
1082
|
-
input_schema: { type: "object", properties: {} }
|
|
1083
|
-
},
|
|
1084
|
-
{
|
|
1085
|
-
name: "browser_click",
|
|
1086
|
-
description: "Click on an element in the user's browser using a CSS selector. Use browser_get_elements first to find clickable elements.",
|
|
1087
|
-
input_schema: {
|
|
1088
|
-
type: "object",
|
|
1089
|
-
properties: {
|
|
1090
|
-
selector: {
|
|
1091
|
-
type: "string",
|
|
1092
|
-
description: "CSS selector of the element to click (e.g. '#submit-btn', 'a.nav-link', 'button:nth-of-type(2)')"
|
|
1093
|
-
}
|
|
1094
|
-
},
|
|
1095
|
-
required: ["selector"]
|
|
1096
|
-
}
|
|
1097
|
-
},
|
|
1098
|
-
{
|
|
1099
|
-
name: "browser_type",
|
|
1100
|
-
description: "Type text into an input field in the user's browser. Focuses the element and sets its value.",
|
|
1101
|
-
input_schema: {
|
|
1102
|
-
type: "object",
|
|
1103
|
-
properties: {
|
|
1104
|
-
selector: {
|
|
1105
|
-
type: "string",
|
|
1106
|
-
description: "CSS selector of the input element"
|
|
1107
|
-
},
|
|
1108
|
-
text: {
|
|
1109
|
-
type: "string",
|
|
1110
|
-
description: "Text to type into the element"
|
|
1111
|
-
}
|
|
1112
|
-
},
|
|
1113
|
-
required: ["selector", "text"]
|
|
1114
|
-
}
|
|
1115
|
-
},
|
|
1116
|
-
{
|
|
1117
|
-
name: "browser_press_key",
|
|
1118
|
-
description: "Press a keyboard key in the browser. Supports: Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, or any single character.",
|
|
1119
|
-
input_schema: {
|
|
1120
|
-
type: "object",
|
|
1121
|
-
properties: {
|
|
1122
|
-
key: { type: "string", description: "Key to press" }
|
|
1123
|
-
},
|
|
1124
|
-
required: ["key"]
|
|
1125
|
-
}
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
name: "browser_scroll",
|
|
1129
|
-
description: "Scroll the page up or down.",
|
|
1130
|
-
input_schema: {
|
|
1131
|
-
type: "object",
|
|
1132
|
-
properties: {
|
|
1133
|
-
direction: {
|
|
1134
|
-
type: "string",
|
|
1135
|
-
description: "'down' or 'up'"
|
|
1136
|
-
}
|
|
1137
|
-
},
|
|
1138
|
-
required: ["direction"]
|
|
1139
|
-
}
|
|
1140
|
-
},
|
|
1141
|
-
{
|
|
1142
|
-
name: "browser_get_elements",
|
|
1143
|
-
description: "Find all interactive elements (links, buttons, inputs, etc.) on the current page. Returns their tag, text, selector, and attributes. Use this to understand what you can interact with.",
|
|
1144
|
-
input_schema: { type: "object", properties: {} }
|
|
1145
|
-
},
|
|
1146
|
-
{
|
|
1147
|
-
name: "browser_evaluate",
|
|
1148
|
-
description: "Execute JavaScript in the browser page context. Use for advanced interactions or data extraction that other tools can't handle.",
|
|
1149
|
-
input_schema: {
|
|
1150
|
-
type: "object",
|
|
1151
|
-
properties: {
|
|
1152
|
-
expression: {
|
|
1153
|
-
type: "string",
|
|
1154
|
-
description: "JavaScript expression to evaluate"
|
|
1155
|
-
}
|
|
1156
|
-
},
|
|
1157
|
-
required: ["expression"]
|
|
1158
|
-
}
|
|
1159
|
-
},
|
|
1160
|
-
{
|
|
1161
|
-
name: "browser_list_tabs",
|
|
1162
|
-
description: "List all open tabs in the user's browser. Shows title, URL, and which tab is currently connected.",
|
|
1163
|
-
input_schema: { type: "object", properties: {} }
|
|
1164
|
-
},
|
|
1165
|
-
{
|
|
1166
|
-
name: "browser_switch_tab",
|
|
1167
|
-
description: "Switch to a different browser tab by index.",
|
|
1168
|
-
input_schema: {
|
|
1169
|
-
type: "object",
|
|
1170
|
-
properties: {
|
|
1171
|
-
index: {
|
|
1172
|
-
type: "number",
|
|
1173
|
-
description: "Tab index (from browser_list_tabs)"
|
|
1174
|
-
}
|
|
1175
|
-
},
|
|
1176
|
-
required: ["index"]
|
|
1177
|
-
}
|
|
1178
|
-
},
|
|
1179
|
-
{
|
|
1180
|
-
name: "browser_new_tab",
|
|
1181
|
-
description: "Open a new tab in the user's browser, optionally navigating to a URL.",
|
|
1182
|
-
input_schema: {
|
|
1183
|
-
type: "object",
|
|
1184
|
-
properties: {
|
|
1185
|
-
url: { type: "string", description: "URL to open (default: blank)" }
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
},
|
|
1189
|
-
{
|
|
1190
|
-
name: "browser_request_user_action",
|
|
1191
|
-
description: "Request the user to perform an action in their browser that the AI cannot do. Use this when: (1) a page requires login/authentication, (2) a CAPTCHA needs solving, (3) two-factor auth is needed, (4) any action requiring the user's personal credentials. The user will see a notification on the web UI and in the terminal.",
|
|
1192
|
-
input_schema: {
|
|
1193
|
-
type: "object",
|
|
1194
|
-
properties: {
|
|
1195
|
-
message: {
|
|
1196
|
-
type: "string",
|
|
1197
|
-
description: "Clear description of what the user needs to do. E.g.: 'Please log in to amazon.com in your browser. The login page is currently open in the active tab.'"
|
|
1198
|
-
},
|
|
1199
|
-
wait_seconds: {
|
|
1200
|
-
type: "number",
|
|
1201
|
-
description: "How long to wait for the user to complete the action (default: 60)"
|
|
1202
|
-
}
|
|
1203
|
-
},
|
|
1204
|
-
required: ["message"]
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
];
|
|
1208
|
-
}
|
|
1209
|
-
async function executeTool(name, input) {
|
|
1210
|
-
const browser = getBrowser();
|
|
1211
|
-
switch (name) {
|
|
1212
|
-
// ── Filesystem ──────────────────────────────────────────
|
|
1213
|
-
case "read_file":
|
|
1214
|
-
return readFileContent(
|
|
1215
|
-
input.path,
|
|
1216
|
-
input.offset,
|
|
1217
|
-
input.limit
|
|
1218
|
-
);
|
|
1219
|
-
case "write_file":
|
|
1220
|
-
return writeFileContent(input.path, input.content);
|
|
1221
|
-
case "search_files":
|
|
1222
|
-
return searchFiles(input.pattern, input.directory);
|
|
1223
|
-
case "search_content":
|
|
1224
|
-
return searchContent(
|
|
1225
|
-
input.pattern,
|
|
1226
|
-
input.file_glob,
|
|
1227
|
-
input.directory
|
|
1228
|
-
);
|
|
1229
|
-
case "list_directory":
|
|
1230
|
-
return listDirectory(input.path);
|
|
1231
|
-
case "execute_command":
|
|
1232
|
-
return executeShell(input.command, input.cwd);
|
|
1233
|
-
// ── Browser (CDP) ───────────────────────────────────────
|
|
1234
|
-
case "browser_connect":
|
|
1235
|
-
return browser.connect(input.tab_index);
|
|
1236
|
-
case "browser_navigate":
|
|
1237
|
-
if (!browser.isConnected()) await browser.connect();
|
|
1238
|
-
return browser.navigate(input.url);
|
|
1239
|
-
case "browser_read_page":
|
|
1240
|
-
return browser.readPage();
|
|
1241
|
-
case "browser_screenshot":
|
|
1242
|
-
return browser.screenshot();
|
|
1243
|
-
case "browser_click":
|
|
1244
|
-
return browser.click(input.selector);
|
|
1245
|
-
case "browser_type":
|
|
1246
|
-
return browser.typeText(input.selector, input.text);
|
|
1247
|
-
case "browser_press_key":
|
|
1248
|
-
return browser.pressKey(input.key);
|
|
1249
|
-
case "browser_scroll":
|
|
1250
|
-
return input.direction === "up" ? browser.scrollUp() : browser.scrollDown();
|
|
1251
|
-
case "browser_get_elements":
|
|
1252
|
-
return browser.getInteractiveElements();
|
|
1253
|
-
case "browser_evaluate":
|
|
1254
|
-
return browser.evaluate(input.expression);
|
|
1255
|
-
case "browser_list_tabs":
|
|
1256
|
-
return browser.listTabs();
|
|
1257
|
-
case "browser_switch_tab":
|
|
1258
|
-
return browser.switchTab(input.index);
|
|
1259
|
-
case "browser_new_tab":
|
|
1260
|
-
return browser.openNewTab(input.url);
|
|
1261
|
-
case "browser_request_user_action": {
|
|
1262
|
-
const message = input.message;
|
|
1263
|
-
const waitSeconds = input.wait_seconds || 60;
|
|
1264
|
-
console.log("\n");
|
|
1265
|
-
console.log("\u2501".repeat(60));
|
|
1266
|
-
console.log(" \u{1F64B} USER ACTION REQUIRED");
|
|
1267
|
-
console.log("\u2501".repeat(60));
|
|
1268
|
-
console.log(` ${message}`);
|
|
1269
|
-
console.log(` (Waiting up to ${waitSeconds}s for you to complete this)`);
|
|
1270
|
-
console.log("\u2501".repeat(60));
|
|
1271
|
-
console.log("\n");
|
|
1272
|
-
await new Promise((r) => setTimeout(r, waitSeconds * 1e3));
|
|
1273
|
-
try {
|
|
1274
|
-
const pageInfo = await browser.readPage();
|
|
1275
|
-
return `User action wait completed. Current page state:
|
|
1276
|
-
${pageInfo.slice(0, 3e3)}`;
|
|
1277
|
-
} catch {
|
|
1278
|
-
return "User action wait completed. Could not read page state.";
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
default:
|
|
1282
|
-
return `Unknown tool: ${name}`;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// src/agent/memory.ts
|
|
1287
|
-
var MemoryManager = class {
|
|
1288
|
-
userId;
|
|
1289
|
-
constructor(userId) {
|
|
1290
|
-
this.userId = userId;
|
|
1291
|
-
}
|
|
1292
|
-
/**
|
|
1293
|
-
* Store a new memory. Called by the agent after completing tasks
|
|
1294
|
-
* to remember important facts about the user.
|
|
1295
|
-
*/
|
|
1296
|
-
async remember(content, category = "general", options) {
|
|
1297
|
-
const sb = getSupabase();
|
|
1298
|
-
const expiresAt = options?.expiresInDays ? new Date(
|
|
1299
|
-
Date.now() + options.expiresInDays * 864e5
|
|
1300
|
-
).toISOString() : null;
|
|
1301
|
-
const { data, error } = await sb.from("agent_memories").insert({
|
|
1302
|
-
user_id: this.userId,
|
|
1303
|
-
category,
|
|
1304
|
-
content,
|
|
1305
|
-
importance: options?.importance ?? 5,
|
|
1306
|
-
tags: options?.tags ?? [],
|
|
1307
|
-
source_message_id: options?.sourceMessageId ?? null,
|
|
1308
|
-
expires_at: expiresAt
|
|
1309
|
-
}).select().single();
|
|
1310
|
-
if (error) throw new Error(`Failed to store memory: ${error.message}`);
|
|
1311
|
-
log.debug(`Memory stored: [${category}] ${content.slice(0, 80)}...`);
|
|
1312
|
-
return data;
|
|
1313
|
-
}
|
|
1314
|
-
/**
|
|
1315
|
-
* Search memories by query text. Uses ILIKE + tag containment.
|
|
1316
|
-
*/
|
|
1317
|
-
async search(query2, limit = 10) {
|
|
1318
|
-
const sb = getSupabase();
|
|
1319
|
-
const sanitized = query2.replace(/[%_]/g, "\\$&");
|
|
1320
|
-
const { data, error } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).or(
|
|
1321
|
-
`content.ilike.%${sanitized}%,tags.cs.{${sanitized}}`
|
|
1322
|
-
).order("importance", { ascending: false }).limit(limit);
|
|
1323
|
-
if (error) {
|
|
1324
|
-
log.warn(`Memory search failed: ${error.message}`);
|
|
1325
|
-
return [];
|
|
1326
|
-
}
|
|
1327
|
-
if (data && data.length > 0) {
|
|
1328
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1329
|
-
await Promise.all(
|
|
1330
|
-
data.map(
|
|
1331
|
-
(m) => sb.from("agent_memories").update({
|
|
1332
|
-
access_count: m.access_count + 1,
|
|
1333
|
-
last_accessed_at: now
|
|
1334
|
-
}).eq("id", m.id)
|
|
1335
|
-
)
|
|
1336
|
-
);
|
|
1337
|
-
}
|
|
1338
|
-
return data || [];
|
|
1339
|
-
}
|
|
1340
|
-
/**
|
|
1341
|
-
* Get the most important/recent memories to include in context.
|
|
1342
|
-
* Called before each task to build the agent's "working memory".
|
|
1343
|
-
* Automatically filters out expired memories.
|
|
1344
|
-
*/
|
|
1345
|
-
async getContext(maxItems = 20) {
|
|
1346
|
-
const sb = getSupabase();
|
|
1347
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1348
|
-
const { data: instructions } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).eq("category", "instruction").or(`expires_at.is.null,expires_at.gt.${now}`).order("importance", { ascending: false }).limit(5);
|
|
1349
|
-
const { data: preferences } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).eq("category", "preference").or(`expires_at.is.null,expires_at.gt.${now}`).order("importance", { ascending: false }).limit(5);
|
|
1350
|
-
const { data: general } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).not("category", "in", '("instruction","preference")').or(`expires_at.is.null,expires_at.gt.${now}`).order("importance", { ascending: false }).order("updated_at", { ascending: false }).limit(maxItems - 10);
|
|
1351
|
-
const all = [
|
|
1352
|
-
...instructions || [],
|
|
1353
|
-
...preferences || [],
|
|
1354
|
-
...general || []
|
|
1355
|
-
];
|
|
1356
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1357
|
-
return all.filter((m) => {
|
|
1358
|
-
if (seen.has(m.id)) return false;
|
|
1359
|
-
seen.add(m.id);
|
|
1360
|
-
return true;
|
|
1361
|
-
});
|
|
882
|
+
/**
|
|
883
|
+
* Get the most important/recent memories to include in context.
|
|
884
|
+
* Called before each task to build the agent's "working memory".
|
|
885
|
+
* Automatically filters out expired memories.
|
|
886
|
+
*/
|
|
887
|
+
async getContext(maxItems = 20) {
|
|
888
|
+
const sb = getSupabase();
|
|
889
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
890
|
+
const { data: instructions } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).eq("category", "instruction").or(`expires_at.is.null,expires_at.gt.${now}`).order("importance", { ascending: false }).limit(5);
|
|
891
|
+
const { data: preferences } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).eq("category", "preference").or(`expires_at.is.null,expires_at.gt.${now}`).order("importance", { ascending: false }).limit(5);
|
|
892
|
+
const { data: general } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).not("category", "in", '("instruction","preference")').or(`expires_at.is.null,expires_at.gt.${now}`).order("importance", { ascending: false }).order("updated_at", { ascending: false }).limit(maxItems - 10);
|
|
893
|
+
const all = [
|
|
894
|
+
...instructions || [],
|
|
895
|
+
...preferences || [],
|
|
896
|
+
...general || []
|
|
897
|
+
];
|
|
898
|
+
const seen = /* @__PURE__ */ new Set();
|
|
899
|
+
return all.filter((m) => {
|
|
900
|
+
if (seen.has(m.id)) return false;
|
|
901
|
+
seen.add(m.id);
|
|
902
|
+
return true;
|
|
903
|
+
});
|
|
1362
904
|
}
|
|
1363
905
|
/**
|
|
1364
906
|
* Format memories into a string for the system prompt.
|
|
@@ -1431,7 +973,7 @@ import {
|
|
|
1431
973
|
unlinkSync,
|
|
1432
974
|
rmSync
|
|
1433
975
|
} from "fs";
|
|
1434
|
-
import { join
|
|
976
|
+
import { join, basename, dirname } from "path";
|
|
1435
977
|
import { homedir } from "os";
|
|
1436
978
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1437
979
|
"the",
|
|
@@ -1552,8 +1094,8 @@ function bigrams(tokens) {
|
|
|
1552
1094
|
}
|
|
1553
1095
|
return result;
|
|
1554
1096
|
}
|
|
1555
|
-
var SKILLS_DIR =
|
|
1556
|
-
var BUNDLED_SKILLS_DIR =
|
|
1097
|
+
var SKILLS_DIR = join(homedir(), ".config", "assistme", "skills");
|
|
1098
|
+
var BUNDLED_SKILLS_DIR = join(
|
|
1557
1099
|
new URL(".", import.meta.url).pathname,
|
|
1558
1100
|
"..",
|
|
1559
1101
|
"..",
|
|
@@ -1639,11 +1181,11 @@ var SkillManager = class {
|
|
|
1639
1181
|
try {
|
|
1640
1182
|
const entries = readdirSync(dir);
|
|
1641
1183
|
for (const entry of entries) {
|
|
1642
|
-
const fullPath =
|
|
1184
|
+
const fullPath = join(dir, entry);
|
|
1643
1185
|
const stat2 = statSync(fullPath);
|
|
1644
1186
|
if (stat2.isDirectory()) {
|
|
1645
|
-
const skillMd =
|
|
1646
|
-
const skillMdLower =
|
|
1187
|
+
const skillMd = join(fullPath, "SKILL.md");
|
|
1188
|
+
const skillMdLower = join(fullPath, "skill.md");
|
|
1647
1189
|
const mdPath = existsSync(skillMd) ? skillMd : existsSync(skillMdLower) ? skillMdLower : null;
|
|
1648
1190
|
if (mdPath) {
|
|
1649
1191
|
const skill = parseSkillFile(mdPath, source);
|
|
@@ -1752,9 +1294,9 @@ var SkillManager = class {
|
|
|
1752
1294
|
*/
|
|
1753
1295
|
create(name, description, content) {
|
|
1754
1296
|
ensureSkillsDir();
|
|
1755
|
-
const skillDir =
|
|
1297
|
+
const skillDir = join(SKILLS_DIR, name);
|
|
1756
1298
|
mkdirSync(skillDir, { recursive: true });
|
|
1757
|
-
const filePath =
|
|
1299
|
+
const filePath = join(skillDir, "SKILL.md");
|
|
1758
1300
|
const fileContent = `---
|
|
1759
1301
|
name: ${name}
|
|
1760
1302
|
description: ${description}
|
|
@@ -1793,7 +1335,7 @@ ${content}
|
|
|
1793
1335
|
gitUrl += ".git";
|
|
1794
1336
|
}
|
|
1795
1337
|
const name = basename(gitUrl, ".git");
|
|
1796
|
-
const targetDir =
|
|
1338
|
+
const targetDir = join(SKILLS_DIR, name);
|
|
1797
1339
|
if (existsSync(targetDir)) {
|
|
1798
1340
|
throw new Error(`Skill "${name}" already exists. Remove it first.`);
|
|
1799
1341
|
}
|
|
@@ -1809,8 +1351,8 @@ ${content}
|
|
|
1809
1351
|
`Failed to clone: ${err instanceof Error ? err.message : err}`
|
|
1810
1352
|
);
|
|
1811
1353
|
}
|
|
1812
|
-
const skillMd =
|
|
1813
|
-
const skillMdLower =
|
|
1354
|
+
const skillMd = join(targetDir, "SKILL.md");
|
|
1355
|
+
const skillMdLower = join(targetDir, "skill.md");
|
|
1814
1356
|
if (!existsSync(skillMd) && !existsSync(skillMdLower)) {
|
|
1815
1357
|
rmSync(targetDir, { recursive: true, force: true });
|
|
1816
1358
|
throw new Error(
|
|
@@ -1836,9 +1378,9 @@ ${content}
|
|
|
1836
1378
|
throw new Error(`HTTP ${dlResp.status}`);
|
|
1837
1379
|
}
|
|
1838
1380
|
const buffer = Buffer.from(await dlResp.arrayBuffer());
|
|
1839
|
-
const skillDir =
|
|
1381
|
+
const skillDir = join(SKILLS_DIR, name);
|
|
1840
1382
|
mkdirSync(skillDir, { recursive: true });
|
|
1841
|
-
const zipPath =
|
|
1383
|
+
const zipPath = join(skillDir, "_download.zip");
|
|
1842
1384
|
writeFileSync(zipPath, buffer);
|
|
1843
1385
|
const { exec: execCb } = await import("child_process");
|
|
1844
1386
|
const { promisify: promisifyUtil } = await import("util");
|
|
@@ -1857,12 +1399,12 @@ ${content}
|
|
|
1857
1399
|
throw new Error(`Could not fetch SKILL.md`);
|
|
1858
1400
|
}
|
|
1859
1401
|
writeFileSync(
|
|
1860
|
-
|
|
1402
|
+
join(skillDir, "SKILL.md"),
|
|
1861
1403
|
await fileResp.text(),
|
|
1862
1404
|
"utf-8"
|
|
1863
1405
|
);
|
|
1864
1406
|
}
|
|
1865
|
-
const skillMd =
|
|
1407
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
1866
1408
|
if (!existsSync(skillMd)) {
|
|
1867
1409
|
rmSync(skillDir, { recursive: true, force: true });
|
|
1868
1410
|
throw new Error("No SKILL.md in downloaded package");
|
|
@@ -2256,27 +1798,305 @@ ${taskResult.slice(0, 1500)}
|
|
|
2256
1798
|
}
|
|
2257
1799
|
}
|
|
2258
1800
|
|
|
2259
|
-
// src/utils/
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
1801
|
+
// src/utils/retry.ts
|
|
1802
|
+
async function withRetry(fn, opts = {}) {
|
|
1803
|
+
const {
|
|
1804
|
+
maxRetries = 3,
|
|
1805
|
+
baseDelayMs = 500,
|
|
1806
|
+
maxDelayMs = 1e4,
|
|
1807
|
+
backoffFactor = 2,
|
|
1808
|
+
retryIf,
|
|
1809
|
+
label
|
|
1810
|
+
} = opts;
|
|
1811
|
+
let lastError;
|
|
1812
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1813
|
+
try {
|
|
1814
|
+
return await fn();
|
|
1815
|
+
} catch (err) {
|
|
1816
|
+
lastError = err;
|
|
1817
|
+
if (retryIf && !retryIf(err)) {
|
|
1818
|
+
throw err;
|
|
1819
|
+
}
|
|
1820
|
+
if (attempt < maxRetries) {
|
|
1821
|
+
const delay = Math.min(
|
|
1822
|
+
baseDelayMs * Math.pow(backoffFactor, attempt),
|
|
1823
|
+
maxDelayMs
|
|
1824
|
+
);
|
|
1825
|
+
if (label) {
|
|
1826
|
+
log.debug(`${label}: attempt ${attempt + 1} failed, retrying in ${delay}ms`);
|
|
1827
|
+
}
|
|
1828
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
2273
1831
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
1832
|
+
throw lastError;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// src/agent/mcp-servers.ts
|
|
1836
|
+
import {
|
|
1837
|
+
createSdkMcpServer,
|
|
1838
|
+
tool
|
|
1839
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
1840
|
+
import { z } from "zod/v4";
|
|
1841
|
+
|
|
1842
|
+
// src/tools/filesystem.ts
|
|
1843
|
+
import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
|
|
1844
|
+
import { resolve, relative, join as join2 } from "path";
|
|
1845
|
+
import { glob } from "glob";
|
|
1846
|
+
function assertWithinWorkspace(filePath) {
|
|
1847
|
+
const config = getConfig();
|
|
1848
|
+
const resolved = resolve(config.workspacePath, filePath);
|
|
1849
|
+
if (!resolved.startsWith(config.workspacePath)) {
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`Access denied: path "${filePath}" is outside workspace "${config.workspacePath}"`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
return resolved;
|
|
1855
|
+
}
|
|
1856
|
+
async function readFileContent(path, offset, limit) {
|
|
1857
|
+
const resolved = assertWithinWorkspace(path);
|
|
1858
|
+
const content = await readFile(resolved, "utf-8");
|
|
1859
|
+
const lines = content.split("\n");
|
|
1860
|
+
const start = offset || 0;
|
|
1861
|
+
const end = limit ? start + limit : lines.length;
|
|
1862
|
+
const selected = lines.slice(start, end);
|
|
1863
|
+
return selected.map((line, i) => `${String(start + i + 1).padStart(5)} | ${line}`).join("\n");
|
|
1864
|
+
}
|
|
1865
|
+
async function writeFileContent(path, content) {
|
|
1866
|
+
const resolved = assertWithinWorkspace(path);
|
|
1867
|
+
const dir = resolve(resolved, "..");
|
|
1868
|
+
await mkdir(dir, { recursive: true });
|
|
1869
|
+
await writeFile(resolved, content, "utf-8");
|
|
1870
|
+
return `File written: ${path} (${content.length} bytes)`;
|
|
1871
|
+
}
|
|
1872
|
+
async function searchFiles(pattern, directory) {
|
|
1873
|
+
const config = getConfig();
|
|
1874
|
+
const cwd = directory ? assertWithinWorkspace(directory) : config.workspacePath;
|
|
1875
|
+
const matches = await glob(pattern, {
|
|
1876
|
+
cwd,
|
|
1877
|
+
nodir: false,
|
|
1878
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
1879
|
+
});
|
|
1880
|
+
if (matches.length === 0) return "No files found matching the pattern.";
|
|
1881
|
+
return matches.slice(0, 50).map((m) => relative(config.workspacePath, join2(cwd, m))).join("\n");
|
|
1882
|
+
}
|
|
1883
|
+
async function listDirectory(path) {
|
|
1884
|
+
const config = getConfig();
|
|
1885
|
+
const resolved = path ? assertWithinWorkspace(path) : config.workspacePath;
|
|
1886
|
+
const entries = await readdir(resolved, { withFileTypes: true });
|
|
1887
|
+
const results = [];
|
|
1888
|
+
for (const entry of entries) {
|
|
1889
|
+
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
1890
|
+
const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
1891
|
+
const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then(
|
|
1892
|
+
(s) => ` (${formatSize(s.size)})`
|
|
1893
|
+
) : "";
|
|
1894
|
+
results.push(`${icon} ${entry.name}${info}`);
|
|
1895
|
+
}
|
|
1896
|
+
return results.join("\n") || "Empty directory.";
|
|
1897
|
+
}
|
|
1898
|
+
function formatSize(bytes) {
|
|
1899
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1900
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1901
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1902
|
+
}
|
|
1903
|
+
async function searchContent(pattern, fileGlob, directory) {
|
|
1904
|
+
const config = getConfig();
|
|
1905
|
+
const cwd = directory ? assertWithinWorkspace(directory) : config.workspacePath;
|
|
1906
|
+
const files = await glob(fileGlob || "**/*", {
|
|
1907
|
+
cwd,
|
|
1908
|
+
nodir: true,
|
|
1909
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
1910
|
+
});
|
|
1911
|
+
const regex = new RegExp(pattern, "gi");
|
|
1912
|
+
const results = [];
|
|
1913
|
+
for (const file of files.slice(0, 200)) {
|
|
1914
|
+
try {
|
|
1915
|
+
const content = await readFile(join2(cwd, file), "utf-8");
|
|
1916
|
+
const lines = content.split("\n");
|
|
1917
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1918
|
+
if (regex.test(lines[i])) {
|
|
1919
|
+
const relPath = relative(config.workspacePath, join2(cwd, file));
|
|
1920
|
+
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
1921
|
+
regex.lastIndex = 0;
|
|
1922
|
+
if (results.length >= 30) break;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (results.length >= 30) break;
|
|
1926
|
+
} catch {
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return results.length > 0 ? results.join("\n") : "No matches found.";
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// src/tools/shell.ts
|
|
1933
|
+
import { exec } from "child_process";
|
|
1934
|
+
var TIMEOUT_MS = 3e4;
|
|
1935
|
+
var MAX_OUTPUT = 5e4;
|
|
1936
|
+
var BLOCKED_PATTERNS = [
|
|
1937
|
+
/rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i,
|
|
1938
|
+
// rm -rf /, rm -fr /, etc.
|
|
1939
|
+
/rm\s+(-\w*\s+)*-\w*r\w*\s+~($|\s|\/)/i,
|
|
1940
|
+
// rm -rf ~, rm -fr ~/, etc.
|
|
1941
|
+
/\bmkfs\b/i,
|
|
1942
|
+
// mkfs (any form)
|
|
1943
|
+
/\bdd\s+.*\bif=/i,
|
|
1944
|
+
// dd if=
|
|
1945
|
+
/:\s*\(\s*\)\s*\{/,
|
|
1946
|
+
// fork bomb :(){}
|
|
1947
|
+
/\bchmod\s+(-\w+\s+)*-R\s+777\s+\//i,
|
|
1948
|
+
// chmod -R 777 /
|
|
1949
|
+
/>\s*\/dev\/sd[a-z]/i,
|
|
1950
|
+
// write to raw disk
|
|
1951
|
+
/\bshutdown\b/i,
|
|
1952
|
+
// shutdown
|
|
1953
|
+
/\breboot\b/i,
|
|
1954
|
+
// reboot
|
|
1955
|
+
/\bsystemctl\s+(start|stop|disable|mask)\b/i
|
|
1956
|
+
// dangerous systemctl ops
|
|
1957
|
+
];
|
|
1958
|
+
function isBlocked(command) {
|
|
1959
|
+
return BLOCKED_PATTERNS.some((pattern) => pattern.test(command));
|
|
1960
|
+
}
|
|
1961
|
+
async function executeShell(command, cwd) {
|
|
1962
|
+
if (isBlocked(command)) {
|
|
1963
|
+
throw new Error(`Command blocked for safety: "${command}"`);
|
|
1964
|
+
}
|
|
1965
|
+
const config = getConfig();
|
|
1966
|
+
const workDir = cwd || config.workspacePath;
|
|
1967
|
+
return new Promise((resolve2) => {
|
|
1968
|
+
exec(
|
|
1969
|
+
command,
|
|
1970
|
+
{
|
|
1971
|
+
cwd: workDir,
|
|
1972
|
+
timeout: TIMEOUT_MS,
|
|
1973
|
+
maxBuffer: 1024 * 1024,
|
|
1974
|
+
// 1MB buffer
|
|
1975
|
+
env: { ...process.env, TERM: "dumb" }
|
|
1976
|
+
},
|
|
1977
|
+
(error, stdout, stderr) => {
|
|
1978
|
+
let output = "";
|
|
1979
|
+
if (stdout) {
|
|
1980
|
+
output += stdout;
|
|
1981
|
+
}
|
|
1982
|
+
if (stderr) {
|
|
1983
|
+
output += stderr ? `
|
|
1984
|
+
[stderr]
|
|
1985
|
+
${stderr}` : "";
|
|
1986
|
+
}
|
|
1987
|
+
if (error && !stdout && !stderr) {
|
|
1988
|
+
output = `Error: ${error.message}`;
|
|
1989
|
+
}
|
|
1990
|
+
if (output.length > MAX_OUTPUT) {
|
|
1991
|
+
output = output.slice(0, MAX_OUTPUT) + `
|
|
1992
|
+
|
|
1993
|
+
[Output truncated at ${MAX_OUTPUT} bytes]`;
|
|
1994
|
+
}
|
|
1995
|
+
resolve2(output || "(no output)");
|
|
1996
|
+
}
|
|
1997
|
+
);
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/tools/index.ts
|
|
2002
|
+
async function executeTool(name, input) {
|
|
2003
|
+
const browser = getBrowser();
|
|
2004
|
+
switch (name) {
|
|
2005
|
+
// ── Filesystem ──────────────────────────────────────────
|
|
2006
|
+
case "read_file":
|
|
2007
|
+
return readFileContent(
|
|
2008
|
+
input.path,
|
|
2009
|
+
input.offset,
|
|
2010
|
+
input.limit
|
|
2011
|
+
);
|
|
2012
|
+
case "write_file":
|
|
2013
|
+
return writeFileContent(input.path, input.content);
|
|
2014
|
+
case "search_files":
|
|
2015
|
+
return searchFiles(input.pattern, input.directory);
|
|
2016
|
+
case "search_content":
|
|
2017
|
+
return searchContent(
|
|
2018
|
+
input.pattern,
|
|
2019
|
+
input.file_glob,
|
|
2020
|
+
input.directory
|
|
2021
|
+
);
|
|
2022
|
+
case "list_directory":
|
|
2023
|
+
return listDirectory(input.path);
|
|
2024
|
+
case "execute_command":
|
|
2025
|
+
return executeShell(input.command, input.cwd);
|
|
2026
|
+
// ── Browser (CDP) ───────────────────────────────────────
|
|
2027
|
+
case "browser_connect":
|
|
2028
|
+
return browser.connect(input.tab_index);
|
|
2029
|
+
case "browser_navigate":
|
|
2030
|
+
if (!browser.isConnected()) await browser.connect();
|
|
2031
|
+
return browser.navigate(input.url);
|
|
2032
|
+
case "browser_read_page":
|
|
2033
|
+
return browser.readPage();
|
|
2034
|
+
case "browser_screenshot":
|
|
2035
|
+
return browser.screenshot();
|
|
2036
|
+
case "browser_click":
|
|
2037
|
+
return browser.click(input.selector);
|
|
2038
|
+
case "browser_type":
|
|
2039
|
+
return browser.typeText(input.selector, input.text);
|
|
2040
|
+
case "browser_press_key":
|
|
2041
|
+
return browser.pressKey(input.key);
|
|
2042
|
+
case "browser_scroll":
|
|
2043
|
+
return input.direction === "up" ? browser.scrollUp() : browser.scrollDown();
|
|
2044
|
+
case "browser_get_elements":
|
|
2045
|
+
return browser.getInteractiveElements();
|
|
2046
|
+
case "browser_evaluate":
|
|
2047
|
+
return browser.evaluate(input.expression);
|
|
2048
|
+
case "browser_list_tabs":
|
|
2049
|
+
return browser.listTabs();
|
|
2050
|
+
case "browser_switch_tab":
|
|
2051
|
+
return browser.switchTab(input.index);
|
|
2052
|
+
case "browser_new_tab":
|
|
2053
|
+
return browser.openNewTab(input.url);
|
|
2054
|
+
case "browser_request_user_action": {
|
|
2055
|
+
const message = input.message;
|
|
2056
|
+
const waitSeconds = input.wait_seconds || 60;
|
|
2057
|
+
console.log("\n");
|
|
2058
|
+
console.log("\u2501".repeat(60));
|
|
2059
|
+
console.log(" \u{1F64B} USER ACTION REQUIRED");
|
|
2060
|
+
console.log("\u2501".repeat(60));
|
|
2061
|
+
console.log(` ${message}`);
|
|
2062
|
+
console.log(` (Waiting up to ${waitSeconds}s for you to complete this)`);
|
|
2063
|
+
console.log("\u2501".repeat(60));
|
|
2064
|
+
console.log("\n");
|
|
2065
|
+
await new Promise((r) => setTimeout(r, waitSeconds * 1e3));
|
|
2066
|
+
try {
|
|
2067
|
+
const pageInfo = await browser.readPage();
|
|
2068
|
+
return `User action wait completed. Current page state:
|
|
2069
|
+
${pageInfo.slice(0, 3e3)}`;
|
|
2070
|
+
} catch {
|
|
2071
|
+
return "User action wait completed. Could not read page state.";
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
default:
|
|
2075
|
+
return `Unknown tool: ${name}`;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// src/utils/rate-limiter.ts
|
|
2080
|
+
var RateLimiter = class {
|
|
2081
|
+
tokens;
|
|
2082
|
+
maxTokens;
|
|
2083
|
+
refillRate;
|
|
2084
|
+
// tokens per second
|
|
2085
|
+
lastRefill;
|
|
2086
|
+
maxWaitMs;
|
|
2087
|
+
constructor(opts) {
|
|
2088
|
+
this.maxTokens = opts.maxTokens;
|
|
2089
|
+
this.refillRate = opts.refillRate;
|
|
2090
|
+
this.tokens = opts.maxTokens;
|
|
2091
|
+
this.lastRefill = Date.now();
|
|
2092
|
+
this.maxWaitMs = opts.maxWaitMs ?? 1e4;
|
|
2093
|
+
}
|
|
2094
|
+
refill() {
|
|
2095
|
+
const now = Date.now();
|
|
2096
|
+
const elapsed = (now - this.lastRefill) / 1e3;
|
|
2097
|
+
this.tokens = Math.min(
|
|
2098
|
+
this.maxTokens,
|
|
2099
|
+
this.tokens + elapsed * this.refillRate
|
|
2280
2100
|
);
|
|
2281
2101
|
this.lastRefill = now;
|
|
2282
2102
|
}
|
|
@@ -2331,47 +2151,373 @@ var toolRateLimiters = {
|
|
|
2331
2151
|
filesystem: new RateLimiter({ maxTokens: 40, refillRate: 20 })
|
|
2332
2152
|
};
|
|
2333
2153
|
function getLimiterForTool(toolName) {
|
|
2334
|
-
if (toolName.startsWith("browser_"))
|
|
2335
|
-
|
|
2336
|
-
if (
|
|
2337
|
-
|
|
2338
|
-
|
|
2154
|
+
if (toolName.includes("assistme-browser") || toolName.startsWith("browser_"))
|
|
2155
|
+
return toolRateLimiters.browser;
|
|
2156
|
+
if (toolName === "Bash" || toolName === "execute_command")
|
|
2157
|
+
return toolRateLimiters.shell;
|
|
2158
|
+
if ([
|
|
2159
|
+
"Read",
|
|
2160
|
+
"Write",
|
|
2161
|
+
"Edit",
|
|
2162
|
+
"Glob",
|
|
2163
|
+
"Grep",
|
|
2164
|
+
"read_file",
|
|
2165
|
+
"write_file",
|
|
2166
|
+
"search_files",
|
|
2167
|
+
"search_content",
|
|
2168
|
+
"list_directory"
|
|
2169
|
+
].includes(toolName))
|
|
2339
2170
|
return toolRateLimiters.filesystem;
|
|
2340
2171
|
return null;
|
|
2341
2172
|
}
|
|
2342
2173
|
|
|
2343
|
-
// src/
|
|
2344
|
-
async function
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2174
|
+
// src/agent/mcp-servers.ts
|
|
2175
|
+
async function callTool(name, input) {
|
|
2176
|
+
const limiter = getLimiterForTool(name);
|
|
2177
|
+
if (limiter) await limiter.acquire();
|
|
2178
|
+
const result = await executeTool(name, input);
|
|
2179
|
+
return { content: [{ type: "text", text: result }] };
|
|
2180
|
+
}
|
|
2181
|
+
var BROWSER_TOOL_NAMES = [
|
|
2182
|
+
"browser_connect",
|
|
2183
|
+
"browser_navigate",
|
|
2184
|
+
"browser_read_page",
|
|
2185
|
+
"browser_screenshot",
|
|
2186
|
+
"browser_click",
|
|
2187
|
+
"browser_type",
|
|
2188
|
+
"browser_press_key",
|
|
2189
|
+
"browser_scroll",
|
|
2190
|
+
"browser_get_elements",
|
|
2191
|
+
"browser_evaluate",
|
|
2192
|
+
"browser_list_tabs",
|
|
2193
|
+
"browser_switch_tab",
|
|
2194
|
+
"browser_new_tab",
|
|
2195
|
+
"browser_request_user_action"
|
|
2196
|
+
];
|
|
2197
|
+
function createBrowserMcpServer() {
|
|
2198
|
+
return createSdkMcpServer({
|
|
2199
|
+
name: "assistme-browser",
|
|
2200
|
+
version: "1.0.0",
|
|
2201
|
+
tools: [
|
|
2202
|
+
tool(
|
|
2203
|
+
"browser_connect",
|
|
2204
|
+
"Connect to the user's real Chrome browser via CDP. The user must have Chrome running with --remote-debugging-port=9222.",
|
|
2205
|
+
{ tab_index: z.number().optional().describe("Tab index (default: 0)") },
|
|
2206
|
+
async (args) => callTool("browser_connect", args)
|
|
2207
|
+
),
|
|
2208
|
+
tool(
|
|
2209
|
+
"browser_navigate",
|
|
2210
|
+
"Navigate the user's browser to a URL, using the user's real browser with all their cookies and logins.",
|
|
2211
|
+
{ url: z.string().describe("URL to navigate to") },
|
|
2212
|
+
async (args) => callTool("browser_navigate", args)
|
|
2213
|
+
),
|
|
2214
|
+
tool(
|
|
2215
|
+
"browser_read_page",
|
|
2216
|
+
"Read the text content of the currently open page. Returns page title, URL, and main text content.",
|
|
2217
|
+
{},
|
|
2218
|
+
async () => callTool("browser_read_page", {})
|
|
2219
|
+
),
|
|
2220
|
+
tool(
|
|
2221
|
+
"browser_screenshot",
|
|
2222
|
+
"Take a screenshot of the current browser page. Returns a base64-encoded PNG image.",
|
|
2223
|
+
{},
|
|
2224
|
+
async () => {
|
|
2225
|
+
const limiter = getLimiterForTool("browser_screenshot");
|
|
2226
|
+
if (limiter) await limiter.acquire();
|
|
2227
|
+
const base64 = await executeTool("browser_screenshot", {});
|
|
2228
|
+
if (base64.length > 100) {
|
|
2229
|
+
return {
|
|
2230
|
+
content: [
|
|
2231
|
+
{
|
|
2232
|
+
type: "image",
|
|
2233
|
+
data: base64,
|
|
2234
|
+
mimeType: "image/png"
|
|
2235
|
+
}
|
|
2236
|
+
]
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
return { content: [{ type: "text", text: base64 }] };
|
|
2369
2240
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2241
|
+
),
|
|
2242
|
+
tool(
|
|
2243
|
+
"browser_click",
|
|
2244
|
+
"Click on an element in the user's browser using a CSS selector.",
|
|
2245
|
+
{ selector: z.string().describe("CSS selector of the element to click") },
|
|
2246
|
+
async (args) => callTool("browser_click", args)
|
|
2247
|
+
),
|
|
2248
|
+
tool(
|
|
2249
|
+
"browser_type",
|
|
2250
|
+
"Type text into an input field in the user's browser.",
|
|
2251
|
+
{
|
|
2252
|
+
selector: z.string().describe("CSS selector of the input element"),
|
|
2253
|
+
text: z.string().describe("Text to type")
|
|
2254
|
+
},
|
|
2255
|
+
async (args) => callTool("browser_type", args)
|
|
2256
|
+
),
|
|
2257
|
+
tool(
|
|
2258
|
+
"browser_press_key",
|
|
2259
|
+
"Press a keyboard key in the browser. Supports: Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp.",
|
|
2260
|
+
{ key: z.string().describe("Key to press") },
|
|
2261
|
+
async (args) => callTool("browser_press_key", args)
|
|
2262
|
+
),
|
|
2263
|
+
tool(
|
|
2264
|
+
"browser_scroll",
|
|
2265
|
+
"Scroll the page up or down.",
|
|
2266
|
+
{ direction: z.string().describe("'down' or 'up'") },
|
|
2267
|
+
async (args) => callTool("browser_scroll", args)
|
|
2268
|
+
),
|
|
2269
|
+
tool(
|
|
2270
|
+
"browser_get_elements",
|
|
2271
|
+
"Find all interactive elements (links, buttons, inputs) on the current page.",
|
|
2272
|
+
{},
|
|
2273
|
+
async () => callTool("browser_get_elements", {})
|
|
2274
|
+
),
|
|
2275
|
+
tool(
|
|
2276
|
+
"browser_evaluate",
|
|
2277
|
+
"Execute JavaScript in the browser page context.",
|
|
2278
|
+
{ expression: z.string().describe("JavaScript expression to evaluate") },
|
|
2279
|
+
async (args) => callTool("browser_evaluate", args)
|
|
2280
|
+
),
|
|
2281
|
+
tool(
|
|
2282
|
+
"browser_list_tabs",
|
|
2283
|
+
"List all open tabs in the user's browser.",
|
|
2284
|
+
{},
|
|
2285
|
+
async () => callTool("browser_list_tabs", {})
|
|
2286
|
+
),
|
|
2287
|
+
tool(
|
|
2288
|
+
"browser_switch_tab",
|
|
2289
|
+
"Switch to a different browser tab by index.",
|
|
2290
|
+
{ index: z.number().describe("Tab index") },
|
|
2291
|
+
async (args) => callTool("browser_switch_tab", args)
|
|
2292
|
+
),
|
|
2293
|
+
tool(
|
|
2294
|
+
"browser_new_tab",
|
|
2295
|
+
"Open a new tab in the user's browser, optionally navigating to a URL.",
|
|
2296
|
+
{ url: z.string().optional().describe("URL to open (default: blank)") },
|
|
2297
|
+
async (args) => callTool("browser_new_tab", args)
|
|
2298
|
+
),
|
|
2299
|
+
tool(
|
|
2300
|
+
"browser_request_user_action",
|
|
2301
|
+
"Request the user to perform an action in their browser (login, CAPTCHA, 2FA, etc.).",
|
|
2302
|
+
{
|
|
2303
|
+
message: z.string().describe("Clear description of what the user needs to do"),
|
|
2304
|
+
wait_seconds: z.number().optional().describe("How long to wait (default: 60)")
|
|
2305
|
+
},
|
|
2306
|
+
async (args) => callTool("browser_request_user_action", args)
|
|
2307
|
+
)
|
|
2308
|
+
]
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
function createAgentToolsServer(deps) {
|
|
2312
|
+
const { memoryManager, skillManager, taskId } = deps;
|
|
2313
|
+
return createSdkMcpServer({
|
|
2314
|
+
name: "assistme-agent",
|
|
2315
|
+
version: "1.0.0",
|
|
2316
|
+
tools: [
|
|
2317
|
+
tool(
|
|
2318
|
+
"memory_store",
|
|
2319
|
+
"Store a memory about the user that persists across conversations. Use when you learn preferences, habits, or standing instructions.",
|
|
2320
|
+
{
|
|
2321
|
+
content: z.string().describe("What to remember (concise, factual statement)"),
|
|
2322
|
+
category: z.string().optional().describe(
|
|
2323
|
+
"Category: general, preference, instruction, context, skill_learned, fact"
|
|
2324
|
+
),
|
|
2325
|
+
importance: z.number().optional().describe("Importance 1-10 (default: 5). Use 8+ for instructions"),
|
|
2326
|
+
tags: z.array(z.string()).optional().describe("Optional tags for searchability")
|
|
2327
|
+
},
|
|
2328
|
+
async (args) => {
|
|
2329
|
+
if (!memoryManager) {
|
|
2330
|
+
return {
|
|
2331
|
+
content: [
|
|
2332
|
+
{ type: "text", text: "Memory manager not available." }
|
|
2333
|
+
]
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
const mem = await memoryManager.remember(
|
|
2337
|
+
args.content,
|
|
2338
|
+
args.category || "general",
|
|
2339
|
+
{
|
|
2340
|
+
importance: args.importance || 5,
|
|
2341
|
+
tags: args.tags || [],
|
|
2342
|
+
sourceMessageId: taskId
|
|
2343
|
+
}
|
|
2344
|
+
);
|
|
2345
|
+
const result = `Memory stored: "${mem.content}" [${mem.category}, importance: ${mem.importance}]`;
|
|
2346
|
+
return { content: [{ type: "text", text: result }] };
|
|
2347
|
+
}
|
|
2348
|
+
),
|
|
2349
|
+
tool(
|
|
2350
|
+
"skill_create",
|
|
2351
|
+
"Create a new reusable skill from a workflow you just executed. Write generic, reusable instructions with placeholders like {product}, {query}.",
|
|
2352
|
+
{
|
|
2353
|
+
name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
|
|
2354
|
+
description: z.string().describe("One-line description of what this skill does"),
|
|
2355
|
+
instructions: z.string().describe("Markdown step-by-step instructions"),
|
|
2356
|
+
emoji: z.string().optional().describe("Single emoji representing this skill")
|
|
2357
|
+
},
|
|
2358
|
+
async (args) => {
|
|
2359
|
+
const existing = skillManager.findSimilar(args.name);
|
|
2360
|
+
if (existing) {
|
|
2361
|
+
return {
|
|
2362
|
+
content: [
|
|
2363
|
+
{
|
|
2364
|
+
type: "text",
|
|
2365
|
+
text: `A similar skill "${existing.name}" already exists. Use skill_improve to update it instead.`
|
|
2366
|
+
}
|
|
2367
|
+
]
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
const filePath = skillManager.create(
|
|
2371
|
+
args.name,
|
|
2372
|
+
args.description,
|
|
2373
|
+
args.instructions
|
|
2374
|
+
);
|
|
2375
|
+
if (args.emoji) {
|
|
2376
|
+
const skill = skillManager.get(args.name);
|
|
2377
|
+
if (skill) {
|
|
2378
|
+
skill.metadata.emoji = args.emoji;
|
|
2379
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
2380
|
+
const metaJson = JSON.stringify({
|
|
2381
|
+
openclaw: { emoji: args.emoji }
|
|
2382
|
+
});
|
|
2383
|
+
const fileContent = `---
|
|
2384
|
+
name: ${args.name}
|
|
2385
|
+
description: ${args.description}
|
|
2386
|
+
version: 1.0.0
|
|
2387
|
+
user-invocable: true
|
|
2388
|
+
metadata: ${metaJson}
|
|
2389
|
+
---
|
|
2390
|
+
|
|
2391
|
+
${args.instructions}
|
|
2392
|
+
`;
|
|
2393
|
+
writeFileSync2(filePath, fileContent, "utf-8");
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
log.success(`Self-improvement: created skill "${args.name}"`);
|
|
2397
|
+
return {
|
|
2398
|
+
content: [
|
|
2399
|
+
{
|
|
2400
|
+
type: "text",
|
|
2401
|
+
text: `Skill "${args.name}" created and saved to ${filePath}. It will be automatically matched to future similar tasks.`
|
|
2402
|
+
}
|
|
2403
|
+
]
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
),
|
|
2407
|
+
tool(
|
|
2408
|
+
"skill_improve",
|
|
2409
|
+
"Improve an existing skill with better instructions based on what you just learned. Version auto-bumped.",
|
|
2410
|
+
{
|
|
2411
|
+
name: z.string().describe("Name of the existing skill to improve"),
|
|
2412
|
+
improved_instructions: z.string().describe("Full updated markdown instructions (not a diff)"),
|
|
2413
|
+
description: z.string().optional().describe("Updated description (optional)")
|
|
2414
|
+
},
|
|
2415
|
+
async (args) => {
|
|
2416
|
+
const existing = skillManager.get(args.name);
|
|
2417
|
+
if (!existing) {
|
|
2418
|
+
const available = skillManager.getAll().map((s) => s.name).join(", ");
|
|
2419
|
+
return {
|
|
2420
|
+
content: [
|
|
2421
|
+
{
|
|
2422
|
+
type: "text",
|
|
2423
|
+
text: `Skill "${args.name}" not found. Available skills: ${available}`
|
|
2424
|
+
}
|
|
2425
|
+
]
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
if (existing.source === "bundled") {
|
|
2429
|
+
skillManager.create(
|
|
2430
|
+
args.name,
|
|
2431
|
+
args.description || existing.description,
|
|
2432
|
+
args.improved_instructions
|
|
2433
|
+
);
|
|
2434
|
+
log.success(
|
|
2435
|
+
`Self-improvement: overrode bundled skill "${args.name}"`
|
|
2436
|
+
);
|
|
2437
|
+
return {
|
|
2438
|
+
content: [
|
|
2439
|
+
{
|
|
2440
|
+
type: "text",
|
|
2441
|
+
text: `Bundled skill "${args.name}" overridden with improved version.`
|
|
2442
|
+
}
|
|
2443
|
+
]
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
const updated = skillManager.update(
|
|
2447
|
+
args.name,
|
|
2448
|
+
args.improved_instructions,
|
|
2449
|
+
args.description
|
|
2450
|
+
);
|
|
2451
|
+
if (updated) {
|
|
2452
|
+
log.success(`Self-improvement: improved skill "${args.name}"`);
|
|
2453
|
+
return {
|
|
2454
|
+
content: [
|
|
2455
|
+
{
|
|
2456
|
+
type: "text",
|
|
2457
|
+
text: `Skill "${args.name}" improved and version bumped.`
|
|
2458
|
+
}
|
|
2459
|
+
]
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
return {
|
|
2463
|
+
content: [
|
|
2464
|
+
{
|
|
2465
|
+
type: "text",
|
|
2466
|
+
text: `Failed to update skill "${args.name}".`
|
|
2467
|
+
}
|
|
2468
|
+
]
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
)
|
|
2472
|
+
]
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// src/agent/event-hooks.ts
|
|
2477
|
+
function stripMcpPrefix(toolName) {
|
|
2478
|
+
const match = toolName.match(/^mcp__[^_]+(?:__)?(.+)$/);
|
|
2479
|
+
return match ? match[1] : toolName;
|
|
2480
|
+
}
|
|
2481
|
+
function createEventHooks(taskId, toolCallRecords) {
|
|
2482
|
+
const preToolUseHook = async (input) => {
|
|
2483
|
+
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
2484
|
+
const rawName = input.tool_name;
|
|
2485
|
+
const displayName = stripMcpPrefix(rawName);
|
|
2486
|
+
const toolInput = input.tool_input;
|
|
2487
|
+
log.tool(displayName, JSON.stringify(toolInput).slice(0, 200));
|
|
2488
|
+
await emitEvent(taskId, "tool_use_start", { name: displayName });
|
|
2489
|
+
await emitEvent(taskId, "tool_use_input", { input: toolInput });
|
|
2490
|
+
if (displayName === "browser_request_user_action") {
|
|
2491
|
+
await emitEvent(taskId, "status_change", {
|
|
2492
|
+
status: "waiting_for_user",
|
|
2493
|
+
message: toolInput?.message
|
|
2494
|
+
});
|
|
2372
2495
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2496
|
+
return { continue: true };
|
|
2497
|
+
};
|
|
2498
|
+
const postToolUseHook = async (input) => {
|
|
2499
|
+
if (input.hook_event_name !== "PostToolUse") return {};
|
|
2500
|
+
const rawName = input.tool_name;
|
|
2501
|
+
const displayName = stripMcpPrefix(rawName);
|
|
2502
|
+
const toolInput = input.tool_input;
|
|
2503
|
+
const toolResponse = input.tool_response;
|
|
2504
|
+
const resultStr = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
|
|
2505
|
+
log.result(resultStr.slice(0, 200));
|
|
2506
|
+
await emitEvent(taskId, "tool_result", {
|
|
2507
|
+
name: displayName,
|
|
2508
|
+
result: resultStr.slice(0, 1e4)
|
|
2509
|
+
});
|
|
2510
|
+
toolCallRecords.push({
|
|
2511
|
+
name: displayName,
|
|
2512
|
+
input: toolInput || {},
|
|
2513
|
+
result: resultStr.slice(0, 300)
|
|
2514
|
+
});
|
|
2515
|
+
return {};
|
|
2516
|
+
};
|
|
2517
|
+
return {
|
|
2518
|
+
PreToolUse: [{ hooks: [preToolUseHook] }],
|
|
2519
|
+
PostToolUse: [{ hooks: [postToolUseHook] }]
|
|
2520
|
+
};
|
|
2375
2521
|
}
|
|
2376
2522
|
|
|
2377
2523
|
// src/agent/processor.ts
|
|
@@ -2385,21 +2531,23 @@ KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This
|
|
|
2385
2531
|
|
|
2386
2532
|
Available capabilities:
|
|
2387
2533
|
1. BROWSER CONTROL (user's real Chrome via CDP):
|
|
2388
|
-
-
|
|
2389
|
-
-
|
|
2390
|
-
- Click buttons, fill forms, scroll, switch tabs
|
|
2391
|
-
- Open new tabs, interact with any web page
|
|
2392
|
-
- If auth is needed: request the user to log in
|
|
2534
|
+
- Use browser tools (browser_connect, browser_navigate, browser_read_page, browser_screenshot, browser_click, browser_type, browser_press_key, browser_scroll, browser_get_elements, browser_evaluate, browser_list_tabs, browser_switch_tab, browser_new_tab) to control the user's real Chrome
|
|
2535
|
+
- If auth is needed: use browser_request_user_action to ask the user to log in
|
|
2393
2536
|
|
|
2394
|
-
2. FILE OPERATIONS:
|
|
2395
|
-
- Read,
|
|
2396
|
-
-
|
|
2537
|
+
2. FILE OPERATIONS & SHELL:
|
|
2538
|
+
- Read, Write, Edit tools for file operations
|
|
2539
|
+
- Bash tool for shell commands
|
|
2540
|
+
- Glob and Grep for file search
|
|
2397
2541
|
|
|
2398
2542
|
3. MEMORY:
|
|
2399
2543
|
- You can remember things about the user using memory_store
|
|
2400
2544
|
- Use this when you learn preferences, important facts, or standing instructions
|
|
2401
2545
|
- Your stored memories persist across conversations
|
|
2402
2546
|
|
|
2547
|
+
4. SELF-IMPROVEMENT:
|
|
2548
|
+
- If you discover a reusable workflow pattern, use skill_create to save it for future tasks
|
|
2549
|
+
- If a skill's instructions could be improved based on your experience, use skill_improve
|
|
2550
|
+
|
|
2403
2551
|
Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7\u683C"):
|
|
2404
2552
|
1. browser_connect \u2192 connect to user's Chrome
|
|
2405
2553
|
2. browser_new_tab \u2192 open a new tab
|
|
@@ -2417,14 +2565,6 @@ Guidelines:
|
|
|
2417
2565
|
- Be thorough: check multiple sources when comparing prices/products
|
|
2418
2566
|
- Summarize results clearly at the end
|
|
2419
2567
|
- When you learn something about the user (preferences, habits), use memory_store to remember it
|
|
2420
|
-
- If you discover a reusable workflow pattern, use skill_create to save it for future tasks
|
|
2421
|
-
- If a skill's instructions could be improved based on your experience, use skill_improve
|
|
2422
|
-
|
|
2423
|
-
4. SELF-IMPROVEMENT:
|
|
2424
|
-
- If you discover a good multi-step workflow during a task, use skill_create to save it as a reusable skill
|
|
2425
|
-
- If you're using a skill but find a better approach, use skill_improve to update it
|
|
2426
|
-
- Skills you create will be automatically matched to future similar tasks
|
|
2427
|
-
- Example: After successfully comparing prices across 5 sites, save the workflow so next time it's even faster
|
|
2428
2568
|
|
|
2429
2569
|
Workspace path: {workspace_path}`;
|
|
2430
2570
|
var TaskProcessor = class {
|
|
@@ -2441,11 +2581,12 @@ var TaskProcessor = class {
|
|
|
2441
2581
|
const config = getConfig();
|
|
2442
2582
|
resetEventSequence();
|
|
2443
2583
|
const taskTimeoutMs = (config.taskTimeoutMinutes || 10) * 6e4;
|
|
2444
|
-
|
|
2584
|
+
newCorrelationId();
|
|
2445
2585
|
log.info(`Processing task ${task.id.slice(0, 8)}...`);
|
|
2446
2586
|
let finalResponse = "";
|
|
2447
2587
|
const toolCallRecords = [];
|
|
2448
2588
|
const usedSkillNames = [];
|
|
2589
|
+
let tokenUsage;
|
|
2449
2590
|
try {
|
|
2450
2591
|
await emitEvent(task.id, "status_change", { status: "running" });
|
|
2451
2592
|
let systemPrompt = BASE_SYSTEM_PROMPT.replace(
|
|
@@ -2470,271 +2611,148 @@ var TaskProcessor = class {
|
|
|
2470
2611
|
for (const s of matchedSkills) {
|
|
2471
2612
|
usedSkillNames.push(s.name);
|
|
2472
2613
|
}
|
|
2473
|
-
const
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
description: "Optional tags for searchability"
|
|
2497
|
-
}
|
|
2498
|
-
},
|
|
2499
|
-
required: ["content"]
|
|
2500
|
-
}
|
|
2501
|
-
},
|
|
2502
|
-
{
|
|
2503
|
-
name: "skill_create",
|
|
2504
|
-
description: "Create a new reusable skill from a workflow you just executed. Use this when you discover a good multi-step approach that would be useful for future similar tasks. The skill will be saved as a SKILL.md file and automatically matched to future tasks. Write generic, reusable instructions with placeholders like {product}, {query}, {website}.",
|
|
2505
|
-
input_schema: {
|
|
2506
|
-
type: "object",
|
|
2507
|
-
properties: {
|
|
2508
|
-
name: {
|
|
2509
|
-
type: "string",
|
|
2510
|
-
description: "Skill name in kebab-case, e.g. 'flight-booking' or 'competitor-analysis'"
|
|
2511
|
-
},
|
|
2512
|
-
description: {
|
|
2513
|
-
type: "string",
|
|
2514
|
-
description: "One-line description of what this skill does"
|
|
2515
|
-
},
|
|
2516
|
-
instructions: {
|
|
2517
|
-
type: "string",
|
|
2518
|
-
description: "Markdown step-by-step instructions. Use ## headings, numbered steps, and **bold** for key actions. Replace specific values with {placeholders}."
|
|
2519
|
-
},
|
|
2520
|
-
emoji: {
|
|
2521
|
-
type: "string",
|
|
2522
|
-
description: "Single emoji that represents this skill"
|
|
2523
|
-
}
|
|
2524
|
-
},
|
|
2525
|
-
required: ["name", "description", "instructions"]
|
|
2526
|
-
}
|
|
2527
|
-
},
|
|
2528
|
-
{
|
|
2529
|
-
name: "skill_improve",
|
|
2530
|
-
description: "Improve an existing skill with better instructions based on what you just learned during this task. Use this when you followed a skill's workflow but found a better approach. The skill version will be auto-bumped.",
|
|
2531
|
-
input_schema: {
|
|
2532
|
-
type: "object",
|
|
2533
|
-
properties: {
|
|
2534
|
-
name: {
|
|
2535
|
-
type: "string",
|
|
2536
|
-
description: "Name of the existing skill to improve"
|
|
2537
|
-
},
|
|
2538
|
-
improved_instructions: {
|
|
2539
|
-
type: "string",
|
|
2540
|
-
description: "The full updated markdown instructions (not a diff, the complete new version)"
|
|
2541
|
-
},
|
|
2542
|
-
description: {
|
|
2543
|
-
type: "string",
|
|
2544
|
-
description: "Updated description (optional, keeps existing if not provided)"
|
|
2545
|
-
}
|
|
2546
|
-
},
|
|
2547
|
-
required: ["name", "improved_instructions"]
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2614
|
+
const browserServer = createBrowserMcpServer();
|
|
2615
|
+
const agentToolsServer = createAgentToolsServer({
|
|
2616
|
+
memoryManager: this.memoryManager,
|
|
2617
|
+
skillManager: this.skillManager,
|
|
2618
|
+
taskId: task.id
|
|
2619
|
+
});
|
|
2620
|
+
const eventHooks = createEventHooks(task.id, toolCallRecords);
|
|
2621
|
+
const allowedTools = [
|
|
2622
|
+
// SDK built-in tools
|
|
2623
|
+
"Read",
|
|
2624
|
+
"Write",
|
|
2625
|
+
"Edit",
|
|
2626
|
+
"Bash",
|
|
2627
|
+
"Glob",
|
|
2628
|
+
"Grep",
|
|
2629
|
+
// Browser MCP tools
|
|
2630
|
+
...BROWSER_TOOL_NAMES.map(
|
|
2631
|
+
(n) => `mcp__assistme-browser__${n}`
|
|
2632
|
+
),
|
|
2633
|
+
// Agent MCP tools (memory, skills)
|
|
2634
|
+
"mcp__assistme-agent__memory_store",
|
|
2635
|
+
"mcp__assistme-agent__skill_create",
|
|
2636
|
+
"mcp__assistme-agent__skill_improve"
|
|
2550
2637
|
];
|
|
2638
|
+
async function* promptMessages() {
|
|
2639
|
+
yield {
|
|
2640
|
+
type: "user",
|
|
2641
|
+
message: {
|
|
2642
|
+
role: "user",
|
|
2643
|
+
content: task.prompt
|
|
2644
|
+
},
|
|
2645
|
+
parent_tool_use_id: null,
|
|
2646
|
+
session_id: ""
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
const abortController = new AbortController();
|
|
2551
2650
|
const options = {
|
|
2552
2651
|
model: config.model,
|
|
2553
2652
|
systemPrompt,
|
|
2554
|
-
allowedTools: [
|
|
2555
|
-
// File system tools
|
|
2556
|
-
"read_file",
|
|
2557
|
-
"write_file",
|
|
2558
|
-
"search_files",
|
|
2559
|
-
"search_content",
|
|
2560
|
-
"list_directory",
|
|
2561
|
-
"execute_command",
|
|
2562
|
-
// Browser tools
|
|
2563
|
-
"browser_connect",
|
|
2564
|
-
"browser_navigate",
|
|
2565
|
-
"browser_read_page",
|
|
2566
|
-
"browser_screenshot",
|
|
2567
|
-
"browser_click",
|
|
2568
|
-
"browser_type",
|
|
2569
|
-
"browser_press_key",
|
|
2570
|
-
"browser_scroll",
|
|
2571
|
-
"browser_get_elements",
|
|
2572
|
-
"browser_evaluate",
|
|
2573
|
-
"browser_list_tabs",
|
|
2574
|
-
"browser_switch_tab",
|
|
2575
|
-
"browser_new_tab",
|
|
2576
|
-
"browser_request_user_action",
|
|
2577
|
-
// Memory
|
|
2578
|
-
"memory_store",
|
|
2579
|
-
// Self-improvement
|
|
2580
|
-
"skill_create",
|
|
2581
|
-
"skill_improve"
|
|
2582
|
-
],
|
|
2583
|
-
tools: toolDefs,
|
|
2584
2653
|
cwd: config.workspacePath,
|
|
2585
|
-
maxTurns: config.maxTurns
|
|
2654
|
+
maxTurns: config.maxTurns,
|
|
2655
|
+
allowedTools,
|
|
2656
|
+
permissionMode: "bypassPermissions",
|
|
2657
|
+
allowDangerouslySkipPermissions: true,
|
|
2658
|
+
mcpServers: {
|
|
2659
|
+
"assistme-browser": browserServer,
|
|
2660
|
+
"assistme-agent": agentToolsServer
|
|
2661
|
+
},
|
|
2662
|
+
hooks: eventHooks,
|
|
2663
|
+
persistSession: false,
|
|
2664
|
+
abortController
|
|
2586
2665
|
};
|
|
2587
2666
|
const taskStartTime = Date.now();
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2667
|
+
const timeoutId = setTimeout(() => {
|
|
2668
|
+
abortController.abort();
|
|
2669
|
+
}, taskTimeoutMs);
|
|
2670
|
+
try {
|
|
2671
|
+
for await (const message of query({
|
|
2672
|
+
prompt: promptMessages(),
|
|
2673
|
+
options
|
|
2674
|
+
})) {
|
|
2593
2675
|
if (Date.now() - taskStartTime > taskTimeoutMs) {
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
);
|
|
2676
|
+
finalResponse += "\n\n[Task timed out]";
|
|
2677
|
+
break;
|
|
2597
2678
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
const content = emoji ? instructions : instructions;
|
|
2617
|
-
const filePath = this.skillManager.create(name, description, content);
|
|
2618
|
-
if (emoji) {
|
|
2619
|
-
const skill = this.skillManager.get(name);
|
|
2620
|
-
if (skill) {
|
|
2621
|
-
skill.metadata.emoji = emoji;
|
|
2622
|
-
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
2623
|
-
const metaJson = JSON.stringify({ openclaw: { emoji } });
|
|
2624
|
-
const fileContent = `---
|
|
2625
|
-
name: ${name}
|
|
2626
|
-
description: ${description}
|
|
2627
|
-
version: 1.0.0
|
|
2628
|
-
user-invocable: true
|
|
2629
|
-
metadata: ${metaJson}
|
|
2630
|
-
---
|
|
2631
|
-
|
|
2632
|
-
${instructions}
|
|
2633
|
-
`;
|
|
2634
|
-
await writeFile2(filePath, fileContent, "utf-8");
|
|
2635
|
-
}
|
|
2679
|
+
switch (message.type) {
|
|
2680
|
+
case "assistant": {
|
|
2681
|
+
const assistantMsg = message;
|
|
2682
|
+
for (const block of assistantMsg.message.content) {
|
|
2683
|
+
if (block.type === "text") {
|
|
2684
|
+
finalResponse += block.text;
|
|
2685
|
+
log.agent(block.text);
|
|
2686
|
+
await emitEvent(task.id, "text_delta", {
|
|
2687
|
+
text: block.text
|
|
2688
|
+
});
|
|
2689
|
+
} else if (block.type === "thinking" && "thinking" in block) {
|
|
2690
|
+
const thinkingText = block.thinking;
|
|
2691
|
+
log.debug(
|
|
2692
|
+
`Thinking: ${thinkingText.slice(0, 100)}...`
|
|
2693
|
+
);
|
|
2694
|
+
await emitEvent(task.id, "thinking", {
|
|
2695
|
+
text: thinkingText
|
|
2696
|
+
});
|
|
2636
2697
|
}
|
|
2637
|
-
result = `Skill "${name}" created and saved to ${filePath}. It will be automatically matched to future similar tasks.`;
|
|
2638
|
-
log.success(`Self-improvement: created skill "${name}"`);
|
|
2639
2698
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
const
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2699
|
+
break;
|
|
2700
|
+
}
|
|
2701
|
+
case "result": {
|
|
2702
|
+
const resultMsg = message;
|
|
2703
|
+
tokenUsage = {
|
|
2704
|
+
input_tokens: resultMsg.usage.input_tokens,
|
|
2705
|
+
output_tokens: resultMsg.usage.output_tokens
|
|
2706
|
+
};
|
|
2707
|
+
if (resultMsg.subtype === "success") {
|
|
2708
|
+
const successMsg = resultMsg;
|
|
2709
|
+
if (!finalResponse && successMsg.result) {
|
|
2710
|
+
finalResponse = successMsg.result;
|
|
2711
|
+
}
|
|
2712
|
+
log.info(
|
|
2713
|
+
`Task cost: $${successMsg.total_cost_usd.toFixed(4)}, turns: ${successMsg.num_turns}`
|
|
2714
|
+
);
|
|
2652
2715
|
} else {
|
|
2653
|
-
const
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
} else {
|
|
2658
|
-
result = `Failed to update skill "${name}".`;
|
|
2716
|
+
const errorMsg = resultMsg;
|
|
2717
|
+
log.warn(`SDK result: ${errorMsg.subtype}`);
|
|
2718
|
+
for (const err of errorMsg.errors) {
|
|
2719
|
+
await emitEvent(task.id, "error", { message: err });
|
|
2659
2720
|
}
|
|
2660
2721
|
}
|
|
2661
|
-
|
|
2662
|
-
const mem = await this.memoryManager.remember(
|
|
2663
|
-
toolInput.content,
|
|
2664
|
-
toolInput.category || "general",
|
|
2665
|
-
{
|
|
2666
|
-
importance: toolInput.importance || 5,
|
|
2667
|
-
tags: toolInput.tags || [],
|
|
2668
|
-
sourceMessageId: task.id
|
|
2669
|
-
}
|
|
2670
|
-
);
|
|
2671
|
-
result = `Memory stored: "${mem.content}" [${mem.category}, importance: ${mem.importance}]`;
|
|
2672
|
-
} else {
|
|
2673
|
-
result = await executeTool(toolName, toolInput);
|
|
2722
|
+
break;
|
|
2674
2723
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
log.error(`Tool error: ${result}`);
|
|
2724
|
+
default:
|
|
2725
|
+
log.debug(`SDK message type: ${message.type}`);
|
|
2726
|
+
break;
|
|
2679
2727
|
}
|
|
2680
|
-
toolCallRecords.push({
|
|
2681
|
-
name: toolName,
|
|
2682
|
-
input: toolInput,
|
|
2683
|
-
result: result.slice(0, 300)
|
|
2684
|
-
});
|
|
2685
|
-
await emitEvent(task.id, "tool_result", {
|
|
2686
|
-
name: toolName,
|
|
2687
|
-
result: result.slice(0, 1e4)
|
|
2688
|
-
});
|
|
2689
|
-
if (toolName === "browser_request_user_action") {
|
|
2690
|
-
await emitEvent(task.id, "status_change", {
|
|
2691
|
-
status: "waiting_for_user",
|
|
2692
|
-
message: toolInput.message
|
|
2693
|
-
});
|
|
2694
|
-
}
|
|
2695
|
-
if (toolName === "browser_screenshot" && result.length > 100) {
|
|
2696
|
-
return {
|
|
2697
|
-
type: "image",
|
|
2698
|
-
data: result,
|
|
2699
|
-
mediaType: "image/png"
|
|
2700
|
-
};
|
|
2701
|
-
}
|
|
2702
|
-
return result;
|
|
2703
|
-
}
|
|
2704
|
-
})) {
|
|
2705
|
-
if (Date.now() - taskStartTime > taskTimeoutMs) {
|
|
2706
|
-
log.warn("Task timed out \u2014 stopping agentic loop");
|
|
2707
|
-
finalResponse += "\n\n[Task timed out]";
|
|
2708
|
-
break;
|
|
2709
|
-
}
|
|
2710
|
-
if (isTextContent(message)) {
|
|
2711
|
-
finalResponse += message.text;
|
|
2712
|
-
log.agent(message.text);
|
|
2713
|
-
await emitEvent(task.id, "text_delta", { text: message.text });
|
|
2714
|
-
} else if (isThinkingContent(message)) {
|
|
2715
|
-
log.debug(`Thinking: ${message.thinking.slice(0, 100)}...`);
|
|
2716
|
-
await emitEvent(task.id, "thinking", { text: message.thinking });
|
|
2717
2728
|
}
|
|
2729
|
+
} finally {
|
|
2730
|
+
clearTimeout(timeoutId);
|
|
2718
2731
|
}
|
|
2719
|
-
await withRetry(
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2732
|
+
await withRetry(
|
|
2733
|
+
() => completeTask(task.id, finalResponse, tokenUsage),
|
|
2734
|
+
{
|
|
2735
|
+
maxRetries: 2,
|
|
2736
|
+
baseDelayMs: 300,
|
|
2737
|
+
label: "completeTask"
|
|
2738
|
+
}
|
|
2739
|
+
);
|
|
2724
2740
|
await emitEvent(task.id, "status_change", { status: "completed" });
|
|
2725
2741
|
log.success("Task completed.");
|
|
2726
2742
|
if (this.memoryManager && finalResponse) {
|
|
2727
2743
|
const mm = this.memoryManager;
|
|
2728
|
-
const
|
|
2744
|
+
const taskIdRef = task.id;
|
|
2729
2745
|
extractMemoriesWithLLM(task.prompt, finalResponse).then(async (memories) => {
|
|
2730
2746
|
for (const mem of memories) {
|
|
2731
2747
|
try {
|
|
2732
2748
|
await mm.remember(mem.content, mem.category, {
|
|
2733
2749
|
importance: mem.importance,
|
|
2734
2750
|
tags: mem.tags,
|
|
2735
|
-
sourceMessageId:
|
|
2751
|
+
sourceMessageId: taskIdRef
|
|
2736
2752
|
});
|
|
2737
|
-
log.info(
|
|
2753
|
+
log.info(
|
|
2754
|
+
`Memory extracted: [${mem.category}] ${mem.content.slice(0, 60)}...`
|
|
2755
|
+
);
|
|
2738
2756
|
} catch {
|
|
2739
2757
|
}
|
|
2740
2758
|
}
|
|
@@ -2758,7 +2776,6 @@ ${instructions}
|
|
|
2758
2776
|
);
|
|
2759
2777
|
return;
|
|
2760
2778
|
}
|
|
2761
|
-
const metaJson = extracted.emoji ? JSON.stringify({ openclaw: { emoji: extracted.emoji } }) : "";
|
|
2762
2779
|
const filePath = sm.create(
|
|
2763
2780
|
extracted.name,
|
|
2764
2781
|
extracted.description,
|
|
@@ -2766,12 +2783,15 @@ ${instructions}
|
|
|
2766
2783
|
);
|
|
2767
2784
|
if (extracted.emoji) {
|
|
2768
2785
|
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
2786
|
+
const metaJson = JSON.stringify({
|
|
2787
|
+
openclaw: { emoji: extracted.emoji }
|
|
2788
|
+
});
|
|
2769
2789
|
const fileContent = `---
|
|
2770
2790
|
name: ${extracted.name}
|
|
2771
2791
|
description: ${extracted.description}
|
|
2772
2792
|
version: 1.0.0
|
|
2773
|
-
user-invocable: true
|
|
2774
|
-
metadata: ${metaJson}
|
|
2793
|
+
user-invocable: true
|
|
2794
|
+
metadata: ${metaJson}
|
|
2775
2795
|
---
|
|
2776
2796
|
|
|
2777
2797
|
${extracted.steps}
|
|
@@ -2794,7 +2814,11 @@ ${extracted.steps}
|
|
|
2794
2814
|
).then(async (improvement) => {
|
|
2795
2815
|
if (!improvement) return;
|
|
2796
2816
|
if (skill.source === "bundled") {
|
|
2797
|
-
sm.create(
|
|
2817
|
+
sm.create(
|
|
2818
|
+
skillName,
|
|
2819
|
+
skill.description,
|
|
2820
|
+
improvement.improved_steps
|
|
2821
|
+
);
|
|
2798
2822
|
} else {
|
|
2799
2823
|
sm.update(skillName, improvement.improved_steps);
|
|
2800
2824
|
}
|
|
@@ -2823,12 +2847,6 @@ ${extracted.steps}
|
|
|
2823
2847
|
}
|
|
2824
2848
|
}
|
|
2825
2849
|
};
|
|
2826
|
-
function isTextContent(msg) {
|
|
2827
|
-
return typeof msg === "object" && msg !== null && "type" in msg && msg.type === "text" && "text" in msg;
|
|
2828
|
-
}
|
|
2829
|
-
function isThinkingContent(msg) {
|
|
2830
|
-
return typeof msg === "object" && msg !== null && "type" in msg && msg.type === "thinking" && "thinking" in msg;
|
|
2831
|
-
}
|
|
2832
2850
|
|
|
2833
2851
|
// src/index.ts
|
|
2834
2852
|
import { createInterface } from "readline";
|
|
@@ -3080,18 +3098,7 @@ program.command("start", { isDefault: true }).description("Start the agent and l
|
|
|
3080
3098
|
}
|
|
3081
3099
|
log.agent(`Processing: "${input}"`);
|
|
3082
3100
|
try {
|
|
3083
|
-
|
|
3084
|
-
const session = sessionManager.getSession();
|
|
3085
|
-
const conversationId = sessionManager.getConversationId();
|
|
3086
|
-
if (session && conversationId) {
|
|
3087
|
-
const task = await createTask2(
|
|
3088
|
-
conversationId,
|
|
3089
|
-
userId,
|
|
3090
|
-
session.id,
|
|
3091
|
-
input
|
|
3092
|
-
);
|
|
3093
|
-
await processor.processTask(task);
|
|
3094
|
-
}
|
|
3101
|
+
await sessionManager.submitTask(input);
|
|
3095
3102
|
} catch (err) {
|
|
3096
3103
|
log.error(`${err instanceof Error ? err.message : err}`);
|
|
3097
3104
|
}
|