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/chunk-TTEGHE2E.js +47 -0
- package/dist/chunk-UWE5WVQI.js +289 -0
- package/dist/config-PUIS2TQL.js +12 -0
- package/dist/index.js +818 -704
- 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 +111 -151
- package/src/agent/memory.test.ts +41 -65
- package/src/agent/memory.ts +33 -134
- package/src/agent/processor.ts +59 -17
- 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/skill-evaluator.ts +258 -0
- package/src/agent/skills.ts +191 -494
- 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();
|
|
@@ -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
|
-
|
|
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
|
}
|
|
1522
1680
|
/**
|
|
1523
1681
|
* Search memories by query text. Uses ILIKE + tag containment.
|
|
1524
1682
|
*/
|
|
1525
|
-
async search(
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
log.warn(`Memory search failed: ${
|
|
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
|
|
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,18 @@ var SkillManager = class {
|
|
|
1772
1902
|
skills = /* @__PURE__ */ new Map();
|
|
1773
1903
|
idfCache = /* @__PURE__ */ new Map();
|
|
1774
1904
|
userId = null;
|
|
1775
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
|
|
1837
|
-
config
|
|
1945
|
+
invocationCount: row.invocation_count || 0
|
|
1838
1946
|
};
|
|
1839
1947
|
}
|
|
1840
|
-
/**
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1915
|
-
*
|
|
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
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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;
|
|
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.
|
|
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
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
2225
|
-
|
|
2226
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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
|
-
|
|
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
|
|
2374
|
-
if (!
|
|
2375
|
-
return
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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: [
|
|
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 += `-
|
|
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 +=
|
|
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
|
|
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
|
-
|
|
3725
|
-
|
|
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(
|
|
3910
|
+
async function saveJobToDb(_userId, jobName, jobDescription, createdSkillNames) {
|
|
3829
3911
|
try {
|
|
3830
|
-
const
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
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
|
-
|
|
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
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
4366
|
-
const
|
|
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 (
|
|
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(
|
|
4682
|
+
const results = await mm.search(query3);
|
|
4571
4683
|
if (results.length === 0) {
|
|
4572
|
-
console.log(chalk7.yellow(`No memories matching "${
|
|
4684
|
+
console.log(chalk7.yellow(`No memories matching "${query3}"`));
|
|
4573
4685
|
return;
|
|
4574
4686
|
}
|
|
4575
4687
|
console.log(chalk7.bold(`
|
|
4576
|
-
Search results for "${
|
|
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 (
|
|
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(
|
|
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 "${
|
|
4763
|
+
console.log(chalk8.yellow(`No skills found for "${query3}"`));
|
|
4652
4764
|
return;
|
|
4653
4765
|
}
|
|
4654
4766
|
console.log(chalk8.bold(`
|
|
4655
|
-
Skills matching "${
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
4853
|
-
|
|
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(
|