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 CHANGED
@@ -563,8 +563,9 @@ function cleanupRelay(state: RelayState) {
563
563
  state.ws = null;
564
564
  }
565
565
  state.e2eSessions.clear();
566
- state.prevE2eSessions.clear();
567
- state.pendingFlushQueue.clear();
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
- // Inspect sub-objects to find agents.create capability
933
- const systemKeys = runtime?.system ? Object.keys(runtime.system) : [];
934
- const toolsKeys = runtime?.tools ? Object.keys(runtime.tools) : [];
935
- const stateKeys = runtime?.state ? Object.keys(runtime.state) : [];
936
- const agentsApiKeys = runtime?.agents ? Object.keys(runtime.agents) : [];
937
- await sendRpcReply({ systemKeys, toolsKeys, stateKeys, agentsApiKeys });
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
- const cfg = runtime.config.loadConfig();
961
- const sessions = runtime.session?.listSessions?.({ cfg, accountId }) ?? [];
962
- await sendRpcReply({ sessions });
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
- // If the app provides an agentId, route to that agent's session:
1029
- // agent:<agentId>:mobile-<appSessionKey>
1030
- // Otherwise fall back to the default main agent route.
1031
- let sessionKey: string;
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
- if (appSessionKey) {
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
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-app",
3
3
  "name": "OpenClaw App",
4
- "version": "1.1.5",
4
+ "version": "1.1.7",
5
5
  "description": "Mobile app channel for OpenClaw — chat via the OpenClaw App app through a Cloudflare Worker relay.",
6
6
  "channels": [
7
7
  "openclaw-app"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-app",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "OpenClaw App channel plugin — relay bridge for the OpenClaw App app",
5
5
  "main": "index.ts",
6
6
  "type": "module",