assistme 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,35 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  JobRunner,
4
- claimTask,
5
- clearConfig,
6
- completeTask,
7
- createSession,
8
- createTask,
9
- emitEvent,
10
- endSession,
11
- failTask,
12
- getConfig,
13
- getConfigPath,
14
- getConversationHistory,
15
- getCurrentUserId,
16
- getOrCreateCliConversation,
17
- getSupabase,
4
+ callMcpHandler,
18
5
  log,
19
- loginWithToken,
20
- logout,
21
6
  newCorrelationId,
22
- pollActionResponse,
23
- pollAndClaimJobRun,
24
- pollAndClaimTask,
25
- resetEventSequence,
26
- setActionRequest,
27
- setConfig,
28
7
  setCorrelationId,
29
- setLogLevel,
30
- setSessionBusy,
31
- updateHeartbeat
32
- } from "./chunk-XY3LGAOY.js";
8
+ setLogLevel
9
+ } from "./chunk-UWE5WVQI.js";
10
+ import {
11
+ clearConfig,
12
+ getConfig,
13
+ getConfigPath,
14
+ setConfig
15
+ } from "./chunk-TTEGHE2E.js";
33
16
 
34
17
  // src/index.ts
35
18
  import { Command } from "commander";
@@ -40,6 +23,217 @@ import { createRequire } from "module";
40
23
  import chalk from "chalk";
41
24
  import ora from "ora";
42
25
  import { createInterface } from "readline";
