assistme 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-TTEGHE2E.js +47 -0
- package/dist/chunk-UWE5WVQI.js +289 -0
- package/dist/config-PUIS2TQL.js +12 -0
- package/dist/index.js +434 -669
- package/dist/job-runner-N4XAAWLJ.js +7 -0
- package/package.json +1 -2
- package/src/agent/job-runner.ts +33 -71
- package/src/agent/mcp-servers.ts +26 -149
- package/src/agent/memory.test.ts +41 -65
- package/src/agent/memory.ts +33 -134
- package/src/agent/scheduler.ts +47 -93
- package/src/agent/session.test.ts +8 -12
- package/src/agent/session.ts +10 -53
- package/src/agent/skills.ts +89 -488
- package/src/commands/job.ts +6 -6
- package/src/commands/status.ts +3 -10
- package/src/db/api-client.ts +68 -0
- package/src/db/supabase.test.ts +71 -184
- package/src/db/supabase.ts +140 -243
- package/dist/chunk-XY3LGAOY.js +0 -580
- package/dist/job-runner-XTGLMPZ3.js +0 -6
package/dist/index.js
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
JobRunner,
|
|
4
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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) =>
|
|
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 (
|
|
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 =
|
|
943
|
+
const home = homedir2();
|
|
750
944
|
const os = platform();
|
|
751
945
|
if (os === "darwin") {
|
|
752
946
|
if (chromePath.includes("Brave Browser"))
|
|
753
|
-
return
|
|
947
|
+
return join2(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
|
|
754
948
|
if (chromePath.includes("Microsoft Edge"))
|
|
755
|
-
return
|
|
949
|
+
return join2(home, "Library", "Application Support", "Microsoft Edge");
|
|
756
950
|
if (chromePath.includes("Chromium"))
|
|
757
|
-
return
|
|
951
|
+
return join2(home, "Library", "Application Support", "Chromium");
|
|
758
952
|
if (chromePath.includes("Canary"))
|
|
759
|
-
return
|
|
760
|
-
return
|
|
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 ||
|
|
957
|
+
const appData = process.env.LOCALAPPDATA || join2(home, "AppData", "Local");
|
|
764
958
|
if (chromePath.includes("brave"))
|
|
765
|
-
return
|
|
766
|
-
if (chromePath.includes("msedge")) return
|
|
767
|
-
return
|
|
768
|
-
}
|
|
769
|
-
if (chromePath.includes("brave")) return
|
|
770
|
-
if (chromePath.includes("microsoft-edge")) return
|
|
771
|
-
if (chromePath.includes("chromium")) return
|
|
772
|
-
return
|
|
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 =
|
|
776
|
-
const debugDir =
|
|
777
|
-
if (!
|
|
778
|
-
|
|
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 (
|
|
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 =
|
|
792
|
-
const dest =
|
|
985
|
+
const src = join2(realDir, file);
|
|
986
|
+
const dest = join2(debugDir, file);
|
|
793
987
|
try {
|
|
794
|
-
if (
|
|
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 =
|
|
802
|
-
const destProfile =
|
|
803
|
-
if (
|
|
804
|
-
|
|
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 =
|
|
807
|
-
const dest =
|
|
1000
|
+
const src = join2(srcProfile, file);
|
|
1001
|
+
const dest = join2(destProfile, file);
|
|
808
1002
|
try {
|
|
809
|
-
if (
|
|
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 =
|
|
817
|
-
const destExt =
|
|
1010
|
+
const srcExt = join2(srcProfile, "Extensions");
|
|
1011
|
+
const destExt = join2(destProfile, "Extensions");
|
|
818
1012
|
try {
|
|
819
|
-
if (
|
|
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 =
|
|
911
|
-
if (
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
})
|
|
1348
|
+
});
|
|
1155
1349
|
try {
|
|
1156
1350
|
await this.onScheduledTask(task);
|
|
1157
|
-
await
|
|
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
|
|
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(
|
|
1169
|
-
const sb = getSupabase();
|
|
1368
|
+
async function createScheduledTask(_userId, name, prompt, cronExpression, timezone = "UTC") {
|
|
1170
1369
|
const nextRun = getNextRunTime(cronExpression, timezone);
|
|
1171
|
-
|
|
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
|
-
})
|
|
1179
|
-
if (error) throw new Error(`Failed to create schedule: ${error.message}`);
|
|
1180
|
-
return data;
|
|
1376
|
+
});
|
|
1181
1377
|
}
|
|
1182
|
-
async function listScheduledTasks(
|
|
1183
|
-
|
|
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
|
|
1190
|
-
const update = { enabled };
|
|
1382
|
+
const params = { task_id: taskId, enabled };
|
|
1191
1383
|
if (enabled) {
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
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
|
|
1400
|
-
|
|
1401
|
-
|
|
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();
|
|
@@ -1493,29 +1656,24 @@ import {
|
|
|
1493
1656
|
|
|
1494
1657
|
// src/agent/memory.ts
|
|
1495
1658
|
var MemoryManager = class {
|
|
1496
|
-
|
|
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
|
|
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
|
-
})
|
|
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
|
}
|
|
@@ -1523,27 +1681,15 @@ var MemoryManager = class {
|
|
|
1523
1681
|
* Search memories by query text. Uses ILIKE + tag containment.
|
|
1524
1682
|
*/
|
|
1525
1683
|
async search(query2, limit = 10) {
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
log.warn(`Memory search failed: ${
|
|
1684
|
+
try {
|
|
1685
|
+
return await callMcpHandler("memory.search", {
|
|
1686
|
+
query: query2,
|
|
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
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
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
|
-
|
|
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
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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,16 @@ 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). */
|
|
1776
1905
|
DESCRIPTION_BUDGET_CHARS = 16e3;
|
|
1777
|
-
/**
|
|
1778
|
-
* Set the user ID for DB operations.
|
|
1779
|
-
* Called after authentication; enables DB-backed skill storage.
|
|
1780
|
-
*/
|
|
1781
1906
|
setUserId(userId) {
|
|
1782
1907
|
this.userId = userId;
|
|
1783
1908
|
}
|
|
1784
|
-
/**
|
|
1785
|
-
* Load skills from DB (user's agent_skills collection).
|
|
1786
|
-
* This is the only loading mechanism — no file-based loading.
|
|
1787
|
-
*/
|
|
1788
1909
|
async loadFromDb() {
|
|
1789
1910
|
if (!this.userId) return;
|
|
1790
1911
|
try {
|
|
1791
|
-
const
|
|
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
|
-
}
|
|
1912
|
+
const data = await callMcpHandler("skill.load");
|
|
1797
1913
|
this.skills.clear();
|
|
1798
|
-
const
|
|
1799
|
-
for (const row of dbRows) {
|
|
1914
|
+
for (const row of data || []) {
|
|
1800
1915
|
const skill = this.rowToSkill(row);
|
|
1801
1916
|
this.skills.set(skill.name, skill);
|
|
1802
1917
|
}
|
|
@@ -1808,15 +1923,7 @@ var SkillManager = class {
|
|
|
1808
1923
|
log.debug(`DB skill load error: ${err}`);
|
|
1809
1924
|
}
|
|
1810
1925
|
}
|
|
1811
|
-
/**
|
|
1812
|
-
* Convert a DB row to a Skill object.
|
|
1813
|
-
*/
|
|
1814
1926
|
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
1927
|
return {
|
|
1821
1928
|
name: row.name,
|
|
1822
1929
|
description: row.description || "",
|
|
@@ -1832,15 +1939,9 @@ var SkillManager = class {
|
|
|
1832
1939
|
filePath: "",
|
|
1833
1940
|
source: row.source || "manual",
|
|
1834
1941
|
dbId: row.id,
|
|
1835
|
-
sourceSkillId: row.source_skill_id || void 0
|
|
1836
|
-
variables,
|
|
1837
|
-
config
|
|
1942
|
+
sourceSkillId: row.source_skill_id || void 0
|
|
1838
1943
|
};
|
|
1839
1944
|
}
|
|
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
|
-
*/
|
|
1844
1945
|
buildIdfCache() {
|
|
1845
1946
|
this.idfCache.clear();
|
|
1846
1947
|
const docFreq = /* @__PURE__ */ new Map();
|
|
@@ -1856,22 +1957,12 @@ var SkillManager = class {
|
|
|
1856
1957
|
this.idfCache.set(word, Math.log(totalSkills / df) + 1);
|
|
1857
1958
|
}
|
|
1858
1959
|
}
|
|
1859
|
-
/**
|
|
1860
|
-
* Get all loaded skills (from user's agent_skills collection).
|
|
1861
|
-
*/
|
|
1862
1960
|
getAll() {
|
|
1863
1961
|
return Array.from(this.skills.values());
|
|
1864
1962
|
}
|
|
1865
|
-
/**
|
|
1866
|
-
* Get a skill by name.
|
|
1867
|
-
*/
|
|
1868
1963
|
get(name) {
|
|
1869
1964
|
return this.skills.get(name);
|
|
1870
1965
|
}
|
|
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
1966
|
findRelevant(prompt, maxResults = 3) {
|
|
1876
1967
|
const lower = prompt.toLowerCase();
|
|
1877
1968
|
const promptTokens = tokenize(lower);
|
|
@@ -1883,37 +1974,25 @@ var SkillManager = class {
|
|
|
1883
1974
|
let score = 0;
|
|
1884
1975
|
if (lower.includes(skill.name.toLowerCase())) score += 10;
|
|
1885
1976
|
for (const kw of skill.keywords) {
|
|
1886
|
-
if (lower.includes(kw.toLowerCase()))
|
|
1887
|
-
score += 8;
|
|
1888
|
-
}
|
|
1977
|
+
if (lower.includes(kw.toLowerCase())) score += 8;
|
|
1889
1978
|
}
|
|
1890
1979
|
const descTokens = tokenize(skill.description.toLowerCase());
|
|
1891
1980
|
for (const word of descTokens) {
|
|
1892
|
-
if (promptTokenSet.has(word))
|
|
1893
|
-
score += 3 * idf(word);
|
|
1894
|
-
}
|
|
1981
|
+
if (promptTokenSet.has(word)) score += 3 * idf(word);
|
|
1895
1982
|
}
|
|
1896
1983
|
const contentTokens = tokenize(skill.content.toLowerCase());
|
|
1897
1984
|
for (const word of contentTokens) {
|
|
1898
|
-
if (promptTokenSet.has(word))
|
|
1899
|
-
score += 0.5 * idf(word);
|
|
1900
|
-
}
|
|
1985
|
+
if (promptTokenSet.has(word)) score += 0.5 * idf(word);
|
|
1901
1986
|
}
|
|
1902
1987
|
const promptBigrams = bigrams(promptTokens);
|
|
1903
1988
|
const descBigrams = bigrams(descTokens);
|
|
1904
1989
|
for (const bg of descBigrams) {
|
|
1905
1990
|
if (promptBigrams.has(bg)) score += 5;
|
|
1906
1991
|
}
|
|
1907
|
-
if (score > 0) {
|
|
1908
|
-
scored.push({ skill, score });
|
|
1909
|
-
}
|
|
1992
|
+
if (score > 0) scored.push({ skill, score });
|
|
1910
1993
|
}
|
|
1911
1994
|
return scored.sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => s.skill);
|
|
1912
1995
|
}
|
|
1913
|
-
/**
|
|
1914
|
-
* Build a lightweight prompt section with skill descriptions only.
|
|
1915
|
-
* Full skill content is loaded on-demand via the skill_invoke tool.
|
|
1916
|
-
*/
|
|
1917
1996
|
buildSkillDescriptions() {
|
|
1918
1997
|
const skills = this.getAll().filter((s) => !s.disableModelInvocation).sort((a, b) => {
|
|
1919
1998
|
if (a.metadata.always && !b.metadata.always) return -1;
|
|
@@ -1942,9 +2021,7 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
1942
2021
|
}
|
|
1943
2022
|
return prompt;
|
|
1944
2023
|
}
|
|
1945
|
-
/**
|
|
1946
|
-
* @deprecated Use buildSkillDescriptions() + skill_invoke tool instead.
|
|
1947
|
-
*/
|
|
2024
|
+
/** @deprecated Use buildSkillDescriptions() + skill_invoke tool instead. */
|
|
1948
2025
|
buildSkillPrompt(taskPrompt) {
|
|
1949
2026
|
const relevant = this.findRelevant(taskPrompt);
|
|
1950
2027
|
if (relevant.length === 0) return "";
|
|
@@ -1961,40 +2038,23 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
1961
2038
|
}
|
|
1962
2039
|
return prompt;
|
|
1963
2040
|
}
|
|
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
2041
|
async create(name, description, content, options) {
|
|
1972
2042
|
if (!this.userId) return null;
|
|
1973
2043
|
try {
|
|
1974
|
-
const
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
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
|
-
}
|
|
2044
|
+
const metadata = options?.emoji ? { openclaw: { emoji: options.emoji } } : {};
|
|
2045
|
+
const data = await callMcpHandler(
|
|
2046
|
+
"skill.create",
|
|
2047
|
+
{
|
|
2048
|
+
name,
|
|
2049
|
+
description,
|
|
2050
|
+
content,
|
|
2051
|
+
version: "1.0.0",
|
|
2052
|
+
source: options?.source || "manual",
|
|
2053
|
+
emoji: options?.emoji || null,
|
|
2054
|
+
keywords: options?.keywords || [],
|
|
2055
|
+
metadata
|
|
2056
|
+
}
|
|
2057
|
+
);
|
|
1998
2058
|
const row = Array.isArray(data) ? data[0] : data;
|
|
1999
2059
|
if (!row) {
|
|
2000
2060
|
log.debug(`Skill create returned no data for "${name}"`);
|
|
@@ -2009,45 +2069,15 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2009
2069
|
return null;
|
|
2010
2070
|
}
|
|
2011
2071
|
}
|
|
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
2072
|
async addSkill(skillId) {
|
|
2018
2073
|
if (!this.userId) return null;
|
|
2019
2074
|
try {
|
|
2020
|
-
const
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
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;
|
|
2075
|
+
const result = await callMcpHandler(
|
|
2076
|
+
"skill.fetch_and_add",
|
|
2077
|
+
{ skill_id: skillId }
|
|
2078
|
+
);
|
|
2079
|
+
const row = result.skill;
|
|
2080
|
+
const agentSkillRow = result.agent_skill && typeof result.agent_skill === "object" ? result.agent_skill : row;
|
|
2051
2081
|
const skill = this.rowToSkill({
|
|
2052
2082
|
...agentSkillRow,
|
|
2053
2083
|
name: row.name,
|
|
@@ -2064,9 +2094,6 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2064
2094
|
return null;
|
|
2065
2095
|
}
|
|
2066
2096
|
}
|
|
2067
|
-
/**
|
|
2068
|
-
* Remove a skill from user's collection (soft-delete in agent_skills).
|
|
2069
|
-
*/
|
|
2070
2097
|
remove(name) {
|
|
2071
2098
|
const skill = this.skills.get(name);
|
|
2072
2099
|
if (!skill) return false;
|
|
@@ -2075,23 +2102,15 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2075
2102
|
});
|
|
2076
2103
|
return true;
|
|
2077
2104
|
}
|
|
2078
|
-
/**
|
|
2079
|
-
* List skills with summary info.
|
|
2080
|
-
*/
|
|
2081
2105
|
listFormatted() {
|
|
2082
2106
|
const skills = this.getAll();
|
|
2083
|
-
if (skills.length === 0)
|
|
2084
|
-
return "No skills in your collection.";
|
|
2085
|
-
}
|
|
2107
|
+
if (skills.length === 0) return "No skills in your collection.";
|
|
2086
2108
|
return skills.map((s) => {
|
|
2087
2109
|
const emoji = s.metadata.emoji || "";
|
|
2088
2110
|
return ` ${emoji ? emoji + " " : ""}${s.name} (${s.version}) [${s.source}]
|
|
2089
2111
|
${s.description}`;
|
|
2090
2112
|
}).join("\n\n");
|
|
2091
2113
|
}
|
|
2092
|
-
/**
|
|
2093
|
-
* Check if a skill with a similar name already exists in user's collection.
|
|
2094
|
-
*/
|
|
2095
2114
|
findSimilar(name) {
|
|
2096
2115
|
if (this.skills.has(name)) return this.skills.get(name);
|
|
2097
2116
|
const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -2112,10 +2131,6 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2112
2131
|
}
|
|
2113
2132
|
return null;
|
|
2114
2133
|
}
|
|
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
2134
|
update(name, newContent, description) {
|
|
2120
2135
|
const skill = this.skills.get(name);
|
|
2121
2136
|
if (!skill) return false;
|
|
@@ -2131,63 +2146,41 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2131
2146
|
}).catch(() => {
|
|
2132
2147
|
});
|
|
2133
2148
|
if (skill.sourceSkillId && this.userId) {
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
})
|
|
2149
|
+
callMcpHandler("skill.update_source", {
|
|
2150
|
+
source_skill_id: skill.sourceSkillId,
|
|
2151
|
+
content: newContent,
|
|
2152
|
+
description: newDescription,
|
|
2153
|
+
version: newVersion
|
|
2154
|
+
}).catch(() => {
|
|
2155
|
+
});
|
|
2141
2156
|
}
|
|
2142
2157
|
log.info(`Skill "${name}" updated to v${newVersion}`);
|
|
2143
2158
|
return true;
|
|
2144
2159
|
}
|
|
2145
2160
|
// ── 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
2161
|
async syncToAgentSkills(name, description, content, version2, options) {
|
|
2151
2162
|
if (!this.userId) return;
|
|
2152
2163
|
try {
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
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
|
|
2164
|
+
const data = await callMcpHandler("skill.upsert", {
|
|
2165
|
+
name,
|
|
2166
|
+
description,
|
|
2167
|
+
content,
|
|
2168
|
+
version: version2,
|
|
2169
|
+
source: options?.source || "manual",
|
|
2170
|
+
emoji: options?.emoji || null,
|
|
2171
|
+
keywords: options?.keywords || [],
|
|
2172
|
+
change_summary: options?.changeSummary || null,
|
|
2173
|
+
source_skill_id: options?.sourceSkillId || null
|
|
2170
2174
|
});
|
|
2171
|
-
if (error) {
|
|
2172
|
-
log.debug(`DB skill sync failed for "${name}": ${error.message}`);
|
|
2173
|
-
return;
|
|
2174
|
-
}
|
|
2175
2175
|
const skill = this.skills.get(name);
|
|
2176
2176
|
if (skill && data && typeof data === "object" && "id" in data) {
|
|
2177
2177
|
skill.dbId = data.id;
|
|
2178
2178
|
}
|
|
2179
|
-
if (skill && options?.variables) {
|
|
2180
|
-
skill.variables = options.variables;
|
|
2181
|
-
}
|
|
2182
2179
|
log.debug(`Skill "${name}" synced to agent_skills`);
|
|
2183
2180
|
} catch (err) {
|
|
2184
2181
|
log.debug(`DB skill sync error for "${name}": ${err}`);
|
|
2185
2182
|
}
|
|
2186
2183
|
}
|
|
2187
|
-
/**
|
|
2188
|
-
* Log a skill invocation to agent_skill_invocations.
|
|
2189
|
-
* Fire-and-forget: failures don't block task execution.
|
|
2190
|
-
*/
|
|
2191
2184
|
async logInvocation(skillName, options) {
|
|
2192
2185
|
if (!this.userId) return;
|
|
2193
2186
|
const skill = this.skills.get(skillName);
|
|
@@ -2197,39 +2190,24 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2197
2190
|
return;
|
|
2198
2191
|
}
|
|
2199
2192
|
try {
|
|
2200
|
-
|
|
2201
|
-
const { error } = await sb.from("agent_skill_invocations").insert({
|
|
2193
|
+
await callMcpHandler("skill.log_invocation", {
|
|
2202
2194
|
skill_id: skillDbId,
|
|
2203
|
-
user_id: this.userId,
|
|
2204
2195
|
message_id: options?.messageId || null,
|
|
2205
2196
|
session_id: options?.sessionId || null,
|
|
2206
2197
|
task_prompt: options?.taskPrompt?.slice(0, 500) || null,
|
|
2207
2198
|
arguments: options?.arguments || null,
|
|
2208
2199
|
success: options?.success ?? null
|
|
2209
2200
|
});
|
|
2210
|
-
if (error) {
|
|
2211
|
-
log.debug(`Invocation log failed: ${error.message}`);
|
|
2212
|
-
}
|
|
2213
2201
|
} catch (err) {
|
|
2214
2202
|
log.debug(`Invocation log error: ${err}`);
|
|
2215
2203
|
}
|
|
2216
2204
|
}
|
|
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
2205
|
async searchDb(query2, limit = 10) {
|
|
2222
2206
|
if (this.userId) {
|
|
2223
2207
|
try {
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
p_query: query2,
|
|
2228
|
-
p_limit: limit
|
|
2229
|
-
});
|
|
2230
|
-
if (!error && data) {
|
|
2231
|
-
const rows = data;
|
|
2232
|
-
return rows.map((row) => ({
|
|
2208
|
+
const data = await callMcpHandler("skill.search", { query: query2, limit });
|
|
2209
|
+
if (data) {
|
|
2210
|
+
return data.map((row) => ({
|
|
2233
2211
|
name: row.name,
|
|
2234
2212
|
description: row.description || "",
|
|
2235
2213
|
emoji: row.emoji || "",
|
|
@@ -2249,21 +2227,14 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2249
2227
|
invocationCount: 0
|
|
2250
2228
|
}));
|
|
2251
2229
|
}
|
|
2252
|
-
/**
|
|
2253
|
-
* Remove a skill from DB (soft-delete).
|
|
2254
|
-
*/
|
|
2255
2230
|
async removeFromDb(name) {
|
|
2256
2231
|
if (!this.userId) return;
|
|
2257
2232
|
try {
|
|
2258
|
-
|
|
2259
|
-
await sb.from("agent_skills").update({ is_active: false }).eq("user_id", this.userId).eq("name", name);
|
|
2233
|
+
await callMcpHandler("skill.remove", { name });
|
|
2260
2234
|
} catch {
|
|
2261
2235
|
}
|
|
2262
2236
|
}
|
|
2263
2237
|
// ── Marketplace ────────────────────────────────────────────────────
|
|
2264
|
-
/**
|
|
2265
|
-
* Publish a skill to the marketplace (sets is_public = true in skills table).
|
|
2266
|
-
*/
|
|
2267
2238
|
async publish(name, options) {
|
|
2268
2239
|
if (!this.userId) return null;
|
|
2269
2240
|
const skill = this.skills.get(name);
|
|
@@ -2273,31 +2244,21 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2273
2244
|
return null;
|
|
2274
2245
|
}
|
|
2275
2246
|
try {
|
|
2276
|
-
const
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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
|
-
}
|
|
2247
|
+
const data = await callMcpHandler("skill.publish", {
|
|
2248
|
+
name: skill.name,
|
|
2249
|
+
description: skill.description,
|
|
2250
|
+
version: skill.version,
|
|
2251
|
+
emoji: skill.metadata.emoji || null,
|
|
2252
|
+
content: skill.content,
|
|
2253
|
+
argument_hint: skill.argumentHint || null,
|
|
2254
|
+
keywords: skill.keywords,
|
|
2255
|
+
allowed_tools: skill.allowedTools,
|
|
2256
|
+
author_name: options?.authorName || null,
|
|
2257
|
+
metadata: skill.metadata,
|
|
2258
|
+
homepage: skill.homepage || null,
|
|
2259
|
+
category: options?.category || null,
|
|
2260
|
+
source: skill.source
|
|
2261
|
+
});
|
|
2301
2262
|
log.info(`Skill "${name}" published to marketplace`);
|
|
2302
2263
|
return data;
|
|
2303
2264
|
} catch (err) {
|
|
@@ -2305,21 +2266,16 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2305
2266
|
return null;
|
|
2306
2267
|
}
|
|
2307
2268
|
}
|
|
2308
|
-
/**
|
|
2309
|
-
* Browse marketplace skills (public + active).
|
|
2310
|
-
*/
|
|
2311
2269
|
async browse(options) {
|
|
2312
2270
|
try {
|
|
2313
|
-
const
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
p_offset: options?.offset || 0
|
|
2271
|
+
const data = await callMcpHandler("skill.browse", {
|
|
2272
|
+
query: options?.query || null,
|
|
2273
|
+
category: options?.category || null,
|
|
2274
|
+
sort: options?.sort || "popular",
|
|
2275
|
+
limit: options?.limit || 20,
|
|
2276
|
+
offset: options?.offset || 0
|
|
2320
2277
|
});
|
|
2321
|
-
|
|
2322
|
-
return data.map((r) => ({
|
|
2278
|
+
return (data || []).map((r) => ({
|
|
2323
2279
|
id: r.id,
|
|
2324
2280
|
name: r.name,
|
|
2325
2281
|
description: r.description || "",
|
|
@@ -2335,54 +2291,7 @@ _(${skills.length - included} additional skills available \u2014 use skill_invok
|
|
|
2335
2291
|
return [];
|
|
2336
2292
|
}
|
|
2337
2293
|
}
|
|
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
2294
|
};
|
|
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
|
-
});
|
|
2385
|
-
}
|
|
2386
2295
|
function substituteArguments(content, args) {
|
|
2387
2296
|
const parts = args.split(/\s+/);
|
|
2388
2297
|
content = content.replace(/\$ARGUMENTS/g, args);
|
|
@@ -2443,7 +2352,7 @@ import { z } from "zod/v4";
|
|
|
2443
2352
|
|
|
2444
2353
|
// src/tools/filesystem.ts
|
|
2445
2354
|
import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
|
|
2446
|
-
import { resolve, relative, join as
|
|
2355
|
+
import { resolve, relative, join as join3 } from "path";
|
|
2447
2356
|
import { glob } from "glob";
|
|
2448
2357
|
function assertWithinWorkspace(filePath) {
|
|
2449
2358
|
const config = getConfig();
|
|
@@ -2480,7 +2389,7 @@ async function searchFiles(pattern, directory) {
|
|
|
2480
2389
|
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
2481
2390
|
});
|
|
2482
2391
|
if (matches.length === 0) return "No files found matching the pattern.";
|
|
2483
|
-
return matches.slice(0, 50).map((m) => relative(config.workspacePath,
|
|
2392
|
+
return matches.slice(0, 50).map((m) => relative(config.workspacePath, join3(cwd, m))).join("\n");
|
|
2484
2393
|
}
|
|
2485
2394
|
async function listDirectory(path) {
|
|
2486
2395
|
const config = getConfig();
|
|
@@ -2490,7 +2399,7 @@ async function listDirectory(path) {
|
|
|
2490
2399
|
for (const entry of entries) {
|
|
2491
2400
|
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
2492
2401
|
const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
2493
|
-
const info = entry.isFile() ? await stat(
|
|
2402
|
+
const info = entry.isFile() ? await stat(join3(resolved, entry.name)).then(
|
|
2494
2403
|
(s) => ` (${formatSize(s.size)})`
|
|
2495
2404
|
) : "";
|
|
2496
2405
|
results.push(`${icon} ${entry.name}${info}`);
|
|
@@ -2514,11 +2423,11 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
2514
2423
|
const results = [];
|
|
2515
2424
|
for (const file of files.slice(0, 200)) {
|
|
2516
2425
|
try {
|
|
2517
|
-
const content = await readFile(
|
|
2426
|
+
const content = await readFile(join3(cwd, file), "utf-8");
|
|
2518
2427
|
const lines = content.split("\n");
|
|
2519
2428
|
for (let i = 0; i < lines.length; i++) {
|
|
2520
2429
|
if (regex.test(lines[i])) {
|
|
2521
|
-
const relPath = relative(config.workspacePath,
|
|
2430
|
+
const relPath = relative(config.workspacePath, join3(cwd, file));
|
|
2522
2431
|
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
2523
2432
|
regex.lastIndex = 0;
|
|
2524
2433
|
if (results.length >= 30) break;
|
|
@@ -3044,24 +2953,12 @@ function createAgentToolsServer(deps) {
|
|
|
3044
2953
|
),
|
|
3045
2954
|
tool(
|
|
3046
2955
|
"skill_create",
|
|
3047
|
-
"Create a new skill and add it to the user's collection. Returns the skill ID on success.
|
|
2956
|
+
"Create a new skill and add it to the user's collection. Returns the skill ID on success.",
|
|
3048
2957
|
{
|
|
3049
2958
|
name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
|
|
3050
2959
|
description: z.string().describe("One-line description of what this skill does"),
|
|
3051
|
-
instructions: z.string().describe(
|
|
3052
|
-
|
|
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
|
-
)
|
|
2960
|
+
instructions: z.string().describe("Markdown step-by-step instructions"),
|
|
2961
|
+
emoji: z.string().optional().describe("Single emoji representing this skill")
|
|
3065
2962
|
},
|
|
3066
2963
|
async (args) => {
|
|
3067
2964
|
const existing = skillManager.findSimilar(args.name);
|
|
@@ -3079,11 +2976,7 @@ function createAgentToolsServer(deps) {
|
|
|
3079
2976
|
args.name,
|
|
3080
2977
|
args.description,
|
|
3081
2978
|
args.instructions,
|
|
3082
|
-
{
|
|
3083
|
-
source: "manual",
|
|
3084
|
-
emoji: args.emoji,
|
|
3085
|
-
variables: args.variables
|
|
3086
|
-
}
|
|
2979
|
+
{ source: "manual", emoji: args.emoji }
|
|
3087
2980
|
);
|
|
3088
2981
|
if (!result) {
|
|
3089
2982
|
return {
|
|
@@ -3098,30 +2991,17 @@ function createAgentToolsServer(deps) {
|
|
|
3098
2991
|
{
|
|
3099
2992
|
source: "manual",
|
|
3100
2993
|
emoji: args.emoji,
|
|
3101
|
-
sourceSkillId: result.id
|
|
3102
|
-
variables: args.variables
|
|
2994
|
+
sourceSkillId: result.id
|
|
3103
2995
|
}
|
|
3104
2996
|
);
|
|
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
2997
|
log.success(`Skill "${args.name}" created and added to collection`);
|
|
3123
2998
|
return {
|
|
3124
|
-
content: [
|
|
2999
|
+
content: [
|
|
3000
|
+
{
|
|
3001
|
+
type: "text",
|
|
3002
|
+
text: `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`
|
|
3003
|
+
}
|
|
3004
|
+
]
|
|
3125
3005
|
};
|
|
3126
3006
|
}
|
|
3127
3007
|
),
|
|
@@ -3193,7 +3073,6 @@ Use \`skill_configure\` to set these values for the user, or ask the user to pro
|
|
|
3193
3073
|
};
|
|
3194
3074
|
}
|
|
3195
3075
|
let content = skill.content;
|
|
3196
|
-
content = substituteVariables(content, skill.config, skill.variables);
|
|
3197
3076
|
if (args.arguments) {
|
|
3198
3077
|
content = substituteArguments(content, args.arguments);
|
|
3199
3078
|
}
|
|
@@ -3212,18 +3091,6 @@ ${content}`;
|
|
|
3212
3091
|
**Allowed tools for this skill:** ${skill.allowedTools.join(", ")}
|
|
3213
3092
|
`;
|
|
3214
3093
|
}
|
|
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
3094
|
log.info(`Skill invoked: "${args.name}"`);
|
|
3228
3095
|
skillManager.logInvocation(args.name, {
|
|
3229
3096
|
messageId: taskId,
|
|
@@ -3263,53 +3130,6 @@ ${content}`;
|
|
|
3263
3130
|
return { content: [{ type: "text", text: response }] };
|
|
3264
3131
|
}
|
|
3265
3132
|
),
|
|
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
3133
|
tool(
|
|
3314
3134
|
"skill_generate",
|
|
3315
3135
|
"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 +3178,6 @@ ${content}`;
|
|
|
3358
3178
|
response += `- instructions: detailed step-by-step markdown instructions the agent can follow
|
|
3359
3179
|
`;
|
|
3360
3180
|
response += `- emoji: a single emoji representing the skill
|
|
3361
|
-
`;
|
|
3362
|
-
response += `- variables: define user-specific data needed by the skill (see below)
|
|
3363
3181
|
|
|
3364
3182
|
`;
|
|
3365
3183
|
response += `skill_create automatically adds the skill to the user's collection \u2014 no need to call skill_add.
|
|
@@ -3376,59 +3194,9 @@ ${content}`;
|
|
|
3376
3194
|
`;
|
|
3377
3195
|
response += `- Include error handling steps
|
|
3378
3196
|
`;
|
|
3379
|
-
response += `-
|
|
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)",
|
|
3197
|
+
response += `- Use placeholders like {query}, {date} for variable inputs
|
|
3403
3198
|
`;
|
|
3404
|
-
response +=
|
|
3405
|
-
`;
|
|
3406
|
-
response += ` "required": true,
|
|
3407
|
-
`;
|
|
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).
|
|
3199
|
+
response += `- Each skill should be a single, well-defined workflow (10-25 steps)
|
|
3432
3200
|
`;
|
|
3433
3201
|
return { content: [{ type: "text", text: response }] };
|
|
3434
3202
|
}
|
|
@@ -3721,8 +3489,10 @@ ${content}`;
|
|
|
3721
3489
|
const tz = args.timezone || "UTC";
|
|
3722
3490
|
try {
|
|
3723
3491
|
const task = await createScheduledTask(userId, name, prompt, args.cron, tz);
|
|
3724
|
-
|
|
3725
|
-
|
|
3492
|
+
await callMcpHandler("schedule.link_job", {
|
|
3493
|
+
task_id: task.id,
|
|
3494
|
+
job_id: job.jobId
|
|
3495
|
+
});
|
|
3726
3496
|
const nextRun = task.next_run_at ? new Date(task.next_run_at).toLocaleString() : "calculating...";
|
|
3727
3497
|
let response = `## Job Scheduled: ${args.job_name}
|
|
3728
3498
|
|
|
@@ -3825,20 +3595,14 @@ ${content}`;
|
|
|
3825
3595
|
]
|
|
3826
3596
|
});
|
|
3827
3597
|
}
|
|
3828
|
-
async function saveJobToDb(
|
|
3598
|
+
async function saveJobToDb(_userId, jobName, jobDescription, createdSkillNames) {
|
|
3829
3599
|
try {
|
|
3830
|
-
const
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
p_job_description: jobDescription,
|
|
3835
|
-
p_skill_names: createdSkillNames
|
|
3600
|
+
const data = await callMcpHandler("job.save_with_skills", {
|
|
3601
|
+
job_name: jobName,
|
|
3602
|
+
job_description: jobDescription,
|
|
3603
|
+
skill_names: createdSkillNames
|
|
3836
3604
|
});
|
|
3837
|
-
|
|
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`);
|
|
3605
|
+
log.debug(`Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`);
|
|
3842
3606
|
} catch (err) {
|
|
3843
3607
|
log.debug(`saveJobToDb error: ${err}`);
|
|
3844
3608
|
}
|
|
@@ -4359,9 +4123,8 @@ import chalk5 from "chalk";
|
|
|
4359
4123
|
function registerStatusCommand(program2) {
|
|
4360
4124
|
program2.command("status").description("Check the status of the current agent session").action(async () => {
|
|
4361
4125
|
try {
|
|
4362
|
-
|
|
4363
|
-
const
|
|
4364
|
-
const { data: sessions } = await sb.from("agent_sessions").select("*").eq("user_id", userId).in("status", ["online", "busy"]).order("started_at", { ascending: false }).limit(5);
|
|
4126
|
+
await getCurrentUserId();
|
|
4127
|
+
const sessions = await getActiveSessions(5);
|
|
4365
4128
|
if (!sessions || sessions.length === 0) {
|
|
4366
4129
|
console.log(chalk5.yellow("No active sessions found."));
|
|
4367
4130
|
console.log('Run "assistme" to begin a new session.');
|
|
@@ -4742,7 +4505,7 @@ function registerJobCommands(program2) {
|
|
|
4742
4505
|
jobCmd.command("list").description("List your defined jobs").action(async () => {
|
|
4743
4506
|
try {
|
|
4744
4507
|
const userId = await getCurrentUserId();
|
|
4745
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4508
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
|
|
4746
4509
|
const runner = new JobRunner2(userId);
|
|
4747
4510
|
const jobs = await runner.listJobs();
|
|
4748
4511
|
if (jobs.length === 0) {
|
|
@@ -4770,7 +4533,7 @@ function registerJobCommands(program2) {
|
|
|
4770
4533
|
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) => {
|
|
4771
4534
|
try {
|
|
4772
4535
|
const userId = await getCurrentUserId();
|
|
4773
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4536
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
|
|
4774
4537
|
const runner = new JobRunner2(userId);
|
|
4775
4538
|
const runs = await runner.getRunHistory(
|
|
4776
4539
|
name,
|
|
@@ -4824,7 +4587,7 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
|
4824
4587
|
process.exit(1);
|
|
4825
4588
|
}
|
|
4826
4589
|
const userId = await getCurrentUserId();
|
|
4827
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4590
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
|
|
4828
4591
|
const runner = new JobRunner2(userId);
|
|
4829
4592
|
const job = await runner.loadJob(name);
|
|
4830
4593
|
if (!job) {
|
|
@@ -4846,8 +4609,10 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
|
4846
4609
|
opts.cron,
|
|
4847
4610
|
tz
|
|
4848
4611
|
);
|
|
4849
|
-
|
|
4850
|
-
|
|
4612
|
+
await callMcpHandler("schedule.link_job", {
|
|
4613
|
+
task_id: task.id,
|
|
4614
|
+
job_id: job.jobId
|
|
4615
|
+
});
|
|
4851
4616
|
log.success(`Job "${name}" scheduled: ${opts.cron} (${tz})`);
|
|
4852
4617
|
console.log(` Skills: ${job.skills.length}`);
|
|
4853
4618
|
console.log(
|