openclaw-app 1.1.6 → 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 +187 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -930,12 +930,51 @@ async function handleRpc(ctx: any, accountId: string, msg: any): Promise<boolean
|
|
|
930
930
|
agentsSample: Array.isArray(agentsList) ? agentsList.slice(0, 3) : agentsList,
|
|
931
931
|
});
|
|
932
932
|
} else if (method === "runtime.inspect.deep") {
|
|
933
|
-
//
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
|
|
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 });
|
|
939
978
|
} else if (method === "agents.list") {
|
|
940
979
|
const cfg = runtime.config.loadConfig();
|
|
941
980
|
// cfg.agents structure: { defaults: {...}, list: [{id, name, workspace, ...}], ... }
|
|
@@ -958,9 +997,10 @@ async function handleRpc(ctx: any, accountId: string, msg: any): Promise<boolean
|
|
|
958
997
|
})).filter((a: any) => a.id);
|
|
959
998
|
await sendRpcReply({ agents: list });
|
|
960
999
|
} else if (method === "sessions.list") {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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: [] });
|
|
964
1004
|
} else if (method === "agents.create") {
|
|
965
1005
|
// Use CLI: openclaw agents add <name> --workspace <path> [--model <model>]
|
|
966
1006
|
const name = params.name as string | undefined;
|
|
@@ -980,6 +1020,119 @@ async function handleRpc(ctx: any, accountId: string, msg: any): Promise<boolean
|
|
|
980
1020
|
await sendRpcReply({ name, workspace, output: result.stdout ?? '' });
|
|
981
1021
|
}
|
|
982
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
|
+
}
|
|
983
1136
|
} else {
|
|
984
1137
|
await sendRpcReply(null, `Unknown RPC method: ${method}`);
|
|
985
1138
|
}
|
|
@@ -1208,4 +1361,29 @@ export default function register(api: any) {
|
|
|
1208
1361
|
});
|
|
1209
1362
|
|
|
1210
1363
|
api.logger?.info?.("[openclaw-app] Plugin registered");
|
|
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;
|
|
1211
1389
|
}
|
package/openclaw.plugin.json
CHANGED