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 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
- const task = await createTask(
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(`Failed to create scheduled task: ${err}`);
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/tools/filesystem.ts
815
- import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
816
- import { resolve, relative, join } from "path";
817
- import { glob } from "glob";
818
- function assertWithinWorkspace(filePath) {
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
- return resolved;
827
- }
828
- async function readFileContent(path, offset, limit) {
829
- const resolved = assertWithinWorkspace(path);
830
- const content = await readFile(resolved, "utf-8");
831
- const lines = content.split("\n");
832
- const start = offset || 0;
833
- const end = limit ? start + limit : lines.length;
834
- const selected = lines.slice(start, end);
835
- return selected.map((line, i) => `${String(start + i + 1).padStart(5)} | ${line}`).join("\n");
836
- }
837
- async function writeFileContent(path, content) {
838
- const resolved = assertWithinWorkspace(path);
839
- const dir = resolve(resolved, "..");
840
- await mkdir(dir, { recursive: true });
841
- await writeFile(resolved, content, "utf-8");
842
- return `File written: ${path} (${content.length} bytes)`;
843
- }
844
- async function searchFiles(pattern, directory) {
845
- const config = getConfig();
846
- const cwd = directory ? assertWithinWorkspace(directory) : config.workspacePath;
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
- return results.join("\n") || "Empty directory.";
869
- }
870
- function formatSize(bytes) {
871
- if (bytes < 1024) return `${bytes}B`;
872
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
873
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
874
- }
875
- async function searchContent(pattern, fileGlob, directory) {
876
- const config = getConfig();
877
- const cwd = directory ? assertWithinWorkspace(directory) : config.workspacePath;
878
- const files = await glob(fileGlob || "**/*", {
879
- cwd,
880
- nodir: true,
881
- ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
882
- });
883
- const regex = new RegExp(pattern, "gi");
884
- const results = [];
885
- for (const file of files.slice(0, 200)) {
886
- try {
887
- const content = await readFile(join(cwd, file), "utf-8");
888
- const lines = content.split("\n");
889
- for (let i = 0; i < lines.length; i++) {
890
- if (regex.test(lines[i])) {
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
- return results.length > 0 ? results.join("\n") : "No matches found.";
902
- }
903
-
904
- // src/tools/shell.ts
905
- import { exec } from "child_process";
906
- var TIMEOUT_MS = 3e4;
907
- var MAX_OUTPUT = 5e4;
908
- var BLOCKED_PATTERNS = [
909
- /rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i,
910
- // rm -rf /, rm -fr /, etc.
911
- /rm\s+(-\w*\s+)*-\w*r\w*\s+~($|\s|\/)/i,
912
- // rm -rf ~, rm -fr ~/, etc.
913
- /\bmkfs\b/i,
914
- // mkfs (any form)
915
- /\bdd\s+.*\bif=/i,
916
- // dd if=
917
- /:\s*\(\s*\)\s*\{/,
918
- // fork bomb :(){}
919
- /\bchmod\s+(-\w+\s+)*-R\s+777\s+\//i,
920
- // chmod -R 777 /
921
- />\s*\/dev\/sd[a-z]/i,
922
- // write to raw disk
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 as join2, basename, dirname } from "path";
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 = join2(homedir(), ".config", "assistme", "skills");
1556
- var BUNDLED_SKILLS_DIR = join2(
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 = join2(dir, entry);
1184
+ const fullPath = join(dir, entry);
1643
1185
  const stat2 = statSync(fullPath);
1644
1186
  if (stat2.isDirectory()) {
1645
- const skillMd = join2(fullPath, "SKILL.md");
1646
- const skillMdLower = join2(fullPath, "skill.md");
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 = join2(SKILLS_DIR, name);
1297
+ const skillDir = join(SKILLS_DIR, name);
1756
1298
  mkdirSync(skillDir, { recursive: true });
1757
- const filePath = join2(skillDir, "SKILL.md");
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 = join2(SKILLS_DIR, name);
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 = join2(targetDir, "SKILL.md");
1813
- const skillMdLower = join2(targetDir, "skill.md");
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 = join2(SKILLS_DIR, name);
1381
+ const skillDir = join(SKILLS_DIR, name);
1840
1382
  mkdirSync(skillDir, { recursive: true });
1841
- const zipPath = join2(skillDir, "_download.zip");
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
- join2(skillDir, "SKILL.md"),
1402
+ join(skillDir, "SKILL.md"),
1861
1403
  await fileResp.text(),
1862
1404
  "utf-8"
1863
1405
  );
1864
1406
  }
1865
- const skillMd = join2(skillDir, "SKILL.md");
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/rate-limiter.ts
2260
- var RateLimiter = class {
2261
- tokens;
2262
- maxTokens;
2263
- refillRate;
2264
- // tokens per second
2265
- lastRefill;
2266
- maxWaitMs;
2267
- constructor(opts) {
2268
- this.maxTokens = opts.maxTokens;
2269
- this.refillRate = opts.refillRate;
2270
- this.tokens = opts.maxTokens;
2271
- this.lastRefill = Date.now();
2272
- this.maxWaitMs = opts.maxWaitMs ?? 1e4;
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
- refill() {
2275
- const now = Date.now();
2276
- const elapsed = (now - this.lastRefill) / 1e3;
2277
- this.tokens = Math.min(
2278
- this.maxTokens,
2279
- this.tokens + elapsed * this.refillRate
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_")) return toolRateLimiters.browser;
2335
- if (toolName === "execute_command") return toolRateLimiters.shell;
2336
- if (["read_file", "write_file", "search_files", "search_content", "list_directory"].includes(
2337
- toolName
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/utils/retry.ts
2344
- async function withRetry(fn, opts = {}) {
2345
- const {
2346
- maxRetries = 3,
2347
- baseDelayMs = 500,
2348
- maxDelayMs = 1e4,
2349
- backoffFactor = 2,
2350
- retryIf,
2351
- label
2352
- } = opts;
2353
- let lastError;
2354
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
2355
- try {
2356
- return await fn();
2357
- } catch (err) {
2358
- lastError = err;
2359
- if (retryIf && !retryIf(err)) {
2360
- throw err;
2361
- }
2362
- if (attempt < maxRetries) {
2363
- const delay = Math.min(
2364
- baseDelayMs * Math.pow(backoffFactor, attempt),
2365
- maxDelayMs
2366
- );
2367
- if (label) {
2368
- log.debug(`${label}: attempt ${attempt + 1} failed, retrying in ${delay}ms`);
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
- await new Promise((resolve2) => setTimeout(resolve2, delay));
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
- throw lastError;
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
- - Navigate to any website (user's sessions are preserved)
2389
- - Read page content, take screenshots
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, write, search files in the workspace
2396
- - Execute shell commands
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
- const correlationId = newCorrelationId();
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 toolDefs = [
2474
- ...getToolDefinitions(),
2475
- {
2476
- name: "memory_store",
2477
- description: "Store a memory about the user that persists across conversations. Use this when you learn something important about the user \u2014 their preferences, habits, work context, or standing instructions. Categories: general, preference, instruction, context, skill_learned, fact.",
2478
- input_schema: {
2479
- type: "object",
2480
- properties: {
2481
- content: {
2482
- type: "string",
2483
- description: "What to remember (concise, factual statement)"
2484
- },
2485
- category: {
2486
- type: "string",
2487
- description: "Category: general, preference, instruction, context, skill_learned, fact"
2488
- },
2489
- importance: {
2490
- type: "number",
2491
- description: "Importance 1-10 (default: 5). Use 8+ for instructions, 7 for preferences"
2492
- },
2493
- tags: {
2494
- type: "array",
2495
- items: { type: "string" },
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
- for await (const message of query({
2589
- prompt: task.prompt,
2590
- options,
2591
- // Custom tool executor - the SDK calls this for our tools
2592
- onToolCall: async (toolName, toolInput) => {
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
- throw new Error(
2595
- `Task timed out after ${Math.round(taskTimeoutMs / 6e4)} minutes`
2596
- );
2676
+ finalResponse += "\n\n[Task timed out]";
2677
+ break;
2597
2678
  }
2598
- log.tool(toolName, JSON.stringify(toolInput).slice(0, 200));
2599
- await emitEvent(task.id, "tool_use_start", { name: toolName });
2600
- await emitEvent(task.id, "tool_use_input", { input: toolInput });
2601
- let result;
2602
- try {
2603
- const limiter = getLimiterForTool(toolName);
2604
- if (limiter) {
2605
- await limiter.acquire();
2606
- }
2607
- if (toolName === "skill_create") {
2608
- const name = toolInput.name;
2609
- const description = toolInput.description;
2610
- const instructions = toolInput.instructions;
2611
- const emoji = toolInput.emoji || "";
2612
- const existing = this.skillManager.findSimilar(name);
2613
- if (existing) {
2614
- result = `A similar skill "${existing.name}" already exists. Use skill_improve to update it instead.`;
2615
- } else {
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
- } else if (toolName === "skill_improve") {
2641
- const name = toolInput.name;
2642
- const improvedInstructions = toolInput.improved_instructions;
2643
- const description = toolInput.description;
2644
- const existing = this.skillManager.get(name);
2645
- if (!existing) {
2646
- result = `Skill "${name}" not found. Available skills: ${this.skillManager.getAll().map((s) => s.name).join(", ")}`;
2647
- } else if (existing.source === "bundled") {
2648
- const overrideName = name;
2649
- this.skillManager.create(overrideName, description || existing.description, improvedInstructions);
2650
- result = `Bundled skill "${name}" overridden with improved version (user copy created).`;
2651
- log.success(`Self-improvement: overrode bundled skill "${name}"`);
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 updated = this.skillManager.update(name, improvedInstructions, description);
2654
- if (updated) {
2655
- result = `Skill "${name}" improved and version bumped. Changes will apply to future tasks.`;
2656
- log.success(`Self-improvement: improved skill "${name}"`);
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
- } else if (toolName === "memory_store" && this.memoryManager) {
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
- log.result(result.slice(0, 200));
2676
- } catch (err) {
2677
- result = `Error: ${err instanceof Error ? err.message : String(err)}`;
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(() => completeTask(task.id, finalResponse), {
2720
- maxRetries: 2,
2721
- baseDelayMs: 300,
2722
- label: "completeTask"
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 taskId = task.id;
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: taskId
2751
+ sourceMessageId: taskIdRef
2736
2752
  });
2737
- log.info(`Memory extracted: [${mem.category}] ${mem.content.slice(0, 60)}...`);
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${metaJson ? `
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(skillName, skill.description, improvement.improved_steps);
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
- const { createTask: createTask2 } = await import("./supabase-QU7MFNDI.js");
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
  }