openclaw-app 1.1.5 → 1.1.7
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/index.ts +199 -16
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -563,8 +563,9 @@ function cleanupRelay(state: RelayState) {
|
|
|
563
563
|
state.ws = null;
|
|
564
564
|
}
|
|
565
565
|
state.e2eSessions.clear();
|
|
566
|
-
|
|
567
|
-
|
|
566
|
+
// prevE2eSessions is intentionally kept across reconnects so that
|
|
567
|
+
// offline-buffered messages can still be re-encrypted after a plugin
|
|
568
|
+
// reconnect. pendingFlushQueue is also kept for the same reason.
|
|
568
569
|
}
|
|
569
570
|
|
|
570
571
|
function connectRelay(ctx: any, account: ResolvedAccount) {
|
|
@@ -929,12 +930,51 @@ async function handleRpc(ctx: any, accountId: string, msg: any): Promise<boolean
|
|
|
929
930
|
agentsSample: Array.isArray(agentsList) ? agentsList.slice(0, 3) : agentsList,
|
|
930
931
|
});
|
|
931
932
|
} else if (method === "runtime.inspect.deep") {
|
|
932
|
-
//
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const
|
|
937
|
-
|
|
933
|
+
// Deep inspection to discover skills API surface
|
|
934
|
+
const allRuntimeKeys = runtime ? Object.keys(runtime) : [];
|
|
935
|
+
|
|
936
|
+
// Walk every top-level key and record its type + sub-keys (if object/function)
|
|
937
|
+
const runtimeShape: Record<string, any> = {};
|
|
938
|
+
for (const k of allRuntimeKeys) {
|
|
939
|
+
try {
|
|
940
|
+
const v = runtime[k];
|
|
941
|
+
const t = typeof v;
|
|
942
|
+
if (t === 'function') {
|
|
943
|
+
runtimeShape[k] = 'function';
|
|
944
|
+
} else if (v && t === 'object') {
|
|
945
|
+
runtimeShape[k] = Object.keys(v).slice(0, 20);
|
|
946
|
+
} else {
|
|
947
|
+
runtimeShape[k] = t;
|
|
948
|
+
}
|
|
949
|
+
} catch (_) {
|
|
950
|
+
runtimeShape[k] = 'error';
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Specifically probe skills-related paths
|
|
955
|
+
const skillsProbe: Record<string, any> = {};
|
|
956
|
+
const candidates = ['skills', 'skillManager', 'skillLoader', 'skillRegistry', 'pluginSkills'];
|
|
957
|
+
for (const c of candidates) {
|
|
958
|
+
if (runtime?.[c]) {
|
|
959
|
+
skillsProbe[c] = typeof runtime[c] === 'object'
|
|
960
|
+
? Object.keys(runtime[c])
|
|
961
|
+
: typeof runtime[c];
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Also probe config.skills
|
|
966
|
+
let cfgSkills: any = null;
|
|
967
|
+
try {
|
|
968
|
+
const cfg = runtime.config.loadConfig();
|
|
969
|
+
cfgSkills = {
|
|
970
|
+
topKeys: Object.keys(cfg?.skills ?? {}),
|
|
971
|
+
entriesKeys: Object.keys(cfg?.skills?.entries ?? {}),
|
|
972
|
+
entriesSample: Object.entries(cfg?.skills?.entries ?? {}).slice(0, 5)
|
|
973
|
+
.map(([k, v]: [string, any]) => ({ name: k, enabled: v?.enabled })),
|
|
974
|
+
};
|
|
975
|
+
} catch (_) {}
|
|
976
|
+
|
|
977
|
+
await sendRpcReply({ runtimeShape, skillsProbe, cfgSkills });
|
|
938
978
|
} else if (method === "agents.list") {
|
|
939
979
|
const cfg = runtime.config.loadConfig();
|
|
940
980
|
// cfg.agents structure: { defaults: {...}, list: [{id, name, workspace, ...}], ... }
|
|
@@ -957,9 +997,10 @@ async function handleRpc(ctx: any, accountId: string, msg: any): Promise<boolean
|
|
|
957
997
|
})).filter((a: any) => a.id);
|
|
958
998
|
await sendRpcReply({ agents: list });
|
|
959
999
|
} else if (method === "sessions.list") {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1000
|
+
// Sessions are stored locally in the mobile app's Drift DB.
|
|
1001
|
+
// The Gateway/CLI has no sessions API we can call reliably.
|
|
1002
|
+
// Return empty — session_provider.dart falls back to local DB which is the source of truth.
|
|
1003
|
+
await sendRpcReply({ sessions: [] });
|
|
963
1004
|
} else if (method === "agents.create") {
|
|
964
1005
|
// Use CLI: openclaw agents add <name> --workspace <path> [--model <model>]
|
|
965
1006
|
const name = params.name as string | undefined;
|
|
@@ -979,6 +1020,119 @@ async function handleRpc(ctx: any, accountId: string, msg: any): Promise<boolean
|
|
|
979
1020
|
await sendRpcReply({ name, workspace, output: result.stdout ?? '' });
|
|
980
1021
|
}
|
|
981
1022
|
}
|
|
1023
|
+
} else if (method === "tools.list") {
|
|
1024
|
+
// Use `openclaw skills list --json` CLI — most reliable source for loaded skills.
|
|
1025
|
+
// Falls back to fs scan of ~/.openclaw/skills if CLI is unavailable.
|
|
1026
|
+
let tools: any[] = [];
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
// Try object form first; fall back to string form (same as agents.create)
|
|
1030
|
+
let cliResult: any;
|
|
1031
|
+
try {
|
|
1032
|
+
cliResult = await runtime.system.runCommandWithTimeout(
|
|
1033
|
+
{ cmd: 'openclaw', args: ['skills', 'list', '--json'] },
|
|
1034
|
+
10000
|
|
1035
|
+
);
|
|
1036
|
+
} catch (_) {
|
|
1037
|
+
cliResult = await runtime.system.runCommandWithTimeout(
|
|
1038
|
+
'openclaw skills list --json',
|
|
1039
|
+
10000
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
if (cliResult.exitCode === 0 && cliResult.stdout?.trim()) {
|
|
1043
|
+
const parsed = JSON.parse(cliResult.stdout.trim());
|
|
1044
|
+
const raw: any[] = Array.isArray(parsed) ? parsed
|
|
1045
|
+
: Array.isArray(parsed?.skills) ? parsed.skills
|
|
1046
|
+
: parsed?.data ?? [];
|
|
1047
|
+
tools = raw
|
|
1048
|
+
.filter((s: any) => s['user-invocable'] !== false && s['user-invocable'] !== 'false')
|
|
1049
|
+
.map((s: any) => ({
|
|
1050
|
+
name: s.name ?? s.id ?? '',
|
|
1051
|
+
description: s.description ?? s.desc ?? '',
|
|
1052
|
+
category: s.category ?? '',
|
|
1053
|
+
userInvocable: true,
|
|
1054
|
+
}))
|
|
1055
|
+
.filter((t: any) => t.name);
|
|
1056
|
+
}
|
|
1057
|
+
} catch (_) {}
|
|
1058
|
+
|
|
1059
|
+
// Fallback: scan ~/.openclaw for all skills/ subdirs, deduplicated
|
|
1060
|
+
if (tools.length === 0) {
|
|
1061
|
+
try {
|
|
1062
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1063
|
+
const fs = (globalThis as any).require?.('fs') ?? require('fs');
|
|
1064
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1065
|
+
const path = (globalThis as any).require?.('path') ?? require('path');
|
|
1066
|
+
const cfg = runtime.config.loadConfig();
|
|
1067
|
+
const proc = (globalThis as any).process;
|
|
1068
|
+
const home = proc?.env?.HOME ?? proc?.env?.USERPROFILE ?? '~';
|
|
1069
|
+
const ocRoot = `${home}/.openclaw`;
|
|
1070
|
+
|
|
1071
|
+
// Collect all skills/ directories under ~/.openclaw recursively (depth 2)
|
|
1072
|
+
const skillDirs: string[] = [];
|
|
1073
|
+
const addSkillsDir = (dir: string) => {
|
|
1074
|
+
try {
|
|
1075
|
+
if (fs.existsSync(dir)) skillDirs.push(dir);
|
|
1076
|
+
} catch (_) {}
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// ~/.openclaw/skills
|
|
1080
|
+
addSkillsDir(`${ocRoot}/skills`);
|
|
1081
|
+
|
|
1082
|
+
// ~/.openclaw/*/skills (workspace dirs one level deep)
|
|
1083
|
+
try {
|
|
1084
|
+
for (const entry of fs.readdirSync(ocRoot)) {
|
|
1085
|
+
const sub = path.join(ocRoot, entry, 'skills');
|
|
1086
|
+
addSkillsDir(sub);
|
|
1087
|
+
}
|
|
1088
|
+
} catch (_) {}
|
|
1089
|
+
|
|
1090
|
+
// Extra dirs from config
|
|
1091
|
+
for (const d of (cfg?.skills?.load?.extraDirs ?? [])) addSkillsDir(d);
|
|
1092
|
+
|
|
1093
|
+
const seen = new Set<string>();
|
|
1094
|
+
for (const dir of skillDirs) {
|
|
1095
|
+
let entries: string[];
|
|
1096
|
+
try { entries = fs.readdirSync(dir); } catch (_) { continue; }
|
|
1097
|
+
for (const entry of entries) {
|
|
1098
|
+
const skillFile = path.join(dir, entry, 'SKILL.md');
|
|
1099
|
+
let content: string;
|
|
1100
|
+
try { content = fs.readFileSync(skillFile, 'utf8'); } catch (_) { continue; }
|
|
1101
|
+
const fm = _parseSkillFrontmatter(content);
|
|
1102
|
+
const name: string = fm.name ?? entry;
|
|
1103
|
+
if (!name || seen.has(name)) continue;
|
|
1104
|
+
seen.add(name);
|
|
1105
|
+
if (fm['user-invocable'] === false || fm['user-invocable'] === 'false') continue;
|
|
1106
|
+
if (cfg?.skills?.entries?.[name]?.enabled === false) continue;
|
|
1107
|
+
tools.push({ name, description: fm.description ?? '', category: '', userInvocable: true });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
} catch (_) {}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
await sendRpcReply({ tools });
|
|
1114
|
+
} else if (method === "tools.invoke") {
|
|
1115
|
+
// Invoke a skill by injecting a /skill <name> command into the chat session
|
|
1116
|
+
const toolName = params.name as string | undefined;
|
|
1117
|
+
const input = (params.params?.input ?? params.input ?? '') as string;
|
|
1118
|
+
if (!toolName) {
|
|
1119
|
+
await sendRpcReply(null, "tools.invoke: missing required param 'name'");
|
|
1120
|
+
} else {
|
|
1121
|
+
try {
|
|
1122
|
+
// Send /skill <name> [input] as a chat message to the current session
|
|
1123
|
+
const cmd = input ? `/skill ${toolName} ${input}` : `/skill ${toolName}`;
|
|
1124
|
+
const wireSessionKey = replySessionKey ?? 'rpc';
|
|
1125
|
+
await handleInbound(ctx, accountId, {
|
|
1126
|
+
type: 'message',
|
|
1127
|
+
content: cmd,
|
|
1128
|
+
sessionKey: wireSessionKey,
|
|
1129
|
+
chatSessionKey: wireSessionKey,
|
|
1130
|
+
});
|
|
1131
|
+
await sendRpcReply({ dispatched: true, command: cmd });
|
|
1132
|
+
} catch (e: any) {
|
|
1133
|
+
await sendRpcReply(null, `tools.invoke '${toolName}' error: ${e}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
982
1136
|
} else {
|
|
983
1137
|
await sendRpcReply(null, `Unknown RPC method: ${method}`);
|
|
984
1138
|
}
|
|
@@ -1025,13 +1179,17 @@ async function handleInbound(ctx: any, accountId: string, msg: any) {
|
|
|
1025
1179
|
});
|
|
1026
1180
|
|
|
1027
1181
|
// Derive an isolated Gateway session for each mobile conversation.
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
|
|
1182
|
+
//
|
|
1183
|
+
// chatSessionKey format from app: "agent:<agentId>:<uuid1>-<uuid2>"
|
|
1184
|
+
// We use this as the stable per-conversation identity on the Gateway side.
|
|
1185
|
+
// appSessionKey is the wire-level connection ID — used for E2E reply routing.
|
|
1032
1186
|
const appSessionKey = msg.sessionKey ? String(msg.sessionKey) : null;
|
|
1033
1187
|
const msgAgentId = msg.agentId ? String(msg.agentId) : null;
|
|
1034
|
-
|
|
1188
|
+
let sessionKey: string;
|
|
1189
|
+
if (chatSessionKey) {
|
|
1190
|
+
// chatSessionKey from app: "agent:<agentId>:<uuid>" — use as-is for Gateway routing
|
|
1191
|
+
sessionKey = chatSessionKey;
|
|
1192
|
+
} else if (appSessionKey) {
|
|
1035
1193
|
const agentId = msgAgentId || 'main';
|
|
1036
1194
|
sessionKey = `agent:${agentId}:mobile-${appSessionKey}`;
|
|
1037
1195
|
} else {
|
|
@@ -1204,3 +1362,28 @@ export default function register(api: any) {
|
|
|
1204
1362
|
|
|
1205
1363
|
api.logger?.info?.("[openclaw-app] Plugin registered");
|
|
1206
1364
|
}
|
|
1365
|
+
|
|
1366
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Parse YAML frontmatter from a SKILL.md string.
|
|
1370
|
+
* Returns a flat key→value map for simple scalar fields.
|
|
1371
|
+
* Handles: name, description, user-invocable, disable-model-invocation, command-dispatch.
|
|
1372
|
+
*/
|
|
1373
|
+
function _parseSkillFrontmatter(content: string): Record<string, any> {
|
|
1374
|
+
const result: Record<string, any> = {};
|
|
1375
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1376
|
+
if (!match) return result;
|
|
1377
|
+
for (const line of match[1].split('\n')) {
|
|
1378
|
+
const colon = line.indexOf(':');
|
|
1379
|
+
if (colon === -1) continue;
|
|
1380
|
+
const key = line.slice(0, colon).trim();
|
|
1381
|
+
const val = line.slice(colon + 1).trim();
|
|
1382
|
+
if (!key) continue;
|
|
1383
|
+
// Parse booleans
|
|
1384
|
+
if (val === 'true') result[key] = true;
|
|
1385
|
+
else if (val === 'false') result[key] = false;
|
|
1386
|
+
else result[key] = val;
|
|
1387
|
+
}
|
|
1388
|
+
return result;
|
|
1389
|
+
}
|
package/openclaw.plugin.json
CHANGED