26
+
27
+ // src/db/supabase.ts
28
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
29
+ import { join } from "path";
30
+ import { homedir } from "os";
31
+ var AUTH_DIR = join(homedir(), ".config", "assistme");
32
+ var AUTH_FILE = join(AUTH_DIR, "auth.json");
33
+ function ensureAuthDir() {
34
+ if (!existsSync(AUTH_DIR)) {
35
+ mkdirSync(AUTH_DIR, { recursive: true, mode: 448 });
36
+ }
37
+ }
38
+ function readAuthStore() {
39
+ try {
40
+ if (existsSync(AUTH_FILE)) {
41
+ return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
42
+ }
43
+ } catch {
44
+ }
45
+ return {};
46
+ }
47
+ function writeAuthStore(data) {
48
+ ensureAuthDir();
49
+ writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
50
+ }
51
+ async function loginWithToken(mcpToken) {
52
+ if (!mcpToken.startsWith("am_")) {
53
+ throw new Error(
54
+ "Invalid token format. Use an am_ token from the web page."
55
+ );
56
+ }
57
+ const result = await callMcpHandler(
58
+ "auth.validate_token",
59
+ {},
60
+ mcpToken
61
+ );
62
+ const store = readAuthStore();
63
+ store["mcp_token"] = mcpToken;
64
+ writeAuthStore(store);
65
+ return result.user_id;
66
+ }
67
+ async function getCurrentUserId() {
68
+ const result = await callMcpHandler(
69
+ "auth.validate_token"
70
+ );
71
+ return result.user_id;
72
+ }
73
+ async function logout() {
74
+ try {
75
+ writeAuthStore({});
76
+ } catch {
77
+ }
78
+ }
79
+ async function createSession(_userId, sessionName, workspacePath, version2) {
80
+ const { getConfig: getConfig2 } = await import("./config-PUIS2TQL.js");
81
+ const data = await callMcpHandler("session.create", {
82
+ session_name: sessionName,
83
+ workspace_path: workspacePath,
84
+ version: version2,
85
+ model: getConfig2().model || null
86
+ });
87
+ return data;
88
+ }
89
+ async function updateHeartbeat(sessionId) {
90
+ try {
91
+ await callMcpHandler("session.heartbeat", { session_id: sessionId });
92
+ } catch (err) {
93
+ log.warn(`Heartbeat update failed: ${err instanceof Error ? err.message : err}`);
94
+ }
95
+ }
96
+ async function endSession(sessionId) {
97
+ try {
98
+ await callMcpHandler("session.end", { session_id: sessionId });
99
+ } catch (err) {
100
+ log.error(`Failed to end session: ${err instanceof Error ? err.message : err}`);
101
+ }
102
+ }
103
+ async function setSessionBusy(sessionId, busy) {
104
+ await callMcpHandler("session.set_busy", {
105
+ session_id: sessionId,
106
+ busy
107
+ });
108
+ }
109
+ async function cleanupStaleSessions(currentSessionId, thresholdMs = 12e4) {
110
+ try {
111
+ const result = await callMcpHandler(
112
+ "session.cleanup_stale",
113
+ { current_session_id: currentSessionId, threshold_ms: thresholdMs }
114
+ );
115
+ return result.cleaned;
116
+ } catch {
117
+ return 0;
118
+ }
119
+ }
120
+ async function getActiveSessions(limit = 5) {
121
+ return callMcpHandler("session.get_active", { limit });
122
+ }
123
+ async function getOrCreateCliConversation(_userId, _sessionId) {
124
+ const data = await callMcpHandler("conversation.get_or_create");
125
+ return data;
126
+ }
127
+ async function createTask(conversationId, _userId, sessionId, prompt) {
128
+ const data = await callMcpHandler("task.create", {
129
+ conversation_id: conversationId,
130
+ session_id: sessionId,
131
+ prompt
132
+ });
133
+ return { ...data, prompt };
134
+ }
135
+ async function pollAndClaimTask(sessionId) {
136
+ try {
137
+ const data = await callMcpHandler(
138
+ "task.poll_and_claim",
139
+ { session_id: sessionId }
140
+ );
141
+ if (!data) return null;
142
+ return {
143
+ ...data,
144
+ prompt: data.metadata?.prompt || data.content || ""
145
+ };
146
+ } catch (err) {
147
+ log.warn(`Poll-and-claim failed: ${err instanceof Error ? err.message : err}`);
148
+ return null;
149
+ }
150
+ }
151
+ async function claimTask(messageId) {
152
+ const data = await callMcpHandler("task.claim", {
153
+ message_id: messageId
154
+ });
155
+ return data;
156
+ }
157
+ async function completeTask(messageId, resultSummary, tokenUsage) {
158
+ await callMcpHandler("task.complete", {
159
+ message_id: messageId,
160
+ result: resultSummary,
161
+ token_usage: tokenUsage || null
162
+ });
163
+ }
164
+ async function failTask(messageId, errorMessage) {
165
+ try {
166
+ await callMcpHandler("task.fail", {
167
+ message_id: messageId,
168
+ error: errorMessage
169
+ });
170
+ } catch (err) {
171
+ log.error(`Failed to update task status: ${err instanceof Error ? err.message : err}`);
172
+ }
173
+ }
174
+ async function pollAndClaimJobRun(_userId) {
175
+ try {
176
+ const data = await callMcpHandler(
177
+ "job.claim_pending_run"
178
+ );
179
+ return data;
180
+ } catch (err) {
181
+ log.debug(`Job run poll failed: ${err instanceof Error ? err.message : err}`);
182
+ return null;
183
+ }
184
+ }
185
+ async function getConversationHistory(conversationId, excludeMessageId, limit = 20) {
186
+ try {
187
+ const rows = await callMcpHandler(
188
+ "conversation.get_history",
189
+ {
190
+ conversation_id: conversationId,
191
+ exclude_message_id: excludeMessageId,
192
+ limit
193
+ }
194
+ );
195
+ return (rows || []).reverse().map((row) => {
196
+ const prompt = row.metadata?.prompt || "";
197
+ const content = row.content || "";
198
+ const response = row.status === "failed" ? `[Task failed] ${content}` : content;
199
+ return { prompt, response };
200
+ }).filter((entry) => entry.prompt && entry.response);
201
+ } catch (err) {
202
+ log.debug(`Failed to fetch conversation history: ${err instanceof Error ? err.message : err}`);
203
+ return [];
204
+ }
205
+ }
206
+ var eventSequence = 0;
207
+ function resetEventSequence() {
208
+ eventSequence = 0;
209
+ }
210
+ async function emitEvent(messageId, eventType, eventData) {
211
+ eventSequence++;
212
+ try {
213
+ await callMcpHandler("event.emit", {
214
+ message_id: messageId,
215
+ event_type: eventType,
216
+ event_data: eventData,
217
+ seq: eventSequence
218
+ });
219
+ } catch (err) {
220
+ log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
221
+ }
222
+ }
223
+ async function setActionRequest(messageId, actionData) {
224
+ await callMcpHandler("action.set_request", {
225
+ message_id: messageId,
226
+ action_data: actionData
227
+ });
228
+ }
229
+ async function pollActionResponse(messageId) {
230
+ return callMcpHandler(
231
+ "action.poll_response",
232
+ { message_id: messageId }
233
+ );
234
+ }
235
+
236
+ // src/commands/auth.ts
43
237
  function registerAuthCommands(program2) {
44
238
  program2.command("login").description("Authenticate with your AssistMe account via browser token").action(async () => {
45
239
  const tokenPageUrl = process.env.ASSISTME_WEB_URL || "https://assistme.co.nz/agent/token";
@@ -164,9 +358,9 @@ import ora2 from "ora";
164
358
  // src/tools/browser.ts
165
359
  import { WebSocket } from "ws";
166
360
  import { execSync, spawn } from "child_process";
167
- import { platform, homedir } from "os";
168
- import { existsSync, unlinkSync, mkdirSync, cpSync } from "fs";
169
- import { join } from "path";
361
+ import { platform, homedir as homedir2 } from "os";
362
+ import { existsSync as existsSync2, unlinkSync, mkdirSync as mkdirSync2, cpSync } from "fs";
363
+ import { join as join2 } from "path";
170
364
  var BrowserController = class {
171
365
  ws = null;
172
366
  debugPort;
@@ -701,7 +895,7 @@ function findChromePath() {
701
895
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
702
896
  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
703
897
  ];
704
- return paths.find((p) => existsSync(p)) ?? null;
898
+ return paths.find((p) => existsSync2(p)) ?? null;
705
899
  }
706
900
  if (os === "linux") {
707
901
  const names = [
@@ -738,7 +932,7 @@ function findChromePath() {
738
932
  for (const prefix of prefixes) {
739
933
  for (const sub of subPaths) {
740
934
  const p = `${prefix}\\${sub}`;
741
- if (existsSync(p)) return p;
935
+ if (existsSync2(p)) return p;
742
936
  }
743
937
  }
744
938
  return null;
@@ -746,39 +940,39 @@ function findChromePath() {
746
940
  return null;
747
941
  }
748
942
  function getDefaultProfileDir(chromePath) {
749
- const home = homedir();
943
+ const home = homedir2();
750
944
  const os = platform();
751
945
  if (os === "darwin") {
752
946
  if (chromePath.includes("Brave Browser"))
753
- return join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
947
+ return join2(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
754
948
  if (chromePath.includes("Microsoft Edge"))
755
- return join(home, "Library", "Application Support", "Microsoft Edge");
949
+ return join2(home, "Library", "Application Support", "Microsoft Edge");
756
950
  if (chromePath.includes("Chromium"))
757
- return join(home, "Library", "Application Support", "Chromium");
951
+ return join2(home, "Library", "Application Support", "Chromium");
758
952
  if (chromePath.includes("Canary"))
759
- return join(home, "Library", "Application Support", "Google", "Chrome Canary");
760
- return join(home, "Library", "Application Support", "Google", "Chrome");
953
+ return join2(home, "Library", "Application Support", "Google", "Chrome Canary");
954
+ return join2(home, "Library", "Application Support", "Google", "Chrome");
761
955
  }
762
956
  if (os === "win32") {
763
- const appData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
957
+ const appData = process.env.LOCALAPPDATA || join2(home, "AppData", "Local");
764
958
  if (chromePath.includes("brave"))
765
- return join(appData, "BraveSoftware", "Brave-Browser", "User Data");
766
- if (chromePath.includes("msedge")) return join(appData, "Microsoft", "Edge", "User Data");
767
- return join(appData, "Google", "Chrome", "User Data");
768
- }
769
- if (chromePath.includes("brave")) return join(home, ".config", "BraveSoftware", "Brave-Browser");
770
- if (chromePath.includes("microsoft-edge")) return join(home, ".config", "microsoft-edge");
771
- if (chromePath.includes("chromium")) return join(home, ".config", "chromium");
772
- return join(home, ".config", "google-chrome");
959
+ return join2(appData, "BraveSoftware", "Brave-Browser", "User Data");
960
+ if (chromePath.includes("msedge")) return join2(appData, "Microsoft", "Edge", "User Data");
961
+ return join2(appData, "Google", "Chrome", "User Data");
962
+ }
963
+ if (chromePath.includes("brave")) return join2(home, ".config", "BraveSoftware", "Brave-Browser");
964
+ if (chromePath.includes("microsoft-edge")) return join2(home, ".config", "microsoft-edge");
965
+ if (chromePath.includes("chromium")) return join2(home, ".config", "chromium");
966
+ return join2(home, ".config", "google-chrome");
773
967
  }
774
968
  function getDebugProfileDir(chromePath) {
775
- const home = homedir();
776
- const debugDir = join(home, ".assistme", "browser-profile");
777
- if (!existsSync(debugDir)) {
778
- mkdirSync(debugDir, { recursive: true });
969
+ const home = homedir2();
970
+ const debugDir = join2(home, ".assistme", "browser-profile");
971
+ if (!existsSync2(debugDir)) {
972
+ mkdirSync2(debugDir, { recursive: true });
779
973
  log.debug(`Created debug profile directory: ${debugDir}`);
780
974
  const realDir = getDefaultProfileDir(chromePath);
781
- if (existsSync(realDir)) {
975
+ if (existsSync2(realDir)) {
782
976
  seedDebugProfile(realDir, debugDir);
783
977
  }
784
978
  }
@@ -788,35 +982,35 @@ function seedDebugProfile(realDir, debugDir) {
788
982
  const rootFiles = ["Local State"];
789
983
  const profileFiles = ["Bookmarks", "Preferences", "Favicons", "Top Sites", "Shortcuts"];
790
984
  for (const file of rootFiles) {
791
- const src = join(realDir, file);
792
- const dest = join(debugDir, file);
985
+ const src = join2(realDir, file);
986
+ const dest = join2(debugDir, file);
793
987
  try {
794
- if (existsSync(src)) {
988
+ if (existsSync2(src)) {
795
989
  cpSync(src, dest, { force: true });
796
990
  log.debug(`Seeded: ${file}`);
797
991
  }
798
992
  } catch {
799
993
  }
800
994
  }
801
- const srcProfile = join(realDir, "Default");
802
- const destProfile = join(debugDir, "Default");
803
- if (existsSync(srcProfile)) {
804
- mkdirSync(destProfile, { recursive: true });
995
+ const srcProfile = join2(realDir, "Default");
996
+ const destProfile = join2(debugDir, "Default");
997
+ if (existsSync2(srcProfile)) {
998
+ mkdirSync2(destProfile, { recursive: true });
805
999
  for (const file of profileFiles) {
806
- const src = join(srcProfile, file);
807
- const dest = join(destProfile, file);
1000
+ const src = join2(srcProfile, file);
1001
+ const dest = join2(destProfile, file);
808
1002
  try {
809
- if (existsSync(src)) {
1003
+ if (existsSync2(src)) {
810
1004
  cpSync(src, dest, { force: true });
811
1005
  log.debug(`Seeded: Default/${file}`);
812
1006
  }
813
1007
  } catch {
814
1008
  }
815
1009
  }
816
- const srcExt = join(srcProfile, "Extensions");
817
- const destExt = join(destProfile, "Extensions");
1010
+ const srcExt = join2(srcProfile, "Extensions");
1011
+ const destExt = join2(destProfile, "Extensions");
818
1012
  try {
819
- if (existsSync(srcExt)) {
1013
+ if (existsSync2(srcExt)) {
820
1014
  cpSync(srcExt, destExt, { recursive: true, force: true });
821
1015
  log.debug("Seeded: Default/Extensions");
822
1016
  }
@@ -907,14 +1101,14 @@ async function ensureBrowserAvailable(port = 9222) {
907
1101
  return { success: true, action: "launched", chromePath };
908
1102
  }
909
1103
  const debugDir = getDebugProfileDir(chromePath);
910
- const lockPath = join(debugDir, "SingletonLock");
911
- if (existsSync(lockPath)) {
1104
+ const lockPath = join2(debugDir, "SingletonLock");
1105
+ if (existsSync2(lockPath)) {
912
1106
  log.debug("Found stale SingletonLock in debug profile \u2014 removing and retrying");
913
1107
  try {
914
1108
  unlinkSync(lockPath);
915
1109
  for (const f of ["SingletonSocket", "SingletonCookie"]) {
916
1110
  try {
917
- unlinkSync(join(debugDir, f));
1111
+ unlinkSync(join2(debugDir, f));
918
1112
  } catch {
919
1113
  }
920
1114
  }
@@ -1124,13 +1318,14 @@ var Scheduler = class {
1124
1318
  }
1125
1319
  async initializeNextRuns() {
1126
1320
  try {
1127
- const userId = await getCurrentUserId();
1128
- const sb = getSupabase();
1129
- const { data } = await sb.from("agent_scheduled_tasks").select("*").eq("user_id", userId).eq("enabled", true).is("next_run_at", null);
1321
+ const data = await callMcpHandler("schedule.get_uninitialized");
1130
1322
  if (data) {
1131
1323
  for (const task of data) {
1132
1324
  const nextRun = getNextRunTime(task.cron_expression, task.timezone);
1133
- await sb.from("agent_scheduled_tasks").update({ next_run_at: nextRun.toISOString() }).eq("id", task.id);
1325
+ await callMcpHandler("schedule.update", {
1326
+ task_id: task.id,
1327
+ next_run_at: nextRun.toISOString()
1328
+ });
1134
1329
  }
1135
1330
  }
1136
1331
  } catch (err) {
@@ -1140,24 +1335,29 @@ var Scheduler = class {
1140
1335
  async checkDueTasks() {
1141
1336
  if (!this.running || !this.onScheduledTask) return;
1142
1337
  try {
1143
- const userId = await getCurrentUserId();
1144
- const sb = getSupabase();
1145
- const { data: dueTasks } = await sb.from("agent_scheduled_tasks").select("*").eq("user_id", userId).eq("enabled", true).lte("next_run_at", (/* @__PURE__ */ new Date()).toISOString()).order("next_run_at", { ascending: true }).limit(1);
1338
+ const dueTasks = await callMcpHandler("schedule.check_due");
1146
1339
  if (!dueTasks || dueTasks.length === 0) return;
1147
1340
  const task = dueTasks[0];
1148
1341
  log.info(`Scheduled task due: "${task.name}"`);
1149
1342
  const nextRun = getNextRunTime(task.cron_expression, task.timezone);
1150
- await sb.from("agent_scheduled_tasks").update({
1343
+ await callMcpHandler("schedule.update", {
1344
+ task_id: task.id,
1151
1345
  last_run_at: (/* @__PURE__ */ new Date()).toISOString(),
1152
1346
  next_run_at: nextRun.toISOString(),
1153
1347
  run_count: task.run_count + 1
1154
- }).eq("id", task.id);
1348
+ });
1155
1349
  try {
1156
1350
  await this.onScheduledTask(task);
1157
- await sb.from("agent_scheduled_tasks").update({ last_error: null }).eq("id", task.id);
1351
+ await callMcpHandler("schedule.update", {
1352
+ task_id: task.id,
1353
+ last_error: null
1354
+ });
1158
1355
  } catch (err) {
1159
1356
  const errMsg = err instanceof Error ? err.message : String(err);
1160
- await sb.from("agent_scheduled_tasks").update({ last_error: errMsg }).eq("id", task.id);
1357
+ await callMcpHandler("schedule.update", {
1358
+ task_id: task.id,
1359
+ last_error: errMsg
1360
+ });
1161
1361
  log.error(`Scheduled task "${task.name}" failed: ${errMsg}`);
1162
1362
  }
1163
1363
  } catch (err) {
@@ -1165,50 +1365,41 @@ var Scheduler = class {
1165
1365
  }
1166
1366
  }
1167
1367
  };
1168
- async function createScheduledTask(userId, name, prompt, cronExpression, timezone = "UTC") {
1169
- const sb = getSupabase();
1368
+ async function createScheduledTask(_userId, name, prompt, cronExpression, timezone = "UTC") {
1170
1369
  const nextRun = getNextRunTime(cronExpression, timezone);
1171
- const { data, error } = await sb.from("agent_scheduled_tasks").insert({
1172
- user_id: userId,
1370
+ return callMcpHandler("schedule.create", {
1173
1371
  name,
1174
1372
  prompt,
1175
1373
  cron_expression: cronExpression,
1176
1374
  timezone,
1177
1375
  next_run_at: nextRun.toISOString()
1178
- }).select().single();
1179
- if (error) throw new Error(`Failed to create schedule: ${error.message}`);
1180
- return data;
1376
+ });
1181
1377
  }
1182
- async function listScheduledTasks(userId) {
1183
- const sb = getSupabase();
1184
- const { data, error } = await sb.from("agent_scheduled_tasks").select("*").eq("user_id", userId).order("created_at", { ascending: false });
1185
- if (error) throw new Error(`Failed to list schedules: ${error.message}`);
1186
- return data || [];
1378
+ async function listScheduledTasks(_userId) {
1379
+ return callMcpHandler("schedule.list");
1187
1380
  }
1188
1381
  async function toggleScheduledTask(taskId, enabled) {
1189
- const sb = getSupabase();
1190
- const update = { enabled };
1382
+ const params = { task_id: taskId, enabled };
1191
1383
  if (enabled) {
1192
- const { data } = await sb.from("agent_scheduled_tasks").select("cron_expression, timezone").eq("id", taskId).single();
1193
- if (data) {
1194
- const nextRun = getNextRunTime(data.cron_expression, data.timezone);
1195
- update.next_run_at = nextRun.toISOString();
1384
+ const taskData = await callMcpHandler(
1385
+ "schedule.get_task",
1386
+ { task_id: taskId }
1387
+ );
1388
+ if (taskData) {
1389
+ const nextRun = getNextRunTime(taskData.cron_expression, taskData.timezone);
1390
+ params.next_run_at = nextRun.toISOString();
1196
1391
  }
1197
1392
  }
1198
- const { error } = await sb.from("agent_scheduled_tasks").update(update).eq("id", taskId);
1199
- if (error) throw new Error(`Failed to toggle schedule: ${error.message}`);
1393
+ await callMcpHandler("schedule.toggle", params);
1200
1394
  }
1201
1395
  async function deleteScheduledTask(taskId) {
1202
- const sb = getSupabase();
1203
- const { error } = await sb.from("agent_scheduled_tasks").delete().eq("id", taskId);
1204
- if (error) throw new Error(`Failed to delete schedule: ${error.message}`);
1396
+ await callMcpHandler("schedule.delete", { task_id: taskId });
1205
1397
  }
1206
1398
 
1207
1399
  // src/agent/session.ts
1208
1400
  var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
1209
1401
  var DEFAULT_POLL_INTERVAL = 2e3;
1210
1402
  var MAX_POLL_INTERVAL = 3e4;
1211
- var STALE_SESSION_THRESHOLD = 12e4;
1212
1403
  var SessionManager = class {
1213
1404
  session = null;
1214
1405
  heartbeatTimer = null;
@@ -1261,9 +1452,6 @@ var SessionManager = class {
1261
1452
  });
1262
1453
  return this.session;
1263
1454
  }
1264
- /**
1265
- * Schedule the next poll with exponential backoff on failures.
1266
- */
1267
1455
  schedulePoll() {
1268
1456
  if (!this.running) return;
1269
1457
  const delay = this.consecutivePollFailures === 0 ? this.basePollInterval : Math.min(
@@ -1283,10 +1471,6 @@ var SessionManager = class {
1283
1471
  log.error(`Scheduled task error: ${err}`);
1284
1472
  }
1285
1473
  }
1286
- /**
1287
- * Execute a pending job run triggered from the web UI.
1288
- * Loads the job, builds the agentic prompt, and processes it as a chat task.
1289
- */
1290
1474
  async executeJobRun(jobRun) {
1291
1475
  if (!this.session || !this.userId || !this.conversationId || !this.onTask)
1292
1476
  return;
@@ -1312,8 +1496,10 @@ var SessionManager = class {
1312
1496
  return;
1313
1497
  }
1314
1498
  try {
1315
- const sb = getSupabase();
1316
- await sb.from("agent_job_runs").update({ session_id: this.session.id }).eq("id", jobRun.id);
1499
+ await callMcpHandler("job.link_run_session", {
1500
+ run_id: jobRun.id,
1501
+ session_id: this.session.id
1502
+ });
1317
1503
  } catch (linkErr) {
1318
1504
  log.debug(`Failed to link session to job run: ${linkErr}`);
1319
1505
  }
@@ -1389,37 +1575,17 @@ var SessionManager = class {
1389
1575
  }
1390
1576
  this.schedulePoll();
1391
1577
  }
1392
- /**
1393
- * Mark sessions as offline if they haven't sent a heartbeat recently.
1394
- * Runs once on startup to clean up orphaned sessions from crashed processes.
1395
- */
1396
1578
  async cleanupStaleSessions() {
1397
- if (!this.userId) return;
1579
+ if (!this.userId || !this.session) return;
1398
1580
  try {
1399
- const sb = getSupabase();
1400
- const threshold = new Date(
1401
- Date.now() - STALE_SESSION_THRESHOLD
1402
- ).toISOString();
1403
- const { data: stale } = await sb.from("agent_sessions").select("id").eq("user_id", this.userId).in("status", ["online", "busy"]).lt("last_heartbeat_at", threshold).neq("id", this.session?.id || "");
1404
- if (stale && stale.length > 0) {
1405
- for (const s of stale) {
1406
- await sb.from("agent_sessions").update({
1407
- status: "offline",
1408
- ended_at: (/* @__PURE__ */ new Date()).toISOString(),
1409
- metadata: { ended_reason: "stale_session_cleanup" }
1410
- }).eq("id", s.id);
1411
- }
1412
- log.info(`Cleaned up ${stale.length} stale session(s)`);
1581
+ const cleaned = await cleanupStaleSessions(this.session.id);
1582
+ if (cleaned > 0) {
1583
+ log.info(`Cleaned up ${cleaned} stale session(s)`);
1413
1584
  }
1414
1585
  } catch (err) {
1415
1586
  log.debug(`Stale session cleanup error: ${err}`);
1416
1587
  }
1417
1588
  }
1418
- /**
1419
- * Submit a task from the interactive prompt or scheduled job.
1420
- * Sets processing=true BEFORE creating the task so the poll loop
1421
- * never races to pick it up.
1422
- */
1423
1589
  async submitTask(prompt) {
1424
1590
  if (!this.session || !this.userId || !this.conversationId || !this.onTask) {
1425
1591
  throw new Error("Session not started");
@@ -1445,9 +1611,6 @@ var SessionManager = class {
1445
1611
  }
1446
1612
  }
1447
1613
  }
1448
- /**
1449
- * Stop the session with a safety timeout to prevent hanging on shutdown.
1450
- */
1451
1614
  async stop(timeoutMs = 5e3) {
1452
1615
  this.running = false;
1453
1616
  this.scheduler.stop();
@@ -1488,62 +1651,45 @@ var SessionManager = class {
1488
1651
 
1489
1652
  // src/agent/processor.ts
1490
1653
  import {
1491
- query
1654
+ query as query2
1492
1655
  } from "@anthropic-ai/claude-agent-sdk";
1493
1656
 
1494
1657
  // src/agent/memory.ts
1495
1658
  var MemoryManager = class {
1496
- userId;
1497
- constructor(userId) {
1498
- this.userId = userId;
1659
+ constructor(_userId) {
1499
1660
  }
1500
1661
  /**
1501
1662
  * Store a new memory. Called by the agent after completing tasks
1502
1663
  * to remember important facts about the user.
1503
1664
  */
1504
1665
  async remember(content, category = "general", options) {
1505
- const sb = getSupabase();
1506
1666
  const expiresAt = options?.expiresInDays ? new Date(
1507
1667
  Date.now() + options.expiresInDays * 864e5
1508
1668
  ).toISOString() : null;
1509
- const { data, error } = await sb.from("agent_memories").insert({
1510
- user_id: this.userId,
1669
+ const data = await callMcpHandler("memory.store", {
1511
1670
  category,
1512
1671
  content,
1513
1672
  importance: options?.importance ?? 5,
1514
1673
  tags: options?.tags ?? [],
1515
1674
  source_message_id: options?.sourceMessageId ?? null,
1516
1675
  expires_at: expiresAt
1517
- }).select().single();
1518
- if (error) throw new Error(`Failed to store memory: ${error.message}`);
1676
+ });
1519
1677
  log.debug(`Memory stored: [${category}] ${content.slice(0, 80)}...`);
1520
1678
  return data;
1521
1679
  }
1522
1680
  /**
1523
1681
  * Search memories by query text. Uses ILIKE + tag containment.
1524
1682
  */
1525
- async search(query2, limit = 10) {
1526
- const sb = getSupabase();
1527
- const sanitized = query2.replace(/[%_]/g, "\\$&");
1528
- const { data, error } = await sb.from("agent_memories").select("*").eq("user_id", this.userId).or(
1529
- `content.ilike.%${sanitized}%,tags.cs.{${sanitized}}`
1530
- ).order("importance", { ascending: false }).limit(limit);
1531
- if (error) {
1532
- log.warn(`Memory search failed: ${error.message}`);
1683
+ async search(query3, limit = 10) {
1684
+ try {
1685
+ return await callMcpHandler("memory.search", {
1686
+ query: query3,
1687
+ limit
1688
+ });
1689
+ } catch (err) {
1690
+ log.warn(`Memory search failed: ${err instanceof Error ? err.message : err}`);
1533
1691
  return [];
1534
1692
  }
1535
- if (data && data.length > 0) {
1536
- const now = (/* @__PURE__ */ new Date()).toISOString();
1537
- await Promise.all(
1538
- data.map(
1539
- (m) => sb.from("agent_memories").update({
1540
- access_count: m.access_count + 1,
1541
- last_accessed_at: now
1542
- }).eq("id", m.id)
1543
- )
1544
- );
1545
- }
1546
- return data || [];
1547
1693
  }
1548
1694
  /**
1549
1695
  * Get the most important/recent memories to include in context.
@@ -1551,18 +1697,11 @@ var MemoryManager = class {
1551
1697
  * Automatically filters out expired memories.
1552
1698
  */
1553
1699
  async getContext(maxItems = 20) {
1554
- const sb = getSupabase();
1555
- const now = (/* @__PURE__ */ new Date()).toISOString();
1556
- 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);
1557
- 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);
1558
- 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);
1559
- const all = [
1560
- ...instructions || [],
1561
- ...preferences || [],
1562
- ...general || []
1563
- ];
1700
+ const all = await callMcpHandler("memory.get_context", {
1701
+ max_items: maxItems
1702
+ });
1564
1703
  const seen = /* @__PURE__ */ new Set();
1565
- return all.filter((m) => {
1704
+ return (all || []).filter((m) => {
1566
1705
  if (seen.has(m.id)) return false;
1567
1706
  seen.add(m.id);
1568
1707
  return true;
@@ -1599,32 +1738,23 @@ var MemoryManager = class {
1599
1738
  }
1600
1739
  // ── CRUD for CLI ────────────────────────────────────────────
1601
1740
  async list(category, limit = 20) {
1602
- const sb = getSupabase();
1603
- let query2 = sb.from("agent_memories").select("*").eq("user_id", this.userId).order("importance", { ascending: false }).order("created_at", { ascending: false }).limit(limit);
1604
- if (category) {
1605
- query2 = query2.eq("category", category);
1606
- }
1607
- const { data, error } = await query2;
1608
- if (error) throw new Error(`Failed to list memories: ${error.message}`);
1741
+ const data = await callMcpHandler("memory.list", {
1742
+ category: category || null,
1743
+ limit
1744
+ });
1609
1745
  return data || [];
1610
1746
  }
1611
1747
  async add(content, category = "general", importance = 5, tags = []) {
1612
1748
  return this.remember(content, category, { importance, tags });
1613
1749
  }
1614
1750
  async remove(memoryId) {
1615
- const sb = getSupabase();
1616
- const { error } = await sb.from("agent_memories").delete().eq("id", memoryId).eq("user_id", this.userId);
1617
- if (error) throw new Error(`Failed to delete memory: ${error.message}`);
1751
+ await callMcpHandler("memory.remove", { memory_id: memoryId });
1618
1752
  }
1619
1753
  async clear(category) {
1620
- const sb = getSupabase();
1621
- let query2 = sb.from("agent_memories").delete().eq("user_id", this.userId);
1622
- if (category) {
1623
- query2 = query2.eq("category", category);
1624
- }
1625
- const { error, count } = await query2.select("id");
1626
- if (error) throw new Error(`Failed to clear memories: ${error.message}`);
1627
- return count || 0;
1754
+ const result = await callMcpHandler("memory.clear", {
1755
+ category: category || null
1756
+ });
1757
+ return result.count;
1628
1758
  }
1629
1759
  };
1630
1760
 
@@ -1772,31 +1902,18 @@ var SkillManager = class {
1772
1902
  skills = /* @__PURE__ */ new Map();
1773
1903
  idfCache = /* @__PURE__ */ new Map();
1774
1904
  userId = null;
1775
- /** Budget for skill descriptions in system prompt (characters). */
1905
+ /** Cache for findRelevant() keyed by prompt, invalidated on skill changes */
1906
+ relevanceCache = /* @__PURE__ */ new Map();
1776
1907
  DESCRIPTION_BUDGET_CHARS = 16e3;
1777
- /**
1778
- * Set the user ID for DB operations.
1779
- * Called after authentication; enables DB-backed skill storage.
1780
- */
1781
1908
  setUserId(userId) {
1782
1909
  this.userId = userId;
1783
1910
  }
1784
- /**
1785
- * Load skills from DB (user's agent_skills collection).
1786
- * This is the only loading mechanism — no file-based loading.
1787
- */
1788
1911
  async loadFromDb() {
1789
1912
  if (!this.userId) return;
1790
1913
  try {
1791
- const sb = getSupabase();
1792
- const { data, error } = await sb.from("agent_skills").select("*").eq("user_id", this.userId).eq("is_active", true);
1793
- if (error) {
1794
- log.debug(`DB skill load failed: ${error.message}`);
1795
- return;
1796
- }
1914
+ const data = await callMcpHandler("skill.load");
1797
1915
  this.skills.clear();
1798
- const dbRows = data || [];
1799
- for (const row of dbRows) {
1916
+ for (const row of data || []) {
1800
1917
  const skill = this.rowToSkill(row);
1801
1918
  this.skills.set(skill.name, skill);
1802
1919
  }
@@ -1808,15 +1925,7 @@ var SkillManager = class {
1808
1925
  log.debug(`DB skill load error: ${err}`);
1809
1926
  }
1810
1927
  }
1811
- /**
1812
- * Convert a DB row to a Skill object.
1813
- */
1814
1928
  rowToSkill(row) {
1815
- const meta = row.metadata || {};
1816
- const rawVars = row.variables || meta.variables;
1817
- const variables = Array.isArray(rawVars) ? rawVars : void 0;
1818
- const rawConfig = row.config;
1819
- const config = rawConfig && typeof rawConfig === "object" ? rawConfig : void 0;
1820
1929
  return {
1821
1930
  name: row.name,
1822
1931
  description: row.description || "",
@@ -1833,14 +1942,14 @@ var SkillManager = class {
1833
1942
  source: row.source || "manual",
1834
1943
  dbId: row.id,
1835
1944
  sourceSkillId: row.source_skill_id || void 0,
1836
- variables,
1837
- config
1945
+ invocationCount: row.invocation_count || 0
1838
1946
  };
1839
1947
  }
1840
- /**
1841
- * Pre-compute IDF (Inverse Document Frequency) for all tokens across skills.
1842
- * Called once after loadFromDb(), avoids recomputing on every findRelevant() call.
1843
- */
1948
+ /** Invalidate caches when skills change (create, add, update, remove). */
1949
+ invalidateCaches() {
1950
+ this.relevanceCache.clear();
1951
+ this.buildIdfCache();
1952
+ }
1844
1953
  buildIdfCache() {
1845
1954
  this.idfCache.clear();
1846
1955
  const docFreq = /* @__PURE__ */ new Map();
@@ -1856,24 +1965,19 @@ var SkillManager = class {
1856
1965
  this.idfCache.set(word, Math.log(totalSkills / df) + 1);
1857
1966
  }
1858
1967
  }
1859
- /**
1860
- * Get all loaded skills (from user's agent_skills collection).
1861
- */
1862
1968
  getAll() {
1863
1969
  return Array.from(this.skills.values());
1864
1970
  }
1865
- /**
1866
- * Get a skill by name.
1867
- */
1868
1971
  get(name) {
1869
1972
  return this.skills.get(name);
1870
1973
  }
1871
- /**
1872
- * Find skills relevant to a given task prompt.
1873
- * Uses a TF-IDF-inspired scoring approach with pre-computed IDF cache.
1874
- */
1875
1974
  findRelevant(prompt, maxResults = 3) {
1876
- const lower = prompt.toLowerCase();
1975
+ const cacheKey = prompt.toLowerCase();
1976
+ const cached = this.relevanceCache.get(cacheKey);
1977
+ if (cached && cached.maxResults >= maxResults) {
1978
+ return cached.results.slice(0, maxResults);
1979
+ }
1980
+ const lower = cacheKey;
1877
1981
  const promptTokens = tokenize(lower);
1878
1982
  const promptTokenSet = new Set(promptTokens);
1879
1983
  const idf = (word) => this.idfCache.get(word) || 1;
@@ -1883,47 +1987,56 @@ var SkillManager = class {
1883
1987
  let score = 0;
1884
1988
  if (lower.includes(skill.name.toLowerCase())) score += 10;
1885
1989
  for (const kw of skill.keywords) {
1886
- if (lower.includes(kw.toLowerCase())) {
1887
- score += 8;
1888
- }
1990
+ if (lower.includes(kw.toLowerCase())) score += 8;
1889
1991
  }
1890
1992
  const descTokens = tokenize(skill.description.toLowerCase());
1891
1993
  for (const word of descTokens) {
1892
- if (promptTokenSet.has(word)) {
1893
- score += 3 * idf(word);
1894
- }
1994
+ if (promptTokenSet.has(word)) score += 3 * idf(word);
1895
1995
  }
1896
1996
  const contentTokens = tokenize(skill.content.toLowerCase());
1897
1997
  for (const word of contentTokens) {
1898
- if (promptTokenSet.has(word)) {
1899
- score += 0.5 * idf(word);
1900
- }
1998
+ if (promptTokenSet.has(word)) score += 0.5 * idf(word);
1901
1999
  }
1902
2000
  const promptBigrams = bigrams(promptTokens);
1903
2001
  const descBigrams = bigrams(descTokens);
1904
2002
  for (const bg of descBigrams) {
1905
2003
  if (promptBigrams.has(bg)) score += 5;
1906
2004
  }
1907
- if (score > 0) {
1908
- scored.push({ skill, score });
1909
- }
2005
+ if (score > 0) scored.push({ skill, score });
1910
2006
  }
1911
- return scored.sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => s.skill);
2007
+ const results = scored.sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => s.skill);
2008
+ this.relevanceCache.set(cacheKey, { results, maxResults });
2009
+ return results;
1912
2010
  }
1913
2011
  /**
1914
- * Build a lightweight prompt section with skill descriptions only.
1915
- * Full skill content is loaded on-demand via the skill_invoke tool.
2012
+ * Build lightweight skill descriptions for the system prompt.
2013
+ * When a taskPrompt is provided, relevant skills are prioritized to the top;
2014
+ * remaining skills are sorted by usage frequency (invocationCount).
1916
2015
  */
1917
- buildSkillDescriptions() {
1918
- const skills = this.getAll().filter((s) => !s.disableModelInvocation).sort((a, b) => {
1919
- if (a.metadata.always && !b.metadata.always) return -1;
1920
- if (!a.metadata.always && b.metadata.always) return 1;
1921
- return a.description.length - b.description.length;
2016
+ buildSkillDescriptions(taskPrompt) {
2017
+ const all = this.getAll().filter((s) => !s.disableModelInvocation);
2018
+ if (all.length === 0) return "";
2019
+ const alwaysSkills = all.filter((s) => s.metadata.always);
2020
+ const rest = all.filter((s) => !s.metadata.always);
2021
+ let relevantNames = null;
2022
+ if (taskPrompt) {
2023
+ const relevant = this.findRelevant(taskPrompt, 10);
2024
+ relevantNames = new Set(relevant.map((s) => s.name));
2025
+ }
2026
+ const sorted = rest.sort((a, b) => {
2027
+ if (relevantNames) {
2028
+ const aRelevant = relevantNames.has(a.name);
2029
+ const bRelevant = relevantNames.has(b.name);
2030
+ if (aRelevant && !bRelevant) return -1;
2031
+ if (!aRelevant && bRelevant) return 1;
2032
+ }
2033
+ return (b.invocationCount || 0) - (a.invocationCount || 0);
1922
2034
  });
1923
- if (skills.length === 0) return "";
2035
+ const skills = [...alwaysSkills, ...sorted];
1924
2036
  let budget = this.DESCRIPTION_BUDGET_CHARS;
1925
2037
  let prompt = "\n\n## Your Skills\n";
1926
- prompt += "These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n\n";
2038
+ prompt += "These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n";
2039
+ prompt += "If no skill matches but the task is a reusable pattern, consider creating one with skill_create.\n\n";
1927
2040
  let included = 0;
1928
2041
  for (const skill of skills) {
1929
2042
  const emoji = skill.metadata.emoji || "";
@@ -1937,14 +2050,12 @@ var SkillManager = class {
1937
2050
  }
1938
2051
  if (included < skills.length) {
1939
2052
  prompt += `
1940
- _(${skills.length - included} additional skills available \u2014 use skill_invoke to explore)_
2053
+ _(${skills.length - included} additional skills available \u2014 use skill_search to find more)_
1941
2054
  `;
1942
2055
  }
1943
2056
  return prompt;
1944
2057
  }
1945
- /**
1946
- * @deprecated Use buildSkillDescriptions() + skill_invoke tool instead.
1947
- */
2058
+ /** @deprecated Use buildSkillDescriptions() + skill_invoke tool instead. */
1948
2059
  buildSkillPrompt(taskPrompt) {
1949
2060
  const relevant = this.findRelevant(taskPrompt);
1950
2061
  if (relevant.length === 0) return "";
@@ -1961,40 +2072,23 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
1961
2072
  }
1962
2073
  return prompt;
1963
2074
  }
1964
- /**
1965
- * Create a new skill. Saves to `skills` table only (as private draft).
1966
- * The skill is NOT added to user's active collection (agent_skills) until
1967
- * the user approves it via addSkill().
1968
- *
1969
- * Returns { id, name } of the created skill, or null on failure.
1970
- */
1971
2075
  async create(name, description, content, options) {
1972
2076
  if (!this.userId) return null;
1973
2077
  try {
1974
- const sb = getSupabase();
1975
- const source = options?.source || "manual";
1976
- const metadata = {};
1977
- if (options?.emoji) {
1978
- metadata.openclaw = { emoji: options.emoji };
1979
- }
1980
- if (options?.variables && options.variables.length > 0) {
1981
- metadata.variables = options.variables;
1982
- }
1983
- const { data, error } = await sb.rpc("create_skill", {
1984
- p_user_id: this.userId,
1985
- p_name: name,
1986
- p_description: description,
1987
- p_content: content,
1988
- p_version: "1.0.0",
1989
- p_source: source,
1990
- p_emoji: options?.emoji || null,
1991
- p_keywords: options?.keywords || [],
1992
- p_metadata: metadata
1993
- });
1994
- if (error) {
1995
- log.debug(`Skill create failed for "${name}": ${error.message}`);
1996
- return null;
1997
- }
2078
+ const metadata = options?.emoji ? { openclaw: { emoji: options.emoji } } : {};
2079
+ const data = await callMcpHandler(
2080
+ "skill.create",
2081
+ {
2082
+ name,
2083
+ description,
2084
+ content,
2085
+ version: "1.0.0",
2086
+ source: options?.source || "manual",
2087
+ emoji: options?.emoji || null,
2088
+ keywords: options?.keywords || [],
2089
+ metadata
2090
+ }
2091
+ );
1998
2092
  const row = Array.isArray(data) ? data[0] : data;
1999
2093
  if (!row) {
2000
2094
  log.debug(`Skill create returned no data for "${name}"`);
@@ -2002,6 +2096,24 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2002
2096
  }
2003
2097
  const id = row.out_id || row.id;
2004
2098
  const skillName = row.out_name || row.name;
2099
+ this.skills.set(skillName, {
2100
+ name: skillName,
2101
+ description,
2102
+ version: "1.0.0",
2103
+ userInvocable: true,
2104
+ disableModelInvocation: false,
2105
+ keywords: options?.keywords || [],
2106
+ allowedTools: [],
2107
+ argumentHint: "",
2108
+ metadata: options?.emoji ? { emoji: options.emoji } : {},
2109
+ homepage: "",
2110
+ content,
2111
+ filePath: "",
2112
+ source: options?.source || "manual",
2113
+ dbId: id,
2114
+ invocationCount: 0
2115
+ });
2116
+ this.invalidateCaches();
2005
2117
  log.info(`Skill "${skillName}" created in skills table (pending approval)`);
2006
2118
  return { id, name: skillName };
2007
2119
  } catch (err) {
@@ -2009,45 +2121,15 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2009
2121
  return null;
2010
2122
  }
2011
2123
  }
2012
- /**
2013
- * Add a skill to the user's active collection (agent_skills).
2014
- * Copies content from the skills table.
2015
- * This is the "approval" step — after this, the skill is usable.
2016
- */
2017
2124
  async addSkill(skillId) {
2018
2125
  if (!this.userId) return null;
2019
2126
  try {
2020
- const sb = getSupabase();
2021
- const { data: pub, error: fetchErr } = await sb.from("skills").select("*").eq("id", skillId).eq("is_active", true).single();
2022
- if (fetchErr || !pub) {
2023
- log.debug(`Skill not found: ${skillId}`);
2024
- return null;
2025
- }
2026
- const row = pub;
2027
- const { data, error } = await sb.rpc("upsert_agent_skill", {
2028
- p_user_id: this.userId,
2029
- p_name: row.name,
2030
- p_description: row.description || "",
2031
- p_content: row.content,
2032
- p_version: row.version || "1.0.0",
2033
- p_source: row.source || "manual",
2034
- p_emoji: row.emoji || null,
2035
- p_keywords: row.keywords || [],
2036
- p_source_skill_id: skillId
2037
- });
2038
- if (error) {
2039
- log.debug(`addSkill failed: ${error.message}`);
2040
- return null;
2041
- }
2042
- try {
2043
- await sb.rpc("increment_install_count", { p_skill_id: skillId });
2044
- } catch {
2045
- try {
2046
- await sb.from("skills").update({ install_count: (row.install_count || 0) + 1 }).eq("id", skillId);
2047
- } catch {
2048
- }
2049
- }
2050
- const agentSkillRow = data && typeof data === "object" ? data : row;
2127
+ const result = await callMcpHandler(
2128
+ "skill.fetch_and_add",
2129
+ { skill_id: skillId }
2130
+ );
2131
+ const row = result.skill;
2132
+ const agentSkillRow = result.agent_skill && typeof result.agent_skill === "object" ? result.agent_skill : row;
2051
2133
  const skill = this.rowToSkill({
2052
2134
  ...agentSkillRow,
2053
2135
  name: row.name,
@@ -2056,7 +2138,7 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2056
2138
  source_skill_id: skillId
2057
2139
  });
2058
2140
  this.skills.set(skill.name, skill);
2059
- this.buildIdfCache();
2141
+ this.invalidateCaches();
2060
2142
  log.info(`Skill "${row.name}" added to user's collection`);
2061
2143
  return skill;
2062
2144
  } catch (err) {
@@ -2064,34 +2146,24 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2064
2146
  return null;
2065
2147
  }
2066
2148
  }
2067
- /**
2068
- * Remove a skill from user's collection (soft-delete in agent_skills).
2069
- */
2070
2149
  remove(name) {
2071
2150
  const skill = this.skills.get(name);
2072
2151
  if (!skill) return false;
2073
2152
  this.skills.delete(name);
2153
+ this.invalidateCaches();
2074
2154
  this.removeFromDb(name).catch(() => {
2075
2155
  });
2076
2156
  return true;
2077
2157
  }
2078
- /**
2079
- * List skills with summary info.
2080
- */
2081
2158
  listFormatted() {
2082
2159
  const skills = this.getAll();
2083
- if (skills.length === 0) {
2084
- return "No skills in your collection.";
2085
- }
2160
+ if (skills.length === 0) return "No skills in your collection.";
2086
2161
  return skills.map((s) => {
2087
2162
  const emoji = s.metadata.emoji || "";
2088
2163
  return ` ${emoji ? emoji + " " : ""}${s.name} (${s.version}) [${s.source}]
2089
2164
  ${s.description}`;
2090
2165
  }).join("\n\n");
2091
2166
  }
2092
- /**
2093
- * Check if a skill with a similar name already exists in user's collection.
2094
- */
2095
2167
  findSimilar(name) {
2096
2168
  if (this.skills.has(name)) return this.skills.get(name);
2097
2169
  const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -2112,10 +2184,6 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2112
2184
  }
2113
2185
  return null;
2114
2186
  }
2115
- /**
2116
- * Update an existing skill in user's collection.
2117
- * Bumps the patch version automatically. Also updates skills table if user is author.
2118
- */
2119
2187
  update(name, newContent, description) {
2120
2188
  const skill = this.skills.get(name);
2121
2189
  if (!skill) return false;
@@ -2126,68 +2194,47 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2126
2194
  skill.content = newContent;
2127
2195
  skill.description = newDescription;
2128
2196
  skill.version = newVersion;
2197
+ this.invalidateCaches();
2129
2198
  this.syncToAgentSkills(name, newDescription, newContent, newVersion, {
2130
2199
  source: "auto_improved"
2131
2200
  }).catch(() => {
2132
2201
  });
2133
2202
  if (skill.sourceSkillId && this.userId) {
2134
- const sb = getSupabase();
2135
- void (async () => {
2136
- try {
2137
- await sb.from("skills").update({ content: newContent, description: newDescription, version: newVersion }).eq("id", skill.sourceSkillId).eq("author_id", this.userId);
2138
- } catch {
2139
- }
2140
- })();
2203
+ callMcpHandler("skill.update_source", {
2204
+ source_skill_id: skill.sourceSkillId,
2205
+ content: newContent,
2206
+ description: newDescription,
2207
+ version: newVersion
2208
+ }).catch(() => {
2209
+ });
2141
2210
  }
2142
2211
  log.info(`Skill "${name}" updated to v${newVersion}`);
2143
2212
  return true;
2144
2213
  }
2145
2214
  // ── DB Integration ─────────────────────────────────────────────────
2146
- /**
2147
- * Persist a skill to agent_skills via upsert_agent_skill().
2148
- * Fire-and-forget: failures are logged but don't block.
2149
- */
2150
2215
  async syncToAgentSkills(name, description, content, version2, options) {
2151
2216
  if (!this.userId) return;
2152
2217
  try {
2153
- const sb = getSupabase();
2154
- const metadata = {};
2155
- if (options?.variables && options.variables.length > 0) {
2156
- metadata.variables = options.variables;
2157
- }
2158
- const { data, error } = await sb.rpc("upsert_agent_skill", {
2159
- p_user_id: this.userId,
2160
- p_name: name,
2161
- p_description: description,
2162
- p_content: content,
2163
- p_version: version2,
2164
- p_source: options?.source || "manual",
2165
- p_emoji: options?.emoji || null,
2166
- p_keywords: options?.keywords || [],
2167
- p_change_summary: options?.changeSummary || null,
2168
- p_source_skill_id: options?.sourceSkillId || null,
2169
- p_metadata: Object.keys(metadata).length > 0 ? metadata : null
2218
+ const data = await callMcpHandler("skill.upsert", {
2219
+ name,
2220
+ description,
2221
+ content,
2222
+ version: version2,
2223
+ source: options?.source || "manual",
2224
+ emoji: options?.emoji || null,
2225
+ keywords: options?.keywords || [],
2226
+ change_summary: options?.changeSummary || null,
2227
+ source_skill_id: options?.sourceSkillId || null
2170
2228
  });
2171
- if (error) {
2172
- log.debug(`DB skill sync failed for "${name}": ${error.message}`);
2173
- return;
2174
- }
2175
2229
  const skill = this.skills.get(name);
2176
2230
  if (skill && data && typeof data === "object" && "id" in data) {
2177
2231
  skill.dbId = data.id;
2178
2232
  }
2179
- if (skill && options?.variables) {
2180
- skill.variables = options.variables;
2181
- }
2182
2233
  log.debug(`Skill "${name}" synced to agent_skills`);
2183
2234
  } catch (err) {
2184
2235
  log.debug(`DB skill sync error for "${name}": ${err}`);
2185
2236
  }
2186
2237
  }
2187
- /**
2188
- * Log a skill invocation to agent_skill_invocations.
2189
- * Fire-and-forget: failures don't block task execution.
2190
- */
2191
2238
  async logInvocation(skillName, options) {
2192
2239
  if (!this.userId) return;
2193
2240
  const skill = this.skills.get(skillName);
@@ -2197,39 +2244,24 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2197
2244
  return;
2198
2245
  }
2199
2246
  try {
2200
- const sb = getSupabase();
2201
- const { error } = await sb.from("agent_skill_invocations").insert({
2247
+ await callMcpHandler("skill.log_invocation", {
2202
2248
  skill_id: skillDbId,
2203
- user_id: this.userId,
2204
2249
  message_id: options?.messageId || null,
2205
2250
  session_id: options?.sessionId || null,
2206
2251
  task_prompt: options?.taskPrompt?.slice(0, 500) || null,
2207
2252
  arguments: options?.arguments || null,
2208
2253
  success: options?.success ?? null
2209
2254
  });
2210
- if (error) {
2211
- log.debug(`Invocation log failed: ${error.message}`);
2212
- }
2213
2255
  } catch (err) {
2214
2256
  log.debug(`Invocation log error: ${err}`);
2215
2257
  }
2216
2258
  }
2217
- /**
2218
- * Search user's skills in DB using full-text search.
2219
- * Falls back to in-memory findRelevant() if DB is unavailable.
2220
- */
2221
- async searchDb(query2, limit = 10) {
2259
+ async searchDb(query3, limit = 10) {
2222
2260
  if (this.userId) {
2223
2261
  try {
2224
- const sb = getSupabase();
2225
- const { data, error } = await sb.rpc("search_agent_skills", {
2226
- p_user_id: this.userId,
2227
- p_query: query2,
2228
- p_limit: limit
2229
- });
2230
- if (!error && data) {
2231
- const rows = data;
2232
- return rows.map((row) => ({
2262
+ const data = await callMcpHandler("skill.search", { query: query3, limit });
2263
+ if (data) {
2264
+ return data.map((row) => ({
2233
2265
  name: row.name,
2234
2266
  description: row.description || "",
2235
2267
  emoji: row.emoji || "",
@@ -2240,7 +2272,7 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2240
2272
  } catch {
2241
2273
  }
2242
2274
  }
2243
- const results = this.findRelevant(query2, limit);
2275
+ const results = this.findRelevant(query3, limit);
2244
2276
  return results.map((s) => ({
2245
2277
  name: s.name,
2246
2278
  description: s.description,
@@ -2249,21 +2281,14 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2249
2281
  invocationCount: 0
2250
2282
  }));
2251
2283
  }
2252
- /**
2253
- * Remove a skill from DB (soft-delete).
2254
- */
2255
2284
  async removeFromDb(name) {
2256
2285
  if (!this.userId) return;
2257
2286
  try {
2258
- const sb = getSupabase();
2259
- await sb.from("agent_skills").update({ is_active: false }).eq("user_id", this.userId).eq("name", name);
2287
+ await callMcpHandler("skill.remove", { name });
2260
2288
  } catch {
2261
2289
  }
2262
2290
  }
2263
2291
  // ── Marketplace ────────────────────────────────────────────────────
2264
- /**
2265
- * Publish a skill to the marketplace (sets is_public = true in skills table).
2266
- */
2267
2292
  async publish(name, options) {
2268
2293
  if (!this.userId) return null;
2269
2294
  const skill = this.skills.get(name);
@@ -2273,31 +2298,21 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2273
2298
  return null;
2274
2299
  }
2275
2300
  try {
2276
- const sb = getSupabase();
2277
- const { data, error } = await sb.from("skills").upsert(
2278
- {
2279
- name: skill.name,
2280
- description: skill.description,
2281
- version: skill.version,
2282
- emoji: skill.metadata.emoji || null,
2283
- content: skill.content,
2284
- argument_hint: skill.argumentHint || null,
2285
- keywords: skill.keywords,
2286
- allowed_tools: skill.allowedTools,
2287
- author_id: this.userId,
2288
- author_name: options?.authorName || null,
2289
- metadata: skill.metadata,
2290
- homepage: skill.homepage || null,
2291
- category: options?.category || null,
2292
- source: skill.source,
2293
- is_public: true
2294
- },
2295
- { onConflict: "author_id,name" }
2296
- ).select("id").single();
2297
- if (error) {
2298
- log.debug(`Publish failed for "${name}": ${error.message}`);
2299
- return null;
2300
- }
2301
+ const data = await callMcpHandler("skill.publish", {
2302
+ name: skill.name,
2303
+ description: skill.description,
2304
+ version: skill.version,
2305
+ emoji: skill.metadata.emoji || null,
2306
+ content: skill.content,
2307
+ argument_hint: skill.argumentHint || null,
2308
+ keywords: skill.keywords,
2309
+ allowed_tools: skill.allowedTools,
2310
+ author_name: options?.authorName || null,
2311
+ metadata: skill.metadata,
2312
+ homepage: skill.homepage || null,
2313
+ category: options?.category || null,
2314
+ source: skill.source
2315
+ });
2301
2316
  log.info(`Skill "${name}" published to marketplace`);
2302
2317
  return data;
2303
2318
  } catch (err) {
@@ -2305,21 +2320,16 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2305
2320
  return null;
2306
2321
  }
2307
2322
  }
2308
- /**
2309
- * Browse marketplace skills (public + active).
2310
- */
2311
2323
  async browse(options) {
2312
2324
  try {
2313
- const sb = getSupabase();
2314
- const { data, error } = await sb.rpc("browse_skills", {
2315
- p_query: options?.query || null,
2316
- p_category: options?.category || null,
2317
- p_sort: options?.sort || "popular",
2318
- p_limit: options?.limit || 20,
2319
- p_offset: options?.offset || 0
2325
+ const data = await callMcpHandler("skill.browse", {
2326
+ query: options?.query || null,
2327
+ category: options?.category || null,
2328
+ sort: options?.sort || "popular",
2329
+ limit: options?.limit || 20,
2330
+ offset: options?.offset || 0
2320
2331
  });
2321
- if (error || !data) return [];
2322
- return data.map((r) => ({
2332
+ return (data || []).map((r) => ({
2323
2333
  id: r.id,
2324
2334
  name: r.name,
2325
2335
  description: r.description || "",
@@ -2335,53 +2345,17 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
2335
2345
  return [];
2336
2346
  }
2337
2347
  }
2338
- // ── Variable Configuration ──────────────────────────────────────────
2339
- /**
2340
- * Update user-specific variable values (config) for a skill.
2341
- * Persists to agent_skills.config in DB and updates in-memory.
2342
- */
2343
- async updateConfig(skillName, config) {
2344
- const skill = this.skills.get(skillName);
2345
- if (!skill) return false;
2346
- const merged = { ...skill.config || {}, ...config };
2347
- skill.config = merged;
2348
- if (this.userId && skill.dbId) {
2349
- try {
2350
- const sb = getSupabase();
2351
- await sb.from("agent_skills").update({ config: merged }).eq("id", skill.dbId).eq("user_id", this.userId);
2352
- log.debug(`Config updated for skill "${skillName}"`);
2353
- } catch (err) {
2354
- log.debug(`Config update failed for "${skillName}": ${err}`);
2355
- }
2356
- }
2357
- return true;
2358
- }
2359
- /**
2360
- * Get the unresolved (unconfigured) variables for a skill.
2361
- * Returns variables that are required but have no value in config.
2362
- */
2363
- getUnconfiguredVariables(skillName) {
2364
- const skill = this.skills.get(skillName);
2365
- if (!skill?.variables) return [];
2366
- return skill.variables.filter((v) => {
2367
- if (!v.required) return false;
2368
- const value = skill.config?.[v.name];
2369
- return value === void 0 || value === null || value === "";
2370
- });
2371
- }
2372
2348
  };
2373
- function substituteVariables(content, config, variables) {
2374
- if (!content.includes("{{")) return content;
2375
- return content.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
2376
- const value = config?.[varName];
2377
- if (value !== void 0 && value !== null) {
2378
- if (Array.isArray(value)) return value.join(", ");
2379
- return String(value);
2380
- }
2381
- const varDef = variables?.find((v) => v.name === varName);
2382
- if (varDef?.default !== void 0) return varDef.default;
2383
- return `{{${varName}: [NOT CONFIGURED]}}`;
2384
- });
2349
+ function validateSkillName(name) {
2350
+ if (!name || name.length === 0) return "name is empty";
2351
+ if (name.length > 64) return `name too long (${name.length}/64 chars)`;
2352
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
2353
+ return `name must be lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens. Got: "${name}"`;
2354
+ }
2355
+ return null;
2356
+ }
2357
+ function normalizeSkillName(name) {
2358
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 64);
2385
2359
  }
2386
2360
  function substituteArguments(content, args) {
2387
2361
  const parts = args.split(/\s+/);
@@ -2400,6 +2374,184 @@ function preprocessDynamicContext(content, cwd) {
2400
2374
  });
2401
2375
  }
2402
2376
 
2377
+ // src/agent/skill-evaluator.ts
2378
+ import {
2379
+ query
2380
+ } from "@anthropic-ai/claude-agent-sdk";
2381
+ var SKILL_EVALUATION_PROMPT = `You just completed a task. Now evaluate whether it should be saved as a reusable Agent Skill.
2382
+
2383
+ ## Agent Skills Format (agentskills.io)
2384
+
2385
+ A skill follows the SKILL.md format:
2386
+ - name: 1-64 chars, lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens
2387
+ - description: 1-1024 chars, describe WHAT it does AND WHEN to use it, include searchable keywords
2388
+ - body: markdown step-by-step instructions, examples, edge cases. Keep under 500 lines, <5000 tokens.
2389
+ - Use generic placeholders (e.g. {url}, {query}, {product_name}) instead of specific values
2390
+ - Instructions should be a REUSABLE workflow, not a transcript of what just happened
2391
+ - Include error handling steps and tool references (browser_navigate, browser_read_page, Bash, Read, etc.)
2392
+
2393
+ ## Your Decision
2394
+
2395
+ Respond with ONLY a JSON object (no markdown, no explanation outside the JSON). Choose one action:
2396
+
2397
+ 1. **"create"** \u2014 The task is a reusable workflow worth saving.
2398
+ Include: name, description, instructions (full SKILL.md body), emoji, keywords (3-5, include Chinese if task was in Chinese)
2399
+
2400
+ 2. **"update"** \u2014 An existing skill should be improved based on what you just learned.
2401
+ Include: existing_skill_name, improved_instructions (full updated body), improved_description (if changed)
2402
+
2403
+ 3. **"skip"** \u2014 Not worth capturing (simple Q&A, one-off, too vague, already fully covered by existing skill).
2404
+
2405
+ Always include "reason" explaining your decision.
2406
+
2407
+ Use your judgment \u2014 no rigid rules. Consider: Is this repeatable? Can it be generalized? Would it save time next time?`;
2408
+ async function evaluateAndMaybeCreateSkill(opts) {
2409
+ const { sessionId, skillManager, model } = opts;
2410
+ if (!sessionId) {
2411
+ log.debug("Skill evaluation skipped: no session ID to resume");
2412
+ return;
2413
+ }
2414
+ const existingSkills = skillManager.getAll();
2415
+ const existingList = existingSkills.length > 0 ? existingSkills.map((s) => `- ${s.name}: ${s.description}`).join("\n") : "(no existing skills)";
2416
+ const prompt = `${SKILL_EVALUATION_PROMPT}
2417
+
2418
+ ## Existing Skills (do NOT duplicate these)
2419
+ ${existingList}
2420
+
2421
+ Respond with a JSON object now.`;
2422
+ try {
2423
+ let responseText = "";
2424
+ for await (const message of query({
2425
+ prompt,
2426
+ options: {
2427
+ resume: sessionId,
2428
+ model,
2429
+ maxTurns: 1,
2430
+ allowedTools: []
2431
+ }
2432
+ })) {
2433
+ if (message.type === "assistant") {
2434
+ const assistantMsg = message;
2435
+ for (const block of assistantMsg.message.content) {
2436
+ if (block.type === "text") {
2437
+ responseText += block.text;
2438
+ }
2439
+ }
2440
+ } else if (message.type === "result") {
2441
+ const resultMsg = message;
2442
+ if (resultMsg.subtype === "success" && "total_cost_usd" in resultMsg) {
2443
+ log.debug(`Skill evaluation cost: $${resultMsg.total_cost_usd.toFixed(4)}`);
2444
+ }
2445
+ }
2446
+ }
2447
+ const decision = parseJsonResponse(responseText);
2448
+ if (!decision) {
2449
+ log.debug("Skill evaluation: no valid JSON in response");
2450
+ return;
2451
+ }
2452
+ if (!["create", "update", "skip"].includes(decision.action)) {
2453
+ log.debug("Skill evaluation: invalid action");
2454
+ return;
2455
+ }
2456
+ await executeSkillDecision(decision, skillManager);
2457
+ } catch (err) {
2458
+ log.debug(`Skill evaluation error: ${err}`);
2459
+ }
2460
+ }
2461
+ async function executeSkillDecision(decision, skillManager) {
2462
+ switch (decision.action) {
2463
+ case "create": {
2464
+ if (!decision.name || !decision.instructions) {
2465
+ log.debug("Skill create skipped: missing name or instructions");
2466
+ return;
2467
+ }
2468
+ let skillName = decision.name;
2469
+ if (validateSkillName(skillName)) {
2470
+ skillName = normalizeSkillName(skillName);
2471
+ if (!skillName || validateSkillName(skillName)) {
2472
+ log.debug(`Skill create skipped: name "${decision.name}" cannot be normalized`);
2473
+ return;
2474
+ }
2475
+ log.debug(`Normalized skill name: "${decision.name}" \u2192 "${skillName}"`);
2476
+ }
2477
+ const existing = skillManager.findSimilar(skillName);
2478
+ if (existing) {
2479
+ log.debug(`Skill create skipped: similar skill "${existing.name}" exists`);
2480
+ return;
2481
+ }
2482
+ const result = await skillManager.create(
2483
+ skillName,
2484
+ decision.description || "",
2485
+ decision.instructions,
2486
+ {
2487
+ source: "auto_extracted",
2488
+ emoji: decision.emoji,
2489
+ keywords: decision.keywords
2490
+ }
2491
+ );
2492
+ if (result) {
2493
+ await skillManager.syncToAgentSkills(
2494
+ skillName,
2495
+ decision.description || "",
2496
+ decision.instructions,
2497
+ "1.0.0",
2498
+ {
2499
+ source: "auto_extracted",
2500
+ emoji: decision.emoji,
2501
+ keywords: decision.keywords,
2502
+ sourceSkillId: result.id
2503
+ }
2504
+ );
2505
+ log.info(`Auto-created skill "${skillName}": ${decision.reason}`);
2506
+ }
2507
+ break;
2508
+ }
2509
+ case "update": {
2510
+ if (!decision.existing_skill_name || !decision.improved_instructions) {
2511
+ log.debug("Skill update skipped: missing skill name or instructions");
2512
+ return;
2513
+ }
2514
+ const updated = skillManager.update(
2515
+ decision.existing_skill_name,
2516
+ decision.improved_instructions,
2517
+ decision.improved_description
2518
+ );
2519
+ if (updated) {
2520
+ log.info(`Auto-improved skill "${decision.existing_skill_name}": ${decision.reason}`);
2521
+ } else {
2522
+ log.debug(`Skill update failed: "${decision.existing_skill_name}" not found`);
2523
+ }
2524
+ break;
2525
+ }
2526
+ case "skip":
2527
+ log.debug(`Skill evaluation: skip \u2014 ${decision.reason}`);
2528
+ break;
2529
+ }
2530
+ }
2531
+ function parseJsonResponse(text) {
2532
+ const trimmed = text.trim();
2533
+ try {
2534
+ const parsed = JSON.parse(trimmed);
2535
+ if (parsed.action) return parsed;
2536
+ } catch {
2537
+ }
2538
+ const start = trimmed.indexOf("{");
2539
+ if (start === -1) return null;
2540
+ let depth = 0;
2541
+ for (let i = start; i < trimmed.length; i++) {
2542
+ if (trimmed[i] === "{") depth++;
2543
+ else if (trimmed[i] === "}") depth--;
2544
+ if (depth === 0) {
2545
+ try {
2546
+ return JSON.parse(trimmed.slice(start, i + 1));
2547
+ } catch {
2548
+ return null;
2549
+ }
2550
+ }
2551
+ }
2552
+ return null;
2553
+ }
2554
+
2403
2555
  // src/utils/retry.ts
2404
2556
  async function withRetry(fn, opts = {}) {
2405
2557
  const {
@@ -2443,7 +2595,7 @@ import { z } from "zod/v4";
2443
2595
 
2444
2596
  // src/tools/filesystem.ts
2445
2597
  import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
2446
- import { resolve, relative, join as join2 } from "path";
2598
+ import { resolve, relative, join as join3 } from "path";
2447
2599
  import { glob } from "glob";
2448
2600
  function assertWithinWorkspace(filePath) {
2449
2601
  const config = getConfig();
@@ -2480,7 +2632,7 @@ async function searchFiles(pattern, directory) {
2480
2632
  ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
2481
2633
  });
2482
2634
  if (matches.length === 0) return "No files found matching the pattern.";
2483
- return matches.slice(0, 50).map((m) => relative(config.workspacePath, join2(cwd, m))).join("\n");
2635
+ return matches.slice(0, 50).map((m) => relative(config.workspacePath, join3(cwd, m))).join("\n");
2484
2636
  }
2485
2637
  async function listDirectory(path) {
2486
2638
  const config = getConfig();
@@ -2490,7 +2642,7 @@ async function listDirectory(path) {
2490
2642
  for (const entry of entries) {
2491
2643
  if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
2492
2644
  const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
2493
- const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then(
2645
+ const info = entry.isFile() ? await stat(join3(resolved, entry.name)).then(
2494
2646
  (s) => ` (${formatSize(s.size)})`
2495
2647
  ) : "";
2496
2648
  results.push(`${icon} ${entry.name}${info}`);
@@ -2514,11 +2666,11 @@ async function searchContent(pattern, fileGlob, directory) {
2514
2666
  const results = [];
2515
2667
  for (const file of files.slice(0, 200)) {
2516
2668
  try {
2517
- const content = await readFile(join2(cwd, file), "utf-8");
2669
+ const content = await readFile(join3(cwd, file), "utf-8");
2518
2670
  const lines = content.split("\n");
2519
2671
  for (let i = 0; i < lines.length; i++) {
2520
2672
  if (regex.test(lines[i])) {
2521
- const relPath = relative(config.workspacePath, join2(cwd, file));
2673
+ const relPath = relative(config.workspacePath, join3(cwd, file));
2522
2674
  results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
2523
2675
  regex.lastIndex = 0;
2524
2676
  if (results.length >= 30) break;
@@ -3044,26 +3196,23 @@ function createAgentToolsServer(deps) {
3044
3196
  ),
3045
3197
  tool(
3046
3198
  "skill_create",
3047
- "Create a new skill and add it to the user's collection. Returns the skill ID on success. Use {{variable_name}} syntax in instructions for user-specific data (e.g. {{github_repos}}, {{slack_channel}}).",
3199
+ "Create a new skill and add it to the user's collection. Returns the skill ID on success.",
3048
3200
  {
3049
3201
  name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
3050
3202
  description: z.string().describe("One-line description of what this skill does"),
3051
- instructions: z.string().describe(
3052
- "Markdown step-by-step instructions. Use {{variable_name}} placeholders for user-specific data (e.g. {{github_repos}}, {{trello_board}}, {{slack_channel}}). These will be resolved with user's config at invoke time."
3053
- ),
3054
- emoji: z.string().optional().describe("Single emoji representing this skill"),
3055
- variables: z.array(z.object({
3056
- name: z.string().describe("Variable name matching {{name}} in instructions"),
3057
- description: z.string().describe("Human-readable description of what this variable is for"),
3058
- type: z.enum(["string", "string[]", "number", "boolean"]).describe("Data type"),
3059
- required: z.boolean().describe("Whether the skill needs this to function"),
3060
- default: z.string().optional().describe("Default value if user doesn't configure"),
3061
- example: z.string().optional().describe("Example value to guide the user")
3062
- })).optional().describe(
3063
- "Variables that need user-specific configuration. Define one for each {{variable}} used in instructions."
3064
- )
3203
+ instructions: z.string().describe("Markdown step-by-step instructions"),
3204
+ emoji: z.string().optional().describe("Single emoji representing this skill")
3065
3205
  },
3066
3206
  async (args) => {
3207
+ const nameError = validateSkillName(args.name);
3208
+ if (nameError) {
3209
+ return {
3210
+ content: [{
3211
+ type: "text",
3212
+ text: `Invalid skill name: ${nameError}. Use lowercase kebab-case like "flight-booking".`
3213
+ }]
3214
+ };
3215
+ }
3067
3216
  const existing = skillManager.findSimilar(args.name);
3068
3217
  if (existing) {
3069
3218
  return {
@@ -3079,11 +3228,7 @@ function createAgentToolsServer(deps) {
3079
3228
  args.name,
3080
3229
  args.description,
3081
3230
  args.instructions,
3082
- {
3083
- source: "manual",
3084
- emoji: args.emoji,
3085
- variables: args.variables
3086
- }
3231
+ { source: "manual", emoji: args.emoji }
3087
3232
  );
3088
3233
  if (!result) {
3089
3234
  return {
@@ -3098,30 +3243,17 @@ function createAgentToolsServer(deps) {
3098
3243
  {
3099
3244
  source: "manual",
3100
3245
  emoji: args.emoji,
3101
- sourceSkillId: result.id,
3102
- variables: args.variables
3246
+ sourceSkillId: result.id
3103
3247
  }
3104
3248
  );
3105
- let responseText = `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`;
3106
- if (args.variables && args.variables.length > 0) {
3107
- const requiredVars = args.variables.filter((v) => v.required);
3108
- if (requiredVars.length > 0) {
3109
- responseText += `
3110
-
3111
- **Variables that need configuration:**
3112
- `;
3113
- for (const v of requiredVars) {
3114
- const example = v.example ? ` (e.g. ${v.example})` : "";
3115
- responseText += `- \`{{${v.name}}}\`: ${v.description}${example}
3116
- `;
3117
- }
3118
- responseText += `
3119
- Use \`skill_configure\` to set these values for the user, or ask the user to provide them.`;
3120
- }
3121
- }
3122
3249
  log.success(`Skill "${args.name}" created and added to collection`);
3123
3250
  return {
3124
- content: [{ type: "text", text: responseText }]
3251
+ content: [
3252
+ {
3253
+ type: "text",
3254
+ text: `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`
3255
+ }
3256
+ ]
3125
3257
  };
3126
3258
  }
3127
3259
  ),
@@ -3193,7 +3325,6 @@ Use \`skill_configure\` to set these values for the user, or ask the user to pro
3193
3325
  };
3194
3326
  }
3195
3327
  let content = skill.content;
3196
- content = substituteVariables(content, skill.config, skill.variables);
3197
3328
  if (args.arguments) {
3198
3329
  content = substituteArguments(content, args.arguments);
3199
3330
  }
@@ -3212,18 +3343,6 @@ ${content}`;
3212
3343
  **Allowed tools for this skill:** ${skill.allowedTools.join(", ")}
3213
3344
  `;
3214
3345
  }
3215
- const unconfigured = skillManager.getUnconfiguredVariables(args.name);
3216
- if (unconfigured.length > 0) {
3217
- response += `
3218
-
3219
- **\u26A0 Unconfigured variables \u2014 use \`skill_configure\` to set these:**
3220
- `;
3221
- for (const v of unconfigured) {
3222
- const example = v.example ? ` (e.g. ${v.example})` : "";
3223
- response += `- \`{{${v.name}}}\`: ${v.description}${example}
3224
- `;
3225
- }
3226
- }
3227
3346
  log.info(`Skill invoked: "${args.name}"`);
3228
3347
  skillManager.logInvocation(args.name, {
3229
3348
  messageId: taskId,
@@ -3263,53 +3382,6 @@ ${content}`;
3263
3382
  return { content: [{ type: "text", text: response }] };
3264
3383
  }
3265
3384
  ),
3266
- tool(
3267
- "skill_configure",
3268
- "Set user-specific variable values for a skill. Variables are defined in the skill template (e.g. {{github_repos}}) and need to be configured with the user's actual data before the skill can work properly.",
3269
- {
3270
- name: z.string().describe("Name of the skill to configure"),
3271
- config: z.record(z.unknown()).describe(
3272
- 'Key-value pairs of variable values. Keys must match variable names defined in the skill. Example: {"github_repos": ["octocat/hello-world", "myorg/myrepo"], "github_username": "octocat"}'
3273
- )
3274
- },
3275
- async (args) => {
3276
- const skill = skillManager.get(args.name);
3277
- if (!skill) {
3278
- const available = skillManager.getAll().map((s) => s.name).join(", ");
3279
- return {
3280
- content: [{
3281
- type: "text",
3282
- text: `Skill "${args.name}" not found. Available: ${available}`
3283
- }]
3284
- };
3285
- }
3286
- const updated = await skillManager.updateConfig(args.name, args.config);
3287
- if (!updated) {
3288
- return {
3289
- content: [{ type: "text", text: `Failed to update config for "${args.name}".` }]
3290
- };
3291
- }
3292
- const unconfigured = skillManager.getUnconfiguredVariables(args.name);
3293
- let responseText = `Configuration updated for skill "${args.name}".`;
3294
- if (unconfigured.length > 0) {
3295
- responseText += `
3296
-
3297
- **Still needs configuration:**
3298
- `;
3299
- for (const v of unconfigured) {
3300
- const example = v.example ? ` (e.g. ${v.example})` : "";
3301
- responseText += `- \`{{${v.name}}}\`: ${v.description}${example}
3302
- `;
3303
- }
3304
- } else {
3305
- responseText += ` All required variables are configured.`;
3306
- }
3307
- log.info(`Config updated for skill "${args.name}": ${Object.keys(args.config).join(", ")}`);
3308
- return {
3309
- content: [{ type: "text", text: responseText }]
3310
- };
3311
- }
3312
- ),
3313
3385
  tool(
3314
3386
  "skill_generate",
3315
3387
  "Prepare context for generating skills from a job description. Returns existing skills and job info so you can analyze the job and create skills using skill_create (which auto-adds to user's collection). After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
@@ -3358,8 +3430,6 @@ ${content}`;
3358
3430
  response += `- instructions: detailed step-by-step markdown instructions the agent can follow
3359
3431
  `;
3360
3432
  response += `- emoji: a single emoji representing the skill
3361
- `;
3362
- response += `- variables: define user-specific data needed by the skill (see below)
3363
3433
 
3364
3434
  `;
3365
3435
  response += `skill_create automatically adds the skill to the user's collection \u2014 no need to call skill_add.
@@ -3376,59 +3446,9 @@ ${content}`;
3376
3446
  `;
3377
3447
  response += `- Include error handling steps
3378
3448
  `;
3379
- response += `- Each skill should be a single, well-defined workflow (10-25 steps)
3380
-
3381
- `;
3382
- response += `**IMPORTANT \u2014 Use {{variables}} for user-specific data:**
3383
- `;
3384
- response += `Skills often need user-specific information (accounts, repos, boards, channels, etc.).
3385
- `;
3386
- response += `Instead of hardcoding or asking at runtime, use \`{{variable_name}}\` placeholders in instructions.
3387
-
3388
- `;
3389
- response += `Example: A GitHub PR review skill should use \`{{github_repos}}\` in its instructions:
3390
- `;
3391
- response += ` "Navigate to each repository in {{github_repos}} and check for open PRs..."
3392
-
3393
- `;
3394
- response += `For each \`{{variable}}\` used in instructions, add a matching entry in the \`variables\` array:
3395
- `;
3396
- response += `\`\`\`json
3397
- `;
3398
- response += `{
3399
- `;
3400
- response += ` "name": "github_repos",
3401
- `;
3402
- response += ` "description": "GitHub repositories to monitor (owner/repo format)",
3403
- `;
3404
- response += ` "type": "string[]",
3405
- `;
3406
- response += ` "required": true,
3449
+ response += `- Use placeholders like {query}, {date} for variable inputs
3407
3450
  `;
3408
- response += ` "example": "octocat/hello-world, myorg/myrepo"
3409
- `;
3410
- response += `}
3411
- `;
3412
- response += `\`\`\`
3413
-
3414
- `;
3415
- response += `Common variables by platform:
3416
- `;
3417
- response += `- GitHub: github_repos, github_username, github_org
3418
- `;
3419
- response += `- Trello: trello_board_url, trello_list_names
3420
- `;
3421
- response += `- Slack: slack_channels, slack_workspace_url
3422
- `;
3423
- response += `- Email: email_labels, email_filters
3424
- `;
3425
- response += `- E-commerce: store_url, competitor_urls, product_categories
3426
- `;
3427
- response += `- General: timezone, language, notification_channel
3428
-
3429
- `;
3430
- response += `After creating skills, if any have required variables, call \`skill_configure\` to set initial values `;
3431
- response += `(ask the user for the values first).
3451
+ response += `- Each skill should be a single, well-defined workflow (10-25 steps)
3432
3452
  `;
3433
3453
  return { content: [{ type: "text", text: response }] };
3434
3454
  }
@@ -3565,7 +3585,67 @@ ${content}`;
3565
3585
  };
3566
3586
  }
3567
3587
  ),
3568
- // ── User Confirmation Tool ─────────────────────────────────
3588
+ // ── User Interaction Tools ──────────────────────────────────
3589
+ tool(
3590
+ "request_user_input",
3591
+ "Ask the user a clarifying question and wait for their free-text response. Use this when you need information that cannot be inferred from context, memory, or the workspace \u2014 e.g. which account to use, specific preferences, ambiguous instructions, or missing parameters for a skill. Do NOT use this for information you can discover yourself (git remote, file contents, etc.).",
3592
+ {
3593
+ question: z.string().describe("The question to ask the user (supports markdown). Be specific about what you need and why."),
3594
+ placeholder: z.string().optional().describe("Placeholder text for the input field (e.g. 'https://github.com/owner/repo')"),
3595
+ timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)")
3596
+ },
3597
+ async (args) => {
3598
+ const actionId = `input_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3599
+ const timeout = (args.timeout_seconds || 300) * 1e3;
3600
+ const actionData = {
3601
+ id: actionId,
3602
+ type: "input",
3603
+ message: args.question,
3604
+ placeholder: args.placeholder || "",
3605
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3606
+ };
3607
+ try {
3608
+ await setActionRequest(taskId, actionData);
3609
+ log.info(`Input request ${actionId}: "${args.question.slice(0, 80)}..."`);
3610
+ emitEvent(taskId, "user_action_request", actionData).catch(() => {
3611
+ });
3612
+ const startTime = Date.now();
3613
+ const pollInterval = 2e3;
3614
+ while (Date.now() - startTime < timeout) {
3615
+ const response = await pollActionResponse(taskId);
3616
+ if (response && (!response.action_id || response.action_id === actionId)) {
3617
+ const text = response.text || response.value || "";
3618
+ log.info(`User input received: "${text.slice(0, 80)}"`);
3619
+ return {
3620
+ content: [{
3621
+ type: "text",
3622
+ text: JSON.stringify({ status: "responded", text })
3623
+ }]
3624
+ };
3625
+ }
3626
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
3627
+ }
3628
+ log.warn(`Input request ${actionId} timed out`);
3629
+ return {
3630
+ content: [{
3631
+ type: "text",
3632
+ text: JSON.stringify({
3633
+ status: "timeout",
3634
+ message: "User did not respond within the timeout period."
3635
+ })
3636
+ }]
3637
+ };
3638
+ } catch (err) {
3639
+ log.error(`request_user_input failed: ${err}`);
3640
+ return {
3641
+ content: [{
3642
+ type: "text",
3643
+ text: `Failed to request user input: ${err instanceof Error ? err.message : err}`
3644
+ }]
3645
+ };
3646
+ }
3647
+ }
3648
+ ),
3569
3649
  tool(
3570
3650
  "request_user_confirmation",
3571
3651
  "Pause and ask the user for approval or input via the web UI. Returns the user's chosen action_key. Use this BEFORE creating skills, making irreversible changes, etc. The agent will block until the user responds or the timeout expires.",
@@ -3597,7 +3677,7 @@ ${content}`;
3597
3677
  const pollInterval = 2e3;
3598
3678
  while (Date.now() - startTime < timeout) {
3599
3679
  const response = await pollActionResponse(taskId);
3600
- if (response) {
3680
+ if (response && (!response.action_id || response.action_id === actionId)) {
3601
3681
  const actionKey = response.action_key || response.action || "";
3602
3682
  const label = response.label || actionKey;
3603
3683
  log.info(`User responded: ${label} (${actionKey})`);
@@ -3721,8 +3801,10 @@ ${content}`;
3721
3801
  const tz = args.timezone || "UTC";
3722
3802
  try {
3723
3803
  const task = await createScheduledTask(userId, name, prompt, args.cron, tz);
3724
- const sb = getSupabase();
3725
- await sb.from("agent_scheduled_tasks").update({ job_id: job.jobId }).eq("id", task.id);
3804
+ await callMcpHandler("schedule.link_job", {
3805
+ task_id: task.id,
3806
+ job_id: job.jobId
3807
+ });
3726
3808
  const nextRun = task.next_run_at ? new Date(task.next_run_at).toLocaleString() : "calculating...";
3727
3809
  let response = `## Job Scheduled: ${args.job_name}
3728
3810
 
@@ -3825,20 +3907,14 @@ ${content}`;
3825
3907
  ]
3826
3908
  });
3827
3909
  }
3828
- async function saveJobToDb(userId, jobName, jobDescription, createdSkillNames) {
3910
+ async function saveJobToDb(_userId, jobName, jobDescription, createdSkillNames) {
3829
3911
  try {
3830
- const sb = getSupabase();
3831
- const { data, error } = await sb.rpc("save_job_with_skills", {
3832
- p_user_id: userId,
3833
- p_job_name: jobName,
3834
- p_job_description: jobDescription,
3835
- p_skill_names: createdSkillNames
3912
+ const data = await callMcpHandler("job.save_with_skills", {
3913
+ job_name: jobName,
3914
+ job_description: jobDescription,
3915
+ skill_names: createdSkillNames
3836
3916
  });
3837
- if (error) {
3838
- log.debug(`Failed to save job "${jobName}" via RPC: ${error.message}`);
3839
- return;
3840
- }
3841
- log.debug(`Job "${jobName}" saved via RPC (id: ${data}), ${createdSkillNames.length} skill(s) linked`);
3917
+ log.debug(`Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`);
3842
3918
  } catch (err) {
3843
3919
  log.debug(`saveJobToDb error: ${err}`);
3844
3920
  }
@@ -3923,15 +3999,25 @@ Available capabilities:
3923
3999
  - PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
3924
4000
  - Before completing a task, consider if anything learned should be remembered for future conversations
3925
4001
 
3926
- 4. SKILL PLANNING (pre-task):
3927
- - Before executing a complex task, analyze if it matches an existing skill (use skill_invoke)
3928
- - If no matching skill exists, consider whether this task represents a reusable workflow
3929
- - To create a new skill: use skill_create to save a draft, then ASK the user if they want to add it
3930
- - If the user approves, use skill_add to add it to their collection, then proceed with the task
3931
- - If a skill's instructions could be improved based on your experience, use skill_improve
3932
- - Use skill_search to find relevant skills when the task doesn't obviously match the listed skills
3933
- - Skills use {{variable_name}} placeholders for user-specific data (repos, channels, boards, etc.)
3934
- - Use skill_configure to set variable values after creating skills or when the user provides their data
4002
+ 4. SKILL-AWARE EXECUTION (CRITICAL \u2014 follow this for EVERY task):
4003
+ Step A \u2014 Search: Before executing ANY task, check if an existing skill matches (use skill_invoke or skill_search).
4004
+ Step B \u2014 If skill found: load it with skill_invoke and follow its instructions precisely. If the instructions are incomplete or wrong, adapt and improve as you go \u2014 note what changed.
4005
+ Step C \u2014 If NO skill found: BEFORE executing, draft a skill plan following the Agent Skills format:
4006
+ Skill Draft: [kebab-case-name]
4007
+ Description: [what this skill does and when to use it]
4008
+ Steps:
4009
+ 1. [first step]
4010
+ 2. [second step]
4011
+ ...
4012
+ The draft should be a reusable workflow, not specific to this one request. Use generic placeholders where the user provided specific values.
4013
+ Step D \u2014 Execute: Follow the skill draft (or loaded skill) step by step. Refine the draft as you discover better approaches, edge cases, or missing steps.
4014
+ Step E \u2014 After execution: The system will automatically evaluate whether to save the skill. You do NOT need to call skill_create manually.
4015
+
4016
+ Agent Skills format reference (agentskills.io):
4017
+ - name: 1-64 chars, lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens
4018
+ - description: 1-1024 chars, describe what the skill does AND when to use it, include keywords for discoverability
4019
+ - body: markdown step-by-step instructions, examples, edge cases. Keep under 500 lines.
4020
+ - Progressive disclosure: metadata (~100 tokens) \u2192 instructions (<5000 tokens) \u2192 references (on demand)
3935
4021
 
3936
4022
  5. JOB AUTOMATION:
3937
4023
  - When the user describes their job/role/daily work, use skill_generate to decompose it into automatable skills
@@ -3968,6 +4054,14 @@ Guidelines:
3968
4054
  - Summarize results clearly at the end
3969
4055
  - When you learn something about the user (preferences, habits), use memory_store to remember it
3970
4056
 
4057
+ CRITICAL \u2014 Ask before you guess:
4058
+ - Before executing a task, verify you have all required information. If anything is ambiguous or missing, use request_user_input to ask.
4059
+ - First try to resolve unknowns yourself: check memories, read workspace files (e.g. git remote, config files), or infer from conversation history.
4060
+ - If you still lack a critical piece of information after self-resolution, ASK the user via request_user_input. Do NOT guess, assume defaults, or proceed with incomplete information.
4061
+ - Examples of when to ask: which account/repo/project to target, what format the user wants, which of multiple options to choose, credentials or URLs that cannot be inferred.
4062
+ - Keep questions specific and actionable. Explain what you already know and what exactly you need.
4063
+ - After receiving the answer, store it with memory_store if it is likely to be useful in future conversations.
4064
+
3971
4065
  Workspace path: {workspace_path}`;
3972
4066
  var MAX_HISTORY_ENTRIES = 10;
3973
4067
  var MAX_RESPONSE_LENGTH = 1500;
@@ -3992,6 +4086,18 @@ var TaskProcessor = class {
3992
4086
  setSessionId(sessionId) {
3993
4087
  this.sessionId = sessionId;
3994
4088
  }
4089
+ /**
4090
+ * Post-task: resume the same Agent SDK session to evaluate whether
4091
+ * to create/update a skill. The agent already has full context from
4092
+ * the task it just completed — no need to re-describe anything.
4093
+ */
4094
+ async evaluateSkillPostTask(agentSessionId, model) {
4095
+ await evaluateAndMaybeCreateSkill({
4096
+ sessionId: agentSessionId,
4097
+ skillManager: this.skillManager,
4098
+ model
4099
+ });
4100
+ }
3995
4101
  async processTask(task) {
3996
4102
  const config = getConfig();
3997
4103
  resetEventSequence();
@@ -4001,6 +4107,7 @@ var TaskProcessor = class {
4001
4107
  let finalResponse = "";
4002
4108
  const toolCallRecords = [];
4003
4109
  let tokenUsage;
4110
+ let agentSessionId;
4004
4111
  try {
4005
4112
  await emitEvent(task.id, "status_change", { status: "running" });
4006
4113
  let systemPrompt = BASE_SYSTEM_PROMPT.replace("{workspace_path}", config.workspacePath);
@@ -4014,7 +4121,7 @@ var TaskProcessor = class {
4014
4121
  log.debug(`Memory load failed: ${err}`);
4015
4122
  }
4016
4123
  }
4017
- const skillPrompt = this.skillManager.buildSkillDescriptions();
4124
+ const skillPrompt = this.skillManager.buildSkillDescriptions(task.prompt);
4018
4125
  if (skillPrompt) {
4019
4126
  systemPrompt += skillPrompt;
4020
4127
  }
@@ -4065,13 +4172,13 @@ var TaskProcessor = class {
4065
4172
  "mcp__assistme-agent__skill_improve",
4066
4173
  "mcp__assistme-agent__skill_invoke",
4067
4174
  "mcp__assistme-agent__skill_search",
4068
- "mcp__assistme-agent__skill_configure",
4069
4175
  "mcp__assistme-agent__skill_generate",
4070
4176
  "mcp__assistme-agent__skill_link_job",
4071
4177
  "mcp__assistme-agent__skill_browse",
4072
4178
  "mcp__assistme-agent__skill_add",
4073
4179
  "mcp__assistme-agent__skill_publish",
4074
- // User confirmation
4180
+ // User interaction
4181
+ "mcp__assistme-agent__request_user_input",
4075
4182
  "mcp__assistme-agent__request_user_confirmation",
4076
4183
  // Job automation tools
4077
4184
  "mcp__assistme-agent__job_run",
@@ -4103,7 +4210,7 @@ var TaskProcessor = class {
4103
4210
  "assistme-agent": agentToolsServer
4104
4211
  },
4105
4212
  hooks: eventHooks,
4106
- persistSession: false,
4213
+ persistSession: true,
4107
4214
  abortController
4108
4215
  };
4109
4216
  const taskStartTime = Date.now();
@@ -4111,7 +4218,7 @@ var TaskProcessor = class {
4111
4218
  abortController.abort();
4112
4219
  }, taskTimeoutMs);
4113
4220
  try {
4114
- for await (const message of query({
4221
+ for await (const message of query2({
4115
4222
  prompt: promptMessages(),
4116
4223
  options
4117
4224
  })) {
@@ -4163,6 +4270,9 @@ var TaskProcessor = class {
4163
4270
  break;
4164
4271
  }
4165
4272
  default:
4273
+ if (message.type === "system" && "subtype" in message && message.subtype === "init") {
4274
+ agentSessionId = message.session_id;
4275
+ }
4166
4276
  log.debug(`SDK message type: ${message.type}`);
4167
4277
  break;
4168
4278
  }
@@ -4183,6 +4293,9 @@ var TaskProcessor = class {
4183
4293
  convHistory.splice(0, convHistory.length - MAX_HISTORY_ENTRIES * 2);
4184
4294
  }
4185
4295
  this.historyCache.set(task.conversation_id, convHistory);
4296
+ if (agentSessionId) {
4297
+ this.evaluateSkillPostTask(agentSessionId, config.model).catch((err) => log.debug(`Post-task skill evaluation skipped: ${err}`));
4298
+ }
4186
4299
  } catch (err) {
4187
4300
  const errorMsg = err instanceof Error ? err.message : String(err);
4188
4301
  log.error(`Task failed: ${errorMsg}`);
@@ -4362,9 +4475,8 @@ import chalk5 from "chalk";
4362
4475
  function registerStatusCommand(program2) {
4363
4476
  program2.command("status").description("Check the status of the current agent session").action(async () => {
4364
4477
  try {
4365
- const userId = await getCurrentUserId();
4366
- const sb = getSupabase();
4367
- const { data: sessions } = await sb.from("agent_sessions").select("*").eq("user_id", userId).in("status", ["online", "busy"]).order("started_at", { ascending: false }).limit(5);
4478
+ await getCurrentUserId();
4479
+ const sessions = await getActiveSessions(5);
4368
4480
  if (!sessions || sessions.length === 0) {
4369
4481
  console.log(chalk5.yellow("No active sessions found."));
4370
4482
  console.log('Run "assistme" to begin a new session.');
@@ -4563,17 +4675,17 @@ Memories (${memories.length}):`));
4563
4675
  process.exit(1);
4564
4676
  }
4565
4677
  });
4566
- memoryCmd.command("search <query>").description("Search memories").action(async (query2) => {
4678
+ memoryCmd.command("search <query>").description("Search memories").action(async (query3) => {
4567
4679
  try {
4568
4680
  const userId = await getCurrentUserId();
4569
4681
  const mm = new MemoryManager(userId);
4570
- const results = await mm.search(query2);
4682
+ const results = await mm.search(query3);
4571
4683
  if (results.length === 0) {
4572
- console.log(chalk7.yellow(`No memories matching "${query2}"`));
4684
+ console.log(chalk7.yellow(`No memories matching "${query3}"`));
4573
4685
  return;
4574
4686
  }
4575
4687
  console.log(chalk7.bold(`
4576
- Search results for "${query2}":`));
4688
+ Search results for "${query3}":`));
4577
4689
  for (const m of results) {
4578
4690
  console.log(` [${m.category}] ${m.content}`);
4579
4691
  }
@@ -4641,18 +4753,18 @@ function registerSkillCommands(program2) {
4641
4753
  );
4642
4754
  console.log();
4643
4755
  });
4644
- skillCmd.command("search <query>").description("Search skills in your collection and marketplace").action(async (query2) => {
4756
+ skillCmd.command("search <query>").description("Search skills in your collection and marketplace").action(async (query3) => {
4645
4757
  const spinner = ora4("Searching skills...").start();
4646
4758
  const sm = await getAuthenticatedSkillManager();
4647
4759
  try {
4648
- const results = await sm.searchDb(query2);
4760
+ const results = await sm.searchDb(query3);
4649
4761
  spinner.stop();
4650
4762
  if (results.length === 0) {
4651
- console.log(chalk8.yellow(`No skills found for "${query2}"`));
4763
+ console.log(chalk8.yellow(`No skills found for "${query3}"`));
4652
4764
  return;
4653
4765
  }
4654
4766
  console.log(chalk8.bold(`
4655
- Skills matching "${query2}":`));
4767
+ Skills matching "${query3}":`));
4656
4768
  for (const r of results) {
4657
4769
  const emoji = r.emoji ? `${r.emoji} ` : "";
4658
4770
  console.log(` ${emoji}${chalk8.cyan(r.name)} [${r.source}]`);
@@ -4745,7 +4857,7 @@ function registerJobCommands(program2) {
4745
4857
  jobCmd.command("list").description("List your defined jobs").action(async () => {
4746
4858
  try {
4747
4859
  const userId = await getCurrentUserId();
4748
- const { JobRunner: JobRunner2 } = await import("./job-runner-XTGLMPZ3.js");
4860
+ const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
4749
4861
  const runner = new JobRunner2(userId);
4750
4862
  const jobs = await runner.listJobs();
4751
4863
  if (jobs.length === 0) {
@@ -4773,7 +4885,7 @@ function registerJobCommands(program2) {
4773
4885
  jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
4774
4886
  try {
4775
4887
  const userId = await getCurrentUserId();
4776
- const { JobRunner: JobRunner2 } = await import("./job-runner-XTGLMPZ3.js");
4888
+ const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
4777
4889
  const runner = new JobRunner2(userId);
4778
4890
  const runs = await runner.getRunHistory(
4779
4891
  name,
@@ -4827,7 +4939,7 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
4827
4939
  process.exit(1);
4828
4940
  }
4829
4941
  const userId = await getCurrentUserId();
4830
- const { JobRunner: JobRunner2 } = await import("./job-runner-XTGLMPZ3.js");
4942
+ const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
4831
4943
  const runner = new JobRunner2(userId);
4832
4944
  const job = await runner.loadJob(name);
4833
4945
  if (!job) {
@@ -4849,8 +4961,10 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
4849
4961
  opts.cron,
4850
4962
  tz
4851
4963
  );
4852
- const sb = getSupabase();
4853
- await sb.from("agent_scheduled_tasks").update({ job_id: job.jobId }).eq("id", task.id);
4964
+ await callMcpHandler("schedule.link_job", {
4965
+ task_id: task.id,
4966
+ job_id: job.jobId
4967
+ });
4854
4968
  log.success(`Job "${name}" scheduled: ${opts.cron} (${tz})`);
4855
4969
  console.log(` Skills: ${job.skills.length}`);
4856
4970
  console.log(