@alfe.ai/openclaw 0.4.10 → 0.4.12
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/README.md +10 -0
- package/dist/index.cjs +129 -50
- package/dist/index.d.cts +26 -22
- package/dist/index.d.ts +26 -22
- package/dist/index.js +128 -49
- package/dist/plugin2.cjs +622 -252
- package/dist/plugin2.js +623 -253
- package/openclaw.plugin.json +2 -0
- package/package.json +3 -3
package/dist/plugin2.cjs
CHANGED
|
@@ -31,15 +31,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
}) : target, mod));
|
|
32
32
|
//#endregion
|
|
33
33
|
let node_path = require("node:path");
|
|
34
|
-
let node_fs_promises = require("node:fs/promises");
|
|
35
34
|
let node_os = require("node:os");
|
|
36
35
|
let node_module = require("node:module");
|
|
37
36
|
let _alfe_ai_config = require("@alfe.ai/config");
|
|
38
37
|
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
39
|
-
let node_net = require("node:net");
|
|
40
38
|
let node_crypto = require("node:crypto");
|
|
41
39
|
let node_events = require("node:events");
|
|
40
|
+
let node_net = require("node:net");
|
|
41
|
+
let node_string_decoder = require("node:string_decoder");
|
|
42
42
|
let _auriclabs_logger = require("@auriclabs/logger");
|
|
43
|
+
let node_fs = require("node:fs");
|
|
44
|
+
let node_fs_promises = require("node:fs/promises");
|
|
43
45
|
//#region ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
44
46
|
/** Returns true if this value is an async iterator */
|
|
45
47
|
function IsAsyncIterator$2(value) {
|
|
@@ -2786,197 +2788,183 @@ const Type = /* @__PURE__ */ __exportAll({
|
|
|
2786
2788
|
//#endregion
|
|
2787
2789
|
//#region src/types.ts
|
|
2788
2790
|
function isIPCRequest(msg) {
|
|
2789
|
-
|
|
2791
|
+
if (!isRecord$1(msg)) return false;
|
|
2792
|
+
return msg.type === "req" && isBoundedString(msg.id, 128) && isBoundedString(msg.method, 256) && isRecord$1(msg.params);
|
|
2790
2793
|
}
|
|
2791
2794
|
function isIPCResponse(msg) {
|
|
2792
|
-
|
|
2795
|
+
if (!isRecord$1(msg)) return false;
|
|
2796
|
+
return isBoundedString(msg.id, 128) && typeof msg.ok === "boolean" && !("type" in msg) && (msg.error === void 0 || isIPCError(msg.error));
|
|
2793
2797
|
}
|
|
2794
2798
|
function isIPCEvent(msg) {
|
|
2795
|
-
|
|
2799
|
+
if (!isRecord$1(msg)) return false;
|
|
2800
|
+
return msg.type === "event" && isBoundedString(msg.event, 256);
|
|
2801
|
+
}
|
|
2802
|
+
function isIPCError(value) {
|
|
2803
|
+
return isRecord$1(value) && isBoundedString(value.code, 128) && isBoundedString(value.message, 4096);
|
|
2804
|
+
}
|
|
2805
|
+
function isBoundedString(value, maxLength) {
|
|
2806
|
+
return typeof value === "string" && value.length > 0 && value.length <= maxLength;
|
|
2807
|
+
}
|
|
2808
|
+
function isRecord$1(value) {
|
|
2809
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2796
2810
|
}
|
|
2797
2811
|
/** IPC protocol version — must match daemon's PROTOCOL_VERSION */
|
|
2798
2812
|
const PROTOCOL_VERSION = 1;
|
|
2799
2813
|
//#endregion
|
|
2800
2814
|
//#region src/ipc-client.ts
|
|
2801
2815
|
/**
|
|
2802
|
-
*
|
|
2803
|
-
*
|
|
2804
|
-
*
|
|
2805
|
-
* Request: { type: 'req', id, method, params }
|
|
2806
|
-
* Response: { id, ok, payload?, error? }
|
|
2807
|
-
* Event: { type: 'event', event, payload }
|
|
2808
|
-
*
|
|
2809
|
-
* Features:
|
|
2810
|
-
* - Automatic reconnection with exponential backoff (1s → 30s)
|
|
2811
|
-
* - Request/response correlation via message ID
|
|
2812
|
-
* - Event emission for incoming daemon events + requests
|
|
2813
|
-
* - Graceful handling if daemon not running (warns, retries, doesn't crash)
|
|
2816
|
+
* Reconnecting newline-delimited JSON client for the local Alfe gateway.
|
|
2817
|
+
* The socket is local, but it is still a bidirectional runtime boundary: both
|
|
2818
|
+
* peers cap frames, validate envelopes, and bind callbacks to one connection.
|
|
2814
2819
|
*/
|
|
2815
2820
|
const MIN_BACKOFF_MS = 1e3;
|
|
2816
2821
|
const MAX_BACKOFF_MS = 3e4;
|
|
2817
2822
|
const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
2823
|
+
const MAX_REQUEST_TIMEOUT_MS = 12e4;
|
|
2824
|
+
const MAX_PENDING_REQUESTS = 128;
|
|
2825
|
+
/** Must stay aligned with `packages/gateway/src/protocol.ts`. */
|
|
2826
|
+
const MAX_IPC_MESSAGE_BYTES = 1024 * 1024;
|
|
2818
2827
|
var IPCClient = class extends node_events.EventEmitter {
|
|
2819
2828
|
socket = null;
|
|
2820
|
-
socketPath;
|
|
2821
2829
|
buffer = "";
|
|
2830
|
+
decoder = new node_string_decoder.StringDecoder("utf8");
|
|
2822
2831
|
backoffMs = MIN_BACKOFF_MS;
|
|
2823
2832
|
closed = false;
|
|
2824
|
-
|
|
2833
|
+
isSocketConnected = false;
|
|
2825
2834
|
reconnectTimer = null;
|
|
2826
2835
|
pending = /* @__PURE__ */ new Map();
|
|
2827
|
-
log = (0, _auriclabs_logger.createLogger)("IpcClient")
|
|
2828
|
-
constructor(socketPath) {
|
|
2836
|
+
constructor(socketPath, log = (0, _auriclabs_logger.createLogger)("IpcClient")) {
|
|
2829
2837
|
super();
|
|
2830
2838
|
this.socketPath = socketPath;
|
|
2839
|
+
this.log = log;
|
|
2840
|
+
if (socketPath.length < 1 || socketPath.length > 4096 || socketPath.includes("\0")) throw new TypeError("IPC socket path must contain 1 to 4096 characters without NUL bytes");
|
|
2831
2841
|
}
|
|
2832
2842
|
get connected() {
|
|
2833
|
-
return this.
|
|
2843
|
+
return this.isSocketConnected;
|
|
2834
2844
|
}
|
|
2835
|
-
/**
|
|
2836
|
-
* Start the IPC connection with auto-reconnect.
|
|
2837
|
-
*/
|
|
2838
2845
|
start() {
|
|
2846
|
+
if (this.socket || this.reconnectTimer) return;
|
|
2839
2847
|
this.closed = false;
|
|
2840
|
-
this.
|
|
2848
|
+
this.openSocket();
|
|
2841
2849
|
}
|
|
2842
|
-
/**
|
|
2843
|
-
* Stop the IPC connection and all timers.
|
|
2844
|
-
*/
|
|
2845
2850
|
stop() {
|
|
2846
2851
|
this.closed = true;
|
|
2847
2852
|
this.clearReconnectTimer();
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
}
|
|
2857
|
-
});
|
|
2858
|
-
}
|
|
2859
|
-
this.pending.clear();
|
|
2860
|
-
if (this.socket) {
|
|
2861
|
-
try {
|
|
2862
|
-
this.socket.end();
|
|
2863
|
-
} catch {}
|
|
2864
|
-
this.socket = null;
|
|
2865
|
-
}
|
|
2866
|
-
this._connected = false;
|
|
2867
|
-
this.removeAllListeners();
|
|
2853
|
+
this.resolveAllPending("CLIENT_STOPPED", "IPC client stopped");
|
|
2854
|
+
const socket = this.socket;
|
|
2855
|
+
this.socket = null;
|
|
2856
|
+
this.isSocketConnected = false;
|
|
2857
|
+
this.resetDecoder();
|
|
2858
|
+
if (socket) try {
|
|
2859
|
+
socket.destroy();
|
|
2860
|
+
} catch {}
|
|
2868
2861
|
}
|
|
2869
|
-
/**
|
|
2870
|
-
* Send a request to the daemon and wait for a response.
|
|
2871
|
-
*/
|
|
2872
2862
|
async request(method, params = {}, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
2873
|
-
if (!this.
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
message: "Not connected to daemon"
|
|
2879
|
-
}
|
|
2880
|
-
};
|
|
2863
|
+
if (!this.isSocketConnected || !this.socket) return this.errorResponse("", "NOT_CONNECTED", "Not connected to daemon");
|
|
2864
|
+
if (method.length < 1 || method.length > 256) return this.errorResponse("", "INVALID_REQUEST", "IPC method must contain 1 to 256 characters");
|
|
2865
|
+
const runtimeParams = params;
|
|
2866
|
+
if (typeof runtimeParams !== "object" || runtimeParams === null || Array.isArray(runtimeParams)) return this.errorResponse("", "INVALID_REQUEST", "IPC params must be an object");
|
|
2867
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_REQUEST_TIMEOUT_MS) return this.errorResponse("", "INVALID_REQUEST", `IPC timeout must be an integer from 1 to ${String(MAX_REQUEST_TIMEOUT_MS)}ms`);
|
|
2881
2868
|
const id = (0, node_crypto.randomUUID)();
|
|
2869
|
+
if (this.pending.size >= MAX_PENDING_REQUESTS) return this.errorResponse(id, "TOO_MANY_REQUESTS", "Too many IPC requests are in flight");
|
|
2882
2870
|
const request = {
|
|
2883
2871
|
type: "req",
|
|
2884
2872
|
id,
|
|
2885
2873
|
method,
|
|
2886
2874
|
params
|
|
2887
2875
|
};
|
|
2876
|
+
const frame = this.serializeFrame(request, id);
|
|
2877
|
+
if (typeof frame !== "string") return frame;
|
|
2878
|
+
const socket = this.socket;
|
|
2888
2879
|
return new Promise((resolve) => {
|
|
2889
2880
|
const timer = setTimeout(() => {
|
|
2890
2881
|
this.pending.delete(id);
|
|
2891
|
-
resolve({
|
|
2892
|
-
id,
|
|
2893
|
-
ok: false,
|
|
2894
|
-
error: {
|
|
2895
|
-
code: "TIMEOUT",
|
|
2896
|
-
message: `Request ${method} timed out after ${String(timeoutMs)}ms`
|
|
2897
|
-
}
|
|
2898
|
-
});
|
|
2882
|
+
resolve(this.errorResponse(id, "TIMEOUT", `Request ${method} timed out after ${String(timeoutMs)}ms`));
|
|
2899
2883
|
}, timeoutMs);
|
|
2884
|
+
timer.unref();
|
|
2900
2885
|
this.pending.set(id, {
|
|
2901
2886
|
resolve,
|
|
2902
2887
|
timer
|
|
2903
2888
|
});
|
|
2904
2889
|
try {
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
} catch (err) {
|
|
2890
|
+
socket.write(frame);
|
|
2891
|
+
} catch (error) {
|
|
2908
2892
|
clearTimeout(timer);
|
|
2909
2893
|
this.pending.delete(id);
|
|
2910
|
-
resolve({
|
|
2911
|
-
id,
|
|
2912
|
-
ok: false,
|
|
2913
|
-
error: {
|
|
2914
|
-
code: "SEND_FAILED",
|
|
2915
|
-
message: `Failed to send: ${err instanceof Error ? err.message : String(err)}`
|
|
2916
|
-
}
|
|
2917
|
-
});
|
|
2894
|
+
resolve(this.errorResponse(id, "SEND_FAILED", `Failed to send: ${error instanceof Error ? error.message : String(error)}`));
|
|
2918
2895
|
}
|
|
2919
2896
|
});
|
|
2920
2897
|
}
|
|
2921
|
-
/**
|
|
2922
|
-
* Send a response to a daemon request.
|
|
2923
|
-
* Used internally by the request event handler.
|
|
2924
|
-
*/
|
|
2925
2898
|
sendResponse(response) {
|
|
2926
|
-
|
|
2899
|
+
const socket = this.socket;
|
|
2900
|
+
if (!this.isSocketConnected || !socket) return;
|
|
2901
|
+
if (!isIPCResponse(response)) {
|
|
2902
|
+
this.log.warn("Refusing malformed outgoing IPC response");
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
const frame = this.serializeFrame(response, response.id);
|
|
2906
|
+
if (typeof frame !== "string") {
|
|
2907
|
+
this.log.warn(`Refusing oversized or unserializable IPC response for ${response.id}`);
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2927
2910
|
try {
|
|
2928
|
-
|
|
2911
|
+
socket.write(frame);
|
|
2929
2912
|
} catch {}
|
|
2930
2913
|
}
|
|
2931
|
-
|
|
2932
|
-
if (this.closed) return;
|
|
2914
|
+
openSocket() {
|
|
2915
|
+
if (this.closed || this.socket) return;
|
|
2916
|
+
this.resetDecoder();
|
|
2933
2917
|
this.log.debug(`Connecting to daemon at ${this.socketPath}...`);
|
|
2934
|
-
|
|
2918
|
+
const socket = (0, node_net.createConnection)(this.socketPath, () => {
|
|
2919
|
+
if (this.closed || this.socket !== socket) {
|
|
2920
|
+
socket.destroy();
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2935
2923
|
this.log.info(`Connected to daemon (${this.socketPath})`);
|
|
2936
|
-
this.
|
|
2924
|
+
this.isSocketConnected = true;
|
|
2937
2925
|
this.backoffMs = MIN_BACKOFF_MS;
|
|
2938
|
-
this.
|
|
2939
|
-
this.emit("connected");
|
|
2926
|
+
this.safeEmit("connected");
|
|
2940
2927
|
});
|
|
2941
|
-
this.socket
|
|
2942
|
-
|
|
2943
|
-
this.
|
|
2928
|
+
this.socket = socket;
|
|
2929
|
+
socket.on("data", (data) => {
|
|
2930
|
+
if (this.socket !== socket || this.closed) return;
|
|
2931
|
+
this.buffer += this.decoder.write(data);
|
|
2932
|
+
this.processBuffer(socket);
|
|
2933
|
+
if (Buffer.byteLength(this.buffer, "utf8") > 1048576) this.closeOversized(socket, "unterminated frame");
|
|
2944
2934
|
});
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
ok: false,
|
|
2953
|
-
error: {
|
|
2954
|
-
code: "DISCONNECTED",
|
|
2955
|
-
message: "Connection closed"
|
|
2956
|
-
}
|
|
2957
|
-
});
|
|
2958
|
-
}
|
|
2959
|
-
this.pending.clear();
|
|
2935
|
+
socket.on("close", () => {
|
|
2936
|
+
if (this.socket !== socket) return;
|
|
2937
|
+
const wasConnected = this.isSocketConnected;
|
|
2938
|
+
this.socket = null;
|
|
2939
|
+
this.isSocketConnected = false;
|
|
2940
|
+
this.resetDecoder();
|
|
2941
|
+
this.resolveAllPending("DISCONNECTED", "Connection closed");
|
|
2960
2942
|
if (wasConnected) {
|
|
2961
2943
|
this.log.warn("Disconnected from daemon");
|
|
2962
|
-
this.
|
|
2944
|
+
this.safeEmit("disconnected", "connection closed");
|
|
2963
2945
|
}
|
|
2964
|
-
this.scheduleReconnect();
|
|
2946
|
+
if (!this.closed) this.scheduleReconnect();
|
|
2965
2947
|
});
|
|
2966
|
-
|
|
2967
|
-
if (
|
|
2968
|
-
|
|
2969
|
-
this.log.
|
|
2970
|
-
|
|
2948
|
+
socket.on("error", (error) => {
|
|
2949
|
+
if (this.socket !== socket || this.closed) return;
|
|
2950
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOENT" || error.code === "ECONNRESET") {
|
|
2951
|
+
this.log.debug(`Daemon not available: ${error.code}`);
|
|
2952
|
+
return;
|
|
2971
2953
|
}
|
|
2954
|
+
this.log.error(`IPC socket error: ${error.message}`);
|
|
2955
|
+
if (this.listenerCount("error") > 0) this.safeEmit("error", error);
|
|
2972
2956
|
});
|
|
2973
2957
|
}
|
|
2974
|
-
processBuffer() {
|
|
2975
|
-
let
|
|
2976
|
-
while ((
|
|
2977
|
-
const line = this.buffer.slice(0,
|
|
2978
|
-
this.buffer = this.buffer.slice(
|
|
2958
|
+
processBuffer(socket) {
|
|
2959
|
+
let newlineIndex;
|
|
2960
|
+
while ((newlineIndex = this.buffer.indexOf("\n")) !== -1) {
|
|
2961
|
+
const line = this.buffer.slice(0, newlineIndex).trim();
|
|
2962
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
2979
2963
|
if (!line) continue;
|
|
2964
|
+
if (Buffer.byteLength(line, "utf8") > 1048576) {
|
|
2965
|
+
this.closeOversized(socket, "frame");
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2980
2968
|
let parsed;
|
|
2981
2969
|
try {
|
|
2982
2970
|
parsed = JSON.parse(line);
|
|
@@ -2984,24 +2972,11 @@ var IPCClient = class extends node_events.EventEmitter {
|
|
|
2984
2972
|
this.log.warn("Received invalid JSON from daemon");
|
|
2985
2973
|
continue;
|
|
2986
2974
|
}
|
|
2987
|
-
if (isIPCResponse(parsed))
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
if (
|
|
2992
|
-
if (parsed.event === "plugin.replaced") {
|
|
2993
|
-
this.log.warn("Replaced by another process — stopping reconnection");
|
|
2994
|
-
this.closed = true;
|
|
2995
|
-
this.clearReconnectTimer();
|
|
2996
|
-
}
|
|
2997
|
-
this.emit("event", parsed.event, parsed.payload);
|
|
2998
|
-
continue;
|
|
2999
|
-
}
|
|
3000
|
-
if (isIPCRequest(parsed)) {
|
|
3001
|
-
this.handleIncomingRequest(parsed);
|
|
3002
|
-
continue;
|
|
3003
|
-
}
|
|
3004
|
-
this.log.debug({ msg: parsed }, "Unhandled message from daemon");
|
|
2975
|
+
if (isIPCResponse(parsed)) this.handleResponse(parsed);
|
|
2976
|
+
else if (isIPCEvent(parsed)) this.handleEvent(parsed.event, parsed.payload, socket);
|
|
2977
|
+
else if (isIPCRequest(parsed)) this.handleIncomingRequest(parsed);
|
|
2978
|
+
else this.log.warn("Ignoring malformed IPC envelope");
|
|
2979
|
+
if (this.socket !== socket) return;
|
|
3005
2980
|
}
|
|
3006
2981
|
}
|
|
3007
2982
|
handleResponse(response) {
|
|
@@ -3011,37 +2986,117 @@ var IPCClient = class extends node_events.EventEmitter {
|
|
|
3011
2986
|
this.pending.delete(response.id);
|
|
3012
2987
|
pending.resolve(response);
|
|
3013
2988
|
}
|
|
2989
|
+
handleEvent(event, payload, socket) {
|
|
2990
|
+
if (event === "plugin.replaced") {
|
|
2991
|
+
this.log.warn("Replaced by another process — closing this IPC client");
|
|
2992
|
+
this.closed = true;
|
|
2993
|
+
this.clearReconnectTimer();
|
|
2994
|
+
}
|
|
2995
|
+
this.safeEmit("event", event, payload);
|
|
2996
|
+
if (event === "plugin.replaced" && this.socket === socket) socket.destroy();
|
|
2997
|
+
}
|
|
3014
2998
|
handleIncomingRequest(request) {
|
|
2999
|
+
let responded = false;
|
|
3015
3000
|
const respond = (partial) => {
|
|
3016
|
-
|
|
3001
|
+
if (responded) return;
|
|
3002
|
+
responded = true;
|
|
3003
|
+
this.sendResponse({
|
|
3017
3004
|
id: request.id,
|
|
3018
3005
|
...partial
|
|
3019
|
-
};
|
|
3020
|
-
this.sendResponse(response);
|
|
3006
|
+
});
|
|
3021
3007
|
};
|
|
3022
|
-
this.
|
|
3008
|
+
if (this.listenerCount("request") === 0) {
|
|
3009
|
+
respond({
|
|
3010
|
+
ok: false,
|
|
3011
|
+
error: {
|
|
3012
|
+
code: "UNKNOWN_METHOD",
|
|
3013
|
+
message: `No handler for method: ${request.method}`
|
|
3014
|
+
}
|
|
3015
|
+
});
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
try {
|
|
3019
|
+
this.emit("request", request.method, request.params, respond);
|
|
3020
|
+
} catch (error) {
|
|
3021
|
+
this.log.error(`IPC request handler failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3022
|
+
respond({
|
|
3023
|
+
ok: false,
|
|
3024
|
+
error: {
|
|
3025
|
+
code: "HANDLER_FAILED",
|
|
3026
|
+
message: "IPC request handler failed"
|
|
3027
|
+
}
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
serializeFrame(value, id) {
|
|
3032
|
+
let frame;
|
|
3033
|
+
try {
|
|
3034
|
+
frame = `${JSON.stringify(value)}\n`;
|
|
3035
|
+
} catch (error) {
|
|
3036
|
+
return this.errorResponse(id, "SERIALIZE_FAILED", error instanceof Error ? error.message : String(error));
|
|
3037
|
+
}
|
|
3038
|
+
if (Buffer.byteLength(frame, "utf8") > 1048576) return this.errorResponse(id, "PAYLOAD_TOO_LARGE", `IPC frame exceeds ${String(MAX_IPC_MESSAGE_BYTES)} bytes`);
|
|
3039
|
+
return frame;
|
|
3040
|
+
}
|
|
3041
|
+
closeOversized(socket, kind) {
|
|
3042
|
+
this.log.warn(`Closing daemon connection after oversized IPC ${kind} (limit ${String(MAX_IPC_MESSAGE_BYTES)} bytes)`);
|
|
3043
|
+
this.resetDecoder();
|
|
3044
|
+
if (this.socket === socket) socket.destroy();
|
|
3045
|
+
}
|
|
3046
|
+
resolveAllPending(code, message) {
|
|
3047
|
+
for (const [id, pending] of this.pending) {
|
|
3048
|
+
clearTimeout(pending.timer);
|
|
3049
|
+
pending.resolve(this.errorResponse(id, code, message));
|
|
3050
|
+
}
|
|
3051
|
+
this.pending.clear();
|
|
3023
3052
|
}
|
|
3024
3053
|
scheduleReconnect() {
|
|
3025
|
-
if (this.closed) return;
|
|
3026
|
-
this.clearReconnectTimer();
|
|
3054
|
+
if (this.closed || this.reconnectTimer || this.socket) return;
|
|
3027
3055
|
const delay = this.backoffMs;
|
|
3028
3056
|
this.backoffMs = Math.min(this.backoffMs * 2, MAX_BACKOFF_MS);
|
|
3029
3057
|
this.log.debug(`Reconnecting in ${String(delay)}ms...`);
|
|
3030
3058
|
this.reconnectTimer = setTimeout(() => {
|
|
3031
|
-
this.
|
|
3059
|
+
this.reconnectTimer = null;
|
|
3060
|
+
this.openSocket();
|
|
3032
3061
|
}, delay);
|
|
3033
3062
|
this.reconnectTimer.unref();
|
|
3034
3063
|
}
|
|
3035
3064
|
clearReconnectTimer() {
|
|
3036
|
-
if (this.reconnectTimer)
|
|
3037
|
-
|
|
3038
|
-
|
|
3065
|
+
if (!this.reconnectTimer) return;
|
|
3066
|
+
clearTimeout(this.reconnectTimer);
|
|
3067
|
+
this.reconnectTimer = null;
|
|
3068
|
+
}
|
|
3069
|
+
resetDecoder() {
|
|
3070
|
+
this.buffer = "";
|
|
3071
|
+
this.decoder = new node_string_decoder.StringDecoder("utf8");
|
|
3072
|
+
}
|
|
3073
|
+
errorResponse(id, code, message) {
|
|
3074
|
+
return {
|
|
3075
|
+
id,
|
|
3076
|
+
ok: false,
|
|
3077
|
+
error: {
|
|
3078
|
+
code,
|
|
3079
|
+
message
|
|
3080
|
+
}
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
safeEmit(event, ...args) {
|
|
3084
|
+
try {
|
|
3085
|
+
this.emit(event, ...args);
|
|
3086
|
+
} catch (error) {
|
|
3087
|
+
this.log.error(`IPC ${event} listener failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3039
3088
|
}
|
|
3040
3089
|
}
|
|
3041
3090
|
};
|
|
3042
3091
|
//#endregion
|
|
3043
3092
|
//#region src/capability-reporter.ts
|
|
3044
|
-
|
|
3093
|
+
/**
|
|
3094
|
+
* Capability Reporter — registers OpenClaw's capabilities with the daemon.
|
|
3095
|
+
*
|
|
3096
|
+
* On connect (and reconnect), sends a `register` request to the daemon
|
|
3097
|
+
* with OpenClaw framework info, plugin version, and capabilities list.
|
|
3098
|
+
*/
|
|
3099
|
+
const pkg$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
3045
3100
|
const OPENCLAW_CAPABILITIES = [
|
|
3046
3101
|
"sessions",
|
|
3047
3102
|
"tools",
|
|
@@ -3057,7 +3112,7 @@ async function registerWithDaemon(client, log, pluginName = "@alfe.ai/openclaw")
|
|
|
3057
3112
|
const response = await client.request("register", {
|
|
3058
3113
|
framework: "openclaw",
|
|
3059
3114
|
name: pluginName,
|
|
3060
|
-
version:
|
|
3115
|
+
version: pkg$1.version,
|
|
3061
3116
|
protocolVersion: 1,
|
|
3062
3117
|
capabilities: [...OPENCLAW_CAPABILITIES],
|
|
3063
3118
|
pid: process.pid
|
|
@@ -3070,10 +3125,252 @@ async function registerWithDaemon(client, log, pluginName = "@alfe.ai/openclaw")
|
|
|
3070
3125
|
} else log.error(`Registration failed: ${err?.message ?? "unknown error"}`);
|
|
3071
3126
|
return null;
|
|
3072
3127
|
}
|
|
3128
|
+
if (!isRegisterResult(response.payload)) {
|
|
3129
|
+
log.error("Registration failed: daemon returned an invalid response");
|
|
3130
|
+
return null;
|
|
3131
|
+
}
|
|
3073
3132
|
const result = response.payload;
|
|
3074
3133
|
log.info(`Registered with daemon v${result.daemonVersion} (protocol v${String(result.protocolVersion)})`);
|
|
3075
3134
|
return result;
|
|
3076
3135
|
}
|
|
3136
|
+
function isRegisterResult(value) {
|
|
3137
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3138
|
+
const result = value;
|
|
3139
|
+
return typeof result.status === "string" && result.status.length > 0 && result.status.length <= 128 && typeof result.daemonVersion === "string" && result.daemonVersion.length > 0 && result.daemonVersion.length <= 128 && Number.isSafeInteger(result.protocolVersion) && result.protocolVersion > 0;
|
|
3140
|
+
}
|
|
3141
|
+
//#endregion
|
|
3142
|
+
//#region src/avatar-source.ts
|
|
3143
|
+
/** Safe local/remote avatar loading for the `set_avatar` tool. */
|
|
3144
|
+
const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
|
|
3145
|
+
const MAX_AVATAR_URL_LENGTH = 2048;
|
|
3146
|
+
const MAX_LOCAL_PATH_LENGTH = 4096;
|
|
3147
|
+
const AVATAR_DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
3148
|
+
const AVATAR_MIME_BY_EXT = {
|
|
3149
|
+
".png": "image/png",
|
|
3150
|
+
".jpg": "image/jpeg",
|
|
3151
|
+
".jpeg": "image/jpeg",
|
|
3152
|
+
".webp": "image/webp"
|
|
3153
|
+
};
|
|
3154
|
+
function deriveAgentAssetsOrigin(apiUrl) {
|
|
3155
|
+
const api = new URL(apiUrl);
|
|
3156
|
+
if (api.protocol !== "https:" || api.username || api.password || !api.hostname.startsWith("api.")) throw new Error("Cannot derive a trusted agent-assets origin from the configured API URL");
|
|
3157
|
+
api.hostname = api.hostname.replace(/^api\./u, "assets.");
|
|
3158
|
+
api.pathname = "/";
|
|
3159
|
+
api.search = "";
|
|
3160
|
+
api.hash = "";
|
|
3161
|
+
return api.origin;
|
|
3162
|
+
}
|
|
3163
|
+
async function loadAvatarSource(input, options) {
|
|
3164
|
+
if (input.url !== void 0 === (input.path !== void 0)) throw new Error("Provide exactly one of url or path");
|
|
3165
|
+
if (input.url !== void 0) return loadRemoteAvatar(input.url, options.agentAssetsOrigin, options.fetchImpl ?? fetch);
|
|
3166
|
+
return loadWorkspaceAvatar(input.path ?? "", options.workspacePath);
|
|
3167
|
+
}
|
|
3168
|
+
function assertPresignedS3UploadUrl(rawUrl) {
|
|
3169
|
+
if (rawUrl.length < 1 || rawUrl.length > MAX_AVATAR_URL_LENGTH) throw new Error("Avatar upload URL is invalid");
|
|
3170
|
+
const url = new URL(rawUrl);
|
|
3171
|
+
if (url.protocol !== "https:" || url.username || url.password || !(url.hostname === "amazonaws.com" || url.hostname.endsWith(".amazonaws.com"))) throw new Error("Avatar upload URL is not a trusted S3 endpoint");
|
|
3172
|
+
return url.href;
|
|
3173
|
+
}
|
|
3174
|
+
async function loadRemoteAvatar(rawUrl, agentAssetsOrigin, fetchImpl) {
|
|
3175
|
+
if (rawUrl.length < 1 || rawUrl.length > MAX_AVATAR_URL_LENGTH) throw new Error("Avatar URL must contain 1 to 2048 characters");
|
|
3176
|
+
const url = new URL(rawUrl);
|
|
3177
|
+
if (url.protocol !== "https:" || url.username || url.password || url.origin !== agentAssetsOrigin) throw new Error(`Avatar URL must use the trusted agent-assets origin ${agentAssetsOrigin}`);
|
|
3178
|
+
if (!url.pathname.startsWith("/generated/") && !url.pathname.startsWith("/avatars/")) throw new Error("Avatar URL must reference a generated or uploaded agent asset");
|
|
3179
|
+
const response = await fetchImpl(url, {
|
|
3180
|
+
redirect: "error",
|
|
3181
|
+
signal: AbortSignal.timeout(AVATAR_DOWNLOAD_TIMEOUT_MS)
|
|
3182
|
+
});
|
|
3183
|
+
if (!response.ok) throw new Error(`Failed to download the avatar image (${String(response.status)})`);
|
|
3184
|
+
const declaredLength = response.headers.get("content-length");
|
|
3185
|
+
if (declaredLength !== null) {
|
|
3186
|
+
const bytes = Number(declaredLength);
|
|
3187
|
+
if (!Number.isSafeInteger(bytes) || bytes < 1 || bytes > 5242880) throw new Error(`Avatar response exceeds the ${String(MAX_AVATAR_BYTES)} byte limit`);
|
|
3188
|
+
}
|
|
3189
|
+
const mimeType = avatarMime(url.pathname, response.headers.get("content-type"));
|
|
3190
|
+
const bytes = await readResponseBytes(response, MAX_AVATAR_BYTES);
|
|
3191
|
+
assertImageSignature(bytes, mimeType);
|
|
3192
|
+
return {
|
|
3193
|
+
bytes,
|
|
3194
|
+
mimeType
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
async function loadWorkspaceAvatar(rawPath, workspacePath) {
|
|
3198
|
+
if (rawPath.length < 1 || rawPath.length > MAX_LOCAL_PATH_LENGTH || (0, node_path.isAbsolute)(rawPath)) throw new Error("Avatar path must be a workspace-relative path of at most 4096 characters");
|
|
3199
|
+
if (rawPath.includes("\0")) throw new Error("Avatar path contains an invalid character");
|
|
3200
|
+
let workspace;
|
|
3201
|
+
try {
|
|
3202
|
+
workspace = await (0, node_fs_promises.realpath)(workspacePath);
|
|
3203
|
+
} catch {
|
|
3204
|
+
throw new Error("Avatar workspace is unavailable");
|
|
3205
|
+
}
|
|
3206
|
+
const candidate = (0, node_path.resolve)(workspace, rawPath);
|
|
3207
|
+
if (!isWithin(workspace, candidate)) throw new Error("Avatar path resolves outside the workspace");
|
|
3208
|
+
let fileInfo;
|
|
3209
|
+
try {
|
|
3210
|
+
fileInfo = await (0, node_fs_promises.lstat)(candidate);
|
|
3211
|
+
} catch {
|
|
3212
|
+
throw new Error("Avatar file does not exist or cannot be accessed");
|
|
3213
|
+
}
|
|
3214
|
+
if (fileInfo.isSymbolicLink() || !fileInfo.isFile()) throw new Error("Avatar path must reference a regular non-symlink file");
|
|
3215
|
+
let canonical;
|
|
3216
|
+
try {
|
|
3217
|
+
canonical = await (0, node_fs_promises.realpath)(candidate);
|
|
3218
|
+
} catch {
|
|
3219
|
+
throw new Error("Avatar file does not exist or cannot be accessed");
|
|
3220
|
+
}
|
|
3221
|
+
if (!isWithin(workspace, canonical)) throw new Error("Avatar path resolves outside the workspace");
|
|
3222
|
+
const mimeType = avatarMime(canonical, null);
|
|
3223
|
+
const handle = await (0, node_fs_promises.open)(canonical, node_fs.constants.O_RDONLY | node_fs.constants.O_NOFOLLOW);
|
|
3224
|
+
try {
|
|
3225
|
+
const opened = await handle.stat();
|
|
3226
|
+
if (!opened.isFile() || opened.size < 1 || opened.size > 5242880) throw new Error(`Avatar file must contain 1 to ${String(MAX_AVATAR_BYTES)} bytes`);
|
|
3227
|
+
const target = Buffer.allocUnsafe(MAX_AVATAR_BYTES + 1);
|
|
3228
|
+
let offset = 0;
|
|
3229
|
+
while (offset <= MAX_AVATAR_BYTES) {
|
|
3230
|
+
const { bytesRead } = await handle.read(target, offset, target.length - offset, offset);
|
|
3231
|
+
if (bytesRead === 0) break;
|
|
3232
|
+
offset += bytesRead;
|
|
3233
|
+
}
|
|
3234
|
+
if (offset < 1 || offset > 5242880) throw new Error(`Avatar file must contain 1 to ${String(MAX_AVATAR_BYTES)} bytes`);
|
|
3235
|
+
const bytes = Buffer.from(target.subarray(0, offset));
|
|
3236
|
+
assertImageSignature(bytes, mimeType);
|
|
3237
|
+
return {
|
|
3238
|
+
bytes,
|
|
3239
|
+
mimeType
|
|
3240
|
+
};
|
|
3241
|
+
} finally {
|
|
3242
|
+
await handle.close();
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
async function readResponseBytes(response, maxBytes) {
|
|
3246
|
+
if (!response.body) throw new Error("Avatar response had no body");
|
|
3247
|
+
const reader = response.body.getReader();
|
|
3248
|
+
const chunks = [];
|
|
3249
|
+
let total = 0;
|
|
3250
|
+
try {
|
|
3251
|
+
for (;;) {
|
|
3252
|
+
const { done, value } = await reader.read();
|
|
3253
|
+
if (done) break;
|
|
3254
|
+
total += value.byteLength;
|
|
3255
|
+
if (total > maxBytes) {
|
|
3256
|
+
await reader.cancel("avatar exceeds size limit");
|
|
3257
|
+
throw new Error(`Avatar response exceeds the ${String(maxBytes)} byte limit`);
|
|
3258
|
+
}
|
|
3259
|
+
chunks.push(value);
|
|
3260
|
+
}
|
|
3261
|
+
} finally {
|
|
3262
|
+
reader.releaseLock();
|
|
3263
|
+
}
|
|
3264
|
+
if (total < 1) throw new Error("Avatar response was empty");
|
|
3265
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total);
|
|
3266
|
+
}
|
|
3267
|
+
function avatarMime(pathname, contentType) {
|
|
3268
|
+
const normalizedType = (contentType ?? "").split(";", 1)[0]?.trim().toLowerCase();
|
|
3269
|
+
if (normalizedType === "image/png" || normalizedType === "image/jpeg" || normalizedType === "image/webp") return normalizedType;
|
|
3270
|
+
const fromExtension = AVATAR_MIME_BY_EXT[(0, node_path.extname)(pathname).toLowerCase()];
|
|
3271
|
+
if (fromExtension) return fromExtension;
|
|
3272
|
+
throw new Error("Avatar image must be PNG, JPEG, or WebP");
|
|
3273
|
+
}
|
|
3274
|
+
function assertImageSignature(bytes, mimeType) {
|
|
3275
|
+
if (!(mimeType === "image/png" && bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([
|
|
3276
|
+
137,
|
|
3277
|
+
80,
|
|
3278
|
+
78,
|
|
3279
|
+
71,
|
|
3280
|
+
13,
|
|
3281
|
+
10,
|
|
3282
|
+
26,
|
|
3283
|
+
10
|
|
3284
|
+
])) || mimeType === "image/jpeg" && bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255 || mimeType === "image/webp" && bytes.length >= 12 && bytes.subarray(0, 4).toString("ascii") === "RIFF" && bytes.subarray(8, 12).toString("ascii") === "WEBP")) throw new Error(`Avatar bytes do not match the declared ${mimeType} image type`);
|
|
3285
|
+
}
|
|
3286
|
+
function isWithin(root, candidate) {
|
|
3287
|
+
const rel = (0, node_path.relative)(root, candidate);
|
|
3288
|
+
return rel === "" || !rel.startsWith("..") && !(0, node_path.isAbsolute)(rel);
|
|
3289
|
+
}
|
|
3290
|
+
//#endregion
|
|
3291
|
+
//#region src/tool-input.ts
|
|
3292
|
+
/** Runtime validation for model-controlled OpenClaw tool parameters. */
|
|
3293
|
+
const MAX_JSON_DEPTH = 8;
|
|
3294
|
+
const MAX_JSON_NODES = 1e4;
|
|
3295
|
+
const MAX_RECORD_ENTRIES = 1e3;
|
|
3296
|
+
const MAX_RECORD_KEY_LENGTH = 256;
|
|
3297
|
+
const MAX_RECORD_STRING_LENGTH = 1e4;
|
|
3298
|
+
const MAX_JSON_BYTES = 1024 * 1024;
|
|
3299
|
+
const UNSAFE_KEYS = new Set([
|
|
3300
|
+
"__proto__",
|
|
3301
|
+
"constructor",
|
|
3302
|
+
"prototype"
|
|
3303
|
+
]);
|
|
3304
|
+
function requireString(params, field, maxLength, pattern) {
|
|
3305
|
+
const value = params[field];
|
|
3306
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maxLength) throw new Error(`${field} must contain 1 to ${String(maxLength)} characters`);
|
|
3307
|
+
if (pattern && !pattern.test(value)) throw new Error(`${field} has an invalid format`);
|
|
3308
|
+
return value;
|
|
3309
|
+
}
|
|
3310
|
+
function optionalString(params, field, maxLength) {
|
|
3311
|
+
if (params[field] === void 0) return void 0;
|
|
3312
|
+
return requireString(params, field, maxLength);
|
|
3313
|
+
}
|
|
3314
|
+
function requireConfirmation(params, message) {
|
|
3315
|
+
if (params.confirm !== true) throw new Error(message);
|
|
3316
|
+
}
|
|
3317
|
+
function requireMatchingConfirmation(params, field, expected) {
|
|
3318
|
+
if (params[field] !== expected) throw new Error(`${field} must exactly match ${expected}`);
|
|
3319
|
+
}
|
|
3320
|
+
function optionalJsonRecord(params, field) {
|
|
3321
|
+
if (params[field] === void 0) return void 0;
|
|
3322
|
+
return requireJsonRecord(params, field);
|
|
3323
|
+
}
|
|
3324
|
+
function requireJsonRecord(params, field) {
|
|
3325
|
+
const state = {
|
|
3326
|
+
nodes: 0,
|
|
3327
|
+
ancestors: /* @__PURE__ */ new Set()
|
|
3328
|
+
};
|
|
3329
|
+
const cloned = cloneJson(params[field], field, 0, state);
|
|
3330
|
+
if (!isRecord(cloned)) throw new Error(`${field} must be an object`);
|
|
3331
|
+
const serialized = JSON.stringify(cloned);
|
|
3332
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_JSON_BYTES) throw new Error(`${field} exceeds the ${String(MAX_JSON_BYTES)} byte limit`);
|
|
3333
|
+
return cloned;
|
|
3334
|
+
}
|
|
3335
|
+
function cloneJson(value, label, depth, state) {
|
|
3336
|
+
state.nodes += 1;
|
|
3337
|
+
if (state.nodes > MAX_JSON_NODES) throw new Error(`${label} contains too many values`);
|
|
3338
|
+
if (depth > MAX_JSON_DEPTH) throw new Error(`${label} exceeds ${String(MAX_JSON_DEPTH)} nesting levels`);
|
|
3339
|
+
if (value === null || typeof value === "boolean") return value;
|
|
3340
|
+
if (typeof value === "number") {
|
|
3341
|
+
if (!Number.isFinite(value)) throw new Error(`${label} contains a non-finite number`);
|
|
3342
|
+
return value;
|
|
3343
|
+
}
|
|
3344
|
+
if (typeof value === "string") {
|
|
3345
|
+
if (value.length > MAX_RECORD_STRING_LENGTH) throw new Error(`${label} contains a string longer than ${String(MAX_RECORD_STRING_LENGTH)} characters`);
|
|
3346
|
+
return value;
|
|
3347
|
+
}
|
|
3348
|
+
if (typeof value !== "object") throw new Error(`${label} must contain only JSON values`);
|
|
3349
|
+
if (state.ancestors.has(value)) throw new Error(`${label} must not contain cycles`);
|
|
3350
|
+
state.ancestors.add(value);
|
|
3351
|
+
try {
|
|
3352
|
+
if (Array.isArray(value)) {
|
|
3353
|
+
if (value.length > MAX_RECORD_ENTRIES) throw new Error(`${label} contains too many entries`);
|
|
3354
|
+
return value.map((entry) => cloneJson(entry, label, depth + 1, state));
|
|
3355
|
+
}
|
|
3356
|
+
if (!isRecord(value)) throw new Error(`${label} must contain only plain objects`);
|
|
3357
|
+
const entries = Object.entries(value);
|
|
3358
|
+
if (entries.length > MAX_RECORD_ENTRIES) throw new Error(`${label} contains too many entries`);
|
|
3359
|
+
const output = Object.create(null);
|
|
3360
|
+
for (const [key, entry] of entries) {
|
|
3361
|
+
if (key.length < 1 || key.length > MAX_RECORD_KEY_LENGTH || UNSAFE_KEYS.has(key)) throw new Error(`${label} contains an invalid key`);
|
|
3362
|
+
output[key] = cloneJson(entry, label, depth + 1, state);
|
|
3363
|
+
}
|
|
3364
|
+
return output;
|
|
3365
|
+
} finally {
|
|
3366
|
+
state.ancestors.delete(value);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
function isRecord(value) {
|
|
3370
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3371
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3372
|
+
return prototype === Object.prototype || prototype === null;
|
|
3373
|
+
}
|
|
3077
3374
|
//#endregion
|
|
3078
3375
|
//#region src/plugin.ts
|
|
3079
3376
|
/**
|
|
@@ -3085,44 +3382,11 @@ async function registerWithDaemon(client, log, pluginName = "@alfe.ai/openclaw")
|
|
|
3085
3382
|
* 3. Re-registers on reconnect (daemon may restart)
|
|
3086
3383
|
* 4. Gracefully handles daemon being unavailable
|
|
3087
3384
|
*
|
|
3088
|
-
* Follows the same plugin pattern as @alfe.ai/openclaw-voice.
|
|
3089
3385
|
*/
|
|
3090
3386
|
const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
3091
3387
|
const DEFAULT_SOCKET_PATH = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "gateway.sock");
|
|
3092
3388
|
let ipcClient = null;
|
|
3093
|
-
|
|
3094
|
-
const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
|
|
3095
|
-
/** Image types the avatar presign/finalize flow accepts, keyed by file extension. */
|
|
3096
|
-
const AVATAR_MIME_BY_EXT = {
|
|
3097
|
-
".png": "image/png",
|
|
3098
|
-
".jpg": "image/jpeg",
|
|
3099
|
-
".jpeg": "image/jpeg",
|
|
3100
|
-
".webp": "image/webp"
|
|
3101
|
-
};
|
|
3102
|
-
/** Infer an avatar mime type from a file path, rejecting unsupported types. */
|
|
3103
|
-
function avatarMimeFromPath(path) {
|
|
3104
|
-
const ext = (0, node_path.extname)(path).toLowerCase();
|
|
3105
|
-
const mimeType = AVATAR_MIME_BY_EXT[ext];
|
|
3106
|
-
if (!mimeType) throw new Error(`unsupported image type "${ext || path}" — set_avatar accepts .png, .jpg/.jpeg, or .webp`);
|
|
3107
|
-
return mimeType;
|
|
3108
|
-
}
|
|
3109
|
-
/**
|
|
3110
|
-
* Infer an avatar mime type for a remote image — prefer the response's
|
|
3111
|
-
* Content-Type, else the URL path extension. Lets `set_avatar` accept a URL
|
|
3112
|
-
* (e.g. the one `generate_image` returns), which is how an agent naturally
|
|
3113
|
-
* wants to set the picture it just generated.
|
|
3114
|
-
*/
|
|
3115
|
-
function avatarMimeFromUrl(url, contentType) {
|
|
3116
|
-
const ct = (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
3117
|
-
if (ct === "image/png" || ct === "image/jpeg" || ct === "image/webp") return ct;
|
|
3118
|
-
let pathname = url;
|
|
3119
|
-
try {
|
|
3120
|
-
pathname = new URL(url).pathname;
|
|
3121
|
-
} catch {}
|
|
3122
|
-
const byExt = AVATAR_MIME_BY_EXT[(0, node_path.extname)(pathname).toLowerCase()];
|
|
3123
|
-
if (byExt) return byExt;
|
|
3124
|
-
throw new Error(`could not determine the image type for "${url}" — set_avatar accepts png, jpeg, or webp`);
|
|
3125
|
-
}
|
|
3389
|
+
let ipcOwner = null;
|
|
3126
3390
|
function ok(result) {
|
|
3127
3391
|
return { content: [{
|
|
3128
3392
|
type: "text",
|
|
@@ -3152,7 +3416,7 @@ function defineTool(def) {
|
|
|
3152
3416
|
try {
|
|
3153
3417
|
return ok(await def.handler(params));
|
|
3154
3418
|
} catch (e) {
|
|
3155
|
-
return errResult(e.message);
|
|
3419
|
+
return errResult(e instanceof Error ? e.message : "Unknown error");
|
|
3156
3420
|
}
|
|
3157
3421
|
}
|
|
3158
3422
|
};
|
|
@@ -3162,15 +3426,19 @@ function buildIntegrationTools(client) {
|
|
|
3162
3426
|
defineTool({
|
|
3163
3427
|
name: "list_integrations",
|
|
3164
3428
|
description: "List all integrations installed for this agent, including their status and available config fields.",
|
|
3165
|
-
parameters: Type.Object({}),
|
|
3429
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
3166
3430
|
handler: async () => client.listIntegrations()
|
|
3167
3431
|
}),
|
|
3168
3432
|
defineTool({
|
|
3169
3433
|
name: "get_integration_config",
|
|
3170
3434
|
description: "Get the current configuration and editable fields for a specific integration.",
|
|
3171
|
-
parameters: Type.Object({ integrationId: Type.String({
|
|
3435
|
+
parameters: Type.Object({ integrationId: Type.String({
|
|
3436
|
+
description: "The integration to inspect (e.g. \"voice\", \"slack\")",
|
|
3437
|
+
minLength: 1,
|
|
3438
|
+
maxLength: 256
|
|
3439
|
+
}) }, { additionalProperties: false }),
|
|
3172
3440
|
handler: async (params) => {
|
|
3173
|
-
const integrationId = params
|
|
3441
|
+
const integrationId = requireString(params, "integrationId", 256);
|
|
3174
3442
|
return client.getIntegrationConfig(integrationId);
|
|
3175
3443
|
}
|
|
3176
3444
|
}),
|
|
@@ -3178,27 +3446,53 @@ function buildIntegrationTools(client) {
|
|
|
3178
3446
|
name: "update_integration_config",
|
|
3179
3447
|
description: "Update configuration for an installed integration. Only agent-editable fields can be changed.",
|
|
3180
3448
|
parameters: Type.Object({
|
|
3181
|
-
integrationId: Type.String({
|
|
3182
|
-
|
|
3183
|
-
|
|
3449
|
+
integrationId: Type.String({
|
|
3450
|
+
description: "The integration to update",
|
|
3451
|
+
minLength: 1,
|
|
3452
|
+
maxLength: 256
|
|
3453
|
+
}),
|
|
3454
|
+
fields: Type.Record(Type.String({
|
|
3455
|
+
minLength: 1,
|
|
3456
|
+
maxLength: 256
|
|
3457
|
+
}), Type.Unknown(), {
|
|
3458
|
+
description: "Key-value pairs of config fields to update",
|
|
3459
|
+
maxProperties: 1e3
|
|
3460
|
+
})
|
|
3461
|
+
}, { additionalProperties: false }),
|
|
3184
3462
|
handler: async (params) => {
|
|
3185
|
-
const integrationId = params
|
|
3186
|
-
const fields = params
|
|
3463
|
+
const integrationId = requireString(params, "integrationId", 256);
|
|
3464
|
+
const fields = requireJsonRecord(params, "fields");
|
|
3187
3465
|
return client.updateIntegrationConfig(integrationId, fields);
|
|
3188
3466
|
}
|
|
3189
3467
|
}),
|
|
3190
3468
|
defineTool({
|
|
3191
3469
|
name: "install_integration",
|
|
3192
|
-
description: "Install an integration from the registry.
|
|
3470
|
+
description: "Install an integration from the registry. This changes the agent runtime and may run integration lifecycle hooks. Set confirm=true only after the user approves the install.",
|
|
3193
3471
|
parameters: Type.Object({
|
|
3194
|
-
integrationId: Type.String({
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3472
|
+
integrationId: Type.String({
|
|
3473
|
+
description: "The integration to install (e.g. \"voice\", \"slack\", \"discord\")",
|
|
3474
|
+
minLength: 1,
|
|
3475
|
+
maxLength: 256
|
|
3476
|
+
}),
|
|
3477
|
+
version: Type.Optional(Type.String({
|
|
3478
|
+
description: "Specific version to install (defaults to latest)",
|
|
3479
|
+
minLength: 1,
|
|
3480
|
+
maxLength: 64
|
|
3481
|
+
})),
|
|
3482
|
+
config: Type.Optional(Type.Record(Type.String({
|
|
3483
|
+
minLength: 1,
|
|
3484
|
+
maxLength: 256
|
|
3485
|
+
}), Type.Unknown(), {
|
|
3486
|
+
description: "Initial configuration key-value pairs",
|
|
3487
|
+
maxProperties: 1e3
|
|
3488
|
+
})),
|
|
3489
|
+
confirm: Type.Literal(true, { description: "Must be true after the user explicitly approves the integration install" })
|
|
3490
|
+
}, { additionalProperties: false }),
|
|
3198
3491
|
handler: async (params) => {
|
|
3199
|
-
const integrationId = params
|
|
3200
|
-
const version = params
|
|
3201
|
-
const config = params
|
|
3492
|
+
const integrationId = requireString(params, "integrationId", 256);
|
|
3493
|
+
const version = optionalString(params, "version", 64);
|
|
3494
|
+
const config = optionalJsonRecord(params, "config");
|
|
3495
|
+
requireConfirmation(params, "confirm must be true after the user explicitly approves the integration install");
|
|
3202
3496
|
return client.installIntegration(integrationId, {
|
|
3203
3497
|
version,
|
|
3204
3498
|
config
|
|
@@ -3207,27 +3501,39 @@ function buildIntegrationTools(client) {
|
|
|
3207
3501
|
}),
|
|
3208
3502
|
defineTool({
|
|
3209
3503
|
name: "remove_integration",
|
|
3210
|
-
description: "Remove an installed integration. The daemon will
|
|
3211
|
-
parameters: Type.Object({
|
|
3504
|
+
description: "Remove an installed integration. The daemon will tear down its runtime resources during reconciliation. Copy the exact id into confirmIntegrationId after the user approves.",
|
|
3505
|
+
parameters: Type.Object({
|
|
3506
|
+
integrationId: Type.String({
|
|
3507
|
+
description: "The integration to remove",
|
|
3508
|
+
minLength: 1,
|
|
3509
|
+
maxLength: 256
|
|
3510
|
+
}),
|
|
3511
|
+
confirmIntegrationId: Type.String({
|
|
3512
|
+
description: "Exact integration id copied to confirm removal",
|
|
3513
|
+
minLength: 1,
|
|
3514
|
+
maxLength: 256
|
|
3515
|
+
})
|
|
3516
|
+
}, { additionalProperties: false }),
|
|
3212
3517
|
handler: async (params) => {
|
|
3213
|
-
const integrationId = params
|
|
3518
|
+
const integrationId = requireString(params, "integrationId", 256);
|
|
3519
|
+
requireMatchingConfirmation(params, "confirmIntegrationId", integrationId);
|
|
3214
3520
|
return client.removeIntegration(integrationId);
|
|
3215
3521
|
}
|
|
3216
3522
|
}),
|
|
3217
3523
|
defineTool({
|
|
3218
3524
|
name: "browse_integration_registry",
|
|
3219
3525
|
description: "Browse available integrations in the registry. Shows all integrations that can be installed.",
|
|
3220
|
-
parameters: Type.Object({}),
|
|
3526
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
3221
3527
|
handler: async () => client.getRegistry()
|
|
3222
3528
|
})
|
|
3223
3529
|
];
|
|
3224
3530
|
}
|
|
3225
|
-
function buildVoiceTools(client) {
|
|
3531
|
+
function buildVoiceTools(client, options) {
|
|
3226
3532
|
return [
|
|
3227
3533
|
defineTool({
|
|
3228
3534
|
name: "list_voices",
|
|
3229
3535
|
description: "List the available voices this agent can speak with (the ElevenLabs catalogue). Use this to pick or confirm a voice before calling set_voice with the chosen id.",
|
|
3230
|
-
parameters: Type.Object({}),
|
|
3536
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
3231
3537
|
handler: async () => {
|
|
3232
3538
|
const { voices } = await client.listVoices();
|
|
3233
3539
|
return { voices: voices.map((v) => ({
|
|
@@ -3243,9 +3549,14 @@ function buildVoiceTools(client) {
|
|
|
3243
3549
|
defineTool({
|
|
3244
3550
|
name: "set_voice",
|
|
3245
3551
|
description: "Change this agent's OWN voice. Pass a `voiceId` from list_voices. Returns the updated voice config. (Voice is always on — it cannot be turned off per-agent.)",
|
|
3246
|
-
parameters: Type.Object({ voiceId: Type.String({
|
|
3552
|
+
parameters: Type.Object({ voiceId: Type.String({
|
|
3553
|
+
description: "The ElevenLabs voice id to use, from list_voices (e.g. the `id` field).",
|
|
3554
|
+
minLength: 8,
|
|
3555
|
+
maxLength: 64,
|
|
3556
|
+
pattern: "^[A-Za-z0-9]{8,64}$"
|
|
3557
|
+
}) }, { additionalProperties: false }),
|
|
3247
3558
|
handler: async (params) => {
|
|
3248
|
-
const voiceId = params
|
|
3559
|
+
const voiceId = requireString(params, "voiceId", 64, /^[A-Za-z0-9]{8,64}$/u);
|
|
3249
3560
|
return {
|
|
3250
3561
|
updated: true,
|
|
3251
3562
|
voiceConfig: (await client.updateSelf({ voiceConfig: { voiceId } })).voiceConfig
|
|
@@ -3255,9 +3566,13 @@ function buildVoiceTools(client) {
|
|
|
3255
3566
|
defineTool({
|
|
3256
3567
|
name: "generate_avatar",
|
|
3257
3568
|
description: "Generate and set THIS agent's own profile picture / avatar from a text prompt (e.g. a character description). Updates the agent's real profile picture shown in the dashboard, chat, and apps. Use this — do NOT use `config set ui.assistant.avatar`, which has no effect on the real profile.",
|
|
3258
|
-
parameters: Type.Object({ prompt: Type.String({
|
|
3569
|
+
parameters: Type.Object({ prompt: Type.String({
|
|
3570
|
+
description: "Text description of the avatar to generate (e.g. a character or portrait description).",
|
|
3571
|
+
minLength: 1,
|
|
3572
|
+
maxLength: 2e3
|
|
3573
|
+
}) }, { additionalProperties: false }),
|
|
3259
3574
|
handler: async (params) => {
|
|
3260
|
-
const prompt = params
|
|
3575
|
+
const prompt = requireString(params, "prompt", 2e3);
|
|
3261
3576
|
return {
|
|
3262
3577
|
updated: true,
|
|
3263
3578
|
avatarUrl: (await client.generateAvatar({ prompt })).avatarUrl,
|
|
@@ -3267,34 +3582,40 @@ function buildVoiceTools(client) {
|
|
|
3267
3582
|
}),
|
|
3268
3583
|
defineTool({
|
|
3269
3584
|
name: "set_avatar",
|
|
3270
|
-
description: "Set THIS agent's OWN profile picture from an image — pass
|
|
3585
|
+
description: "Set THIS agent's OWN profile picture from an image — pass an Alfe agent-assets `url` (e.g. the URL returned by `generate_image`) OR a workspace-relative local `path` (png/jpeg/webp, <=5MB). Updates the real profile picture shown in the dashboard, chat, and apps. IMPORTANT: this is how you set YOUR OWN avatar — do NOT use `update_identity` (that edits a CONTACT/person you know, not your own profile) and do NOT use `config set ui.assistant.avatar` (no effect).",
|
|
3271
3586
|
parameters: Type.Object({
|
|
3272
|
-
url: Type.Optional(Type.String({
|
|
3273
|
-
|
|
3274
|
-
|
|
3587
|
+
url: Type.Optional(Type.String({
|
|
3588
|
+
description: "Trusted Alfe agent-assets URL — e.g. the `url` returned by `generate_image`. png/jpeg/webp, <=5MB.",
|
|
3589
|
+
minLength: 1,
|
|
3590
|
+
maxLength: 2048,
|
|
3591
|
+
pattern: "^https://"
|
|
3592
|
+
})),
|
|
3593
|
+
path: Type.Optional(Type.String({
|
|
3594
|
+
description: "Workspace-relative path to a local image file (.png, .jpg/.jpeg, or .webp, <=5MB).",
|
|
3595
|
+
minLength: 1,
|
|
3596
|
+
maxLength: 4096
|
|
3597
|
+
}))
|
|
3598
|
+
}, { additionalProperties: false }),
|
|
3275
3599
|
handler: async (params) => {
|
|
3276
|
-
const
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
bytes = Buffer.from(await dl.arrayBuffer());
|
|
3284
|
-
mimeType = avatarMimeFromUrl(url, dl.headers.get("content-type"));
|
|
3285
|
-
} else if (path) {
|
|
3286
|
-
mimeType = avatarMimeFromPath(path);
|
|
3287
|
-
bytes = await (0, node_fs_promises.readFile)(path);
|
|
3288
|
-
} else throw new Error("provide either `url` (an image URL, e.g. from generate_image) or `path` (a local image file)");
|
|
3289
|
-
if (bytes.length > MAX_AVATAR_BYTES) throw new Error(`image is ${String(bytes.length)} bytes — the avatar limit is ${String(MAX_AVATAR_BYTES)} bytes (5MB)`);
|
|
3600
|
+
const { bytes, mimeType } = await loadAvatarSource({
|
|
3601
|
+
url: optionalString(params, "url", 2048),
|
|
3602
|
+
path: optionalString(params, "path", 4096)
|
|
3603
|
+
}, {
|
|
3604
|
+
workspacePath: options.workspacePath,
|
|
3605
|
+
agentAssetsOrigin: options.agentAssetsOrigin
|
|
3606
|
+
});
|
|
3290
3607
|
const { uploadUrl, s3Key } = await client.presignAvatar({
|
|
3291
3608
|
mimeType,
|
|
3292
3609
|
size: bytes.length
|
|
3293
3610
|
});
|
|
3294
|
-
const
|
|
3611
|
+
const trustedUploadUrl = assertPresignedS3UploadUrl(uploadUrl);
|
|
3612
|
+
if (typeof s3Key !== "string" || s3Key.length < 1 || s3Key.length > 1024 || !s3Key.startsWith("avatars/")) throw new Error("Avatar presign returned an invalid object key");
|
|
3613
|
+
const putRes = await fetch(trustedUploadUrl, {
|
|
3295
3614
|
method: "PUT",
|
|
3296
3615
|
headers: { "Content-Type": mimeType },
|
|
3297
|
-
body: new Uint8Array(bytes)
|
|
3616
|
+
body: new Uint8Array(bytes),
|
|
3617
|
+
redirect: "error",
|
|
3618
|
+
signal: AbortSignal.timeout(3e4)
|
|
3298
3619
|
});
|
|
3299
3620
|
if (!putRes.ok) throw new Error(`failed to upload the avatar image (${String(putRes.status)})`);
|
|
3300
3621
|
return {
|
|
@@ -3306,19 +3627,35 @@ function buildVoiceTools(client) {
|
|
|
3306
3627
|
}),
|
|
3307
3628
|
defineTool({
|
|
3308
3629
|
name: "generate_image",
|
|
3309
|
-
description: "Generate an image from a text prompt. Returns a URL to the generated
|
|
3630
|
+
description: "Generate an image from a text prompt. Returns a URL to the generated image. To show the image to the user, embed it in your reply as markdown: . To set the generated image as YOUR OWN profile picture, pass its url to `set_avatar` (NOT update_identity, which edits other people you know). Or skip this and use `generate_avatar` to generate + set your avatar in one step. Optional `model` picks the image model (default gpt-image-1). Generation is queued server-side while this tool polls for completion and may take up to three minutes; if it times out, retry or use a standard size.",
|
|
3310
3631
|
parameters: Type.Object({
|
|
3311
|
-
prompt: Type.String({
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3632
|
+
prompt: Type.String({
|
|
3633
|
+
description: "Text description of the image to generate.",
|
|
3634
|
+
minLength: 1,
|
|
3635
|
+
maxLength: 4e3
|
|
3636
|
+
}),
|
|
3637
|
+
model: Type.Optional(Type.String({
|
|
3638
|
+
description: "Image model id (default \"gpt-image-1\"). Others may be available, e.g. dall-e-3.",
|
|
3639
|
+
minLength: 1,
|
|
3640
|
+
maxLength: 100
|
|
3641
|
+
})),
|
|
3642
|
+
size: Type.Optional(Type.String({
|
|
3643
|
+
description: "Image size. For the default gpt-image-1 use one of \"1024x1024\" (square), \"1536x1024\" (landscape), \"1024x1536\" (portrait), or \"auto\" — NOT \"512x512\". Other models accept their own sizes. Omit to use the provider default.",
|
|
3644
|
+
minLength: 1,
|
|
3645
|
+
maxLength: 32
|
|
3646
|
+
})),
|
|
3647
|
+
quality: Type.Optional(Type.String({
|
|
3648
|
+
description: "Image quality hint (provider-dependent).",
|
|
3649
|
+
minLength: 1,
|
|
3650
|
+
maxLength: 32
|
|
3651
|
+
}))
|
|
3652
|
+
}, { additionalProperties: false }),
|
|
3316
3653
|
handler: async (params) => {
|
|
3317
3654
|
const { imageUrl, model } = await client.generateImage({
|
|
3318
|
-
prompt: params
|
|
3319
|
-
model: params
|
|
3320
|
-
size: params
|
|
3321
|
-
quality: params
|
|
3655
|
+
prompt: requireString(params, "prompt", 4e3),
|
|
3656
|
+
model: optionalString(params, "model", 100),
|
|
3657
|
+
size: optionalString(params, "size", 32),
|
|
3658
|
+
quality: optionalString(params, "quality", 32)
|
|
3322
3659
|
});
|
|
3323
3660
|
return {
|
|
3324
3661
|
imageUrl,
|
|
@@ -3341,42 +3678,59 @@ const plugin = {
|
|
|
3341
3678
|
try {
|
|
3342
3679
|
cfg = (0, _alfe_ai_config.resolveConfig)();
|
|
3343
3680
|
} catch (err) {
|
|
3344
|
-
log.warn(`Integration tools not registered — config not available: ${err.message}`);
|
|
3681
|
+
log.warn(`Integration tools not registered — config not available: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
3345
3682
|
}
|
|
3346
3683
|
if (cfg) {
|
|
3347
3684
|
const client = new _alfe_ai_agent_api_client.AgentApiClient({
|
|
3348
3685
|
apiKey: cfg.apiKey,
|
|
3349
3686
|
apiUrl: cfg.apiUrl
|
|
3350
3687
|
});
|
|
3351
|
-
|
|
3688
|
+
let agentAssetsOrigin = "https://assets.invalid";
|
|
3689
|
+
try {
|
|
3690
|
+
agentAssetsOrigin = deriveAgentAssetsOrigin(cfg.apiUrl);
|
|
3691
|
+
} catch {
|
|
3692
|
+
log.warn("Remote set_avatar URLs are unavailable for this API configuration");
|
|
3693
|
+
}
|
|
3694
|
+
const tools = [...buildIntegrationTools(client), ...buildVoiceTools(client, {
|
|
3695
|
+
workspacePath: cfg.workspacePath,
|
|
3696
|
+
agentAssetsOrigin
|
|
3697
|
+
})];
|
|
3352
3698
|
for (const tool of tools) api.registerTool(tool);
|
|
3353
3699
|
log.info(`Registered ${String(tools.length)} agent tools: ${tools.map((t) => t.name).join(", ")}`);
|
|
3354
3700
|
}
|
|
3701
|
+
const serviceOwner = Symbol("alfe-daemon-ipc-owner");
|
|
3355
3702
|
const startDaemonIpc = () => {
|
|
3356
3703
|
if (globalThis.__alfeOpenclawPluginActivated === true) {
|
|
3357
3704
|
log.debug("Alfe OpenClaw Plugin already activated — skipping duplicate");
|
|
3358
3705
|
return;
|
|
3359
3706
|
}
|
|
3360
3707
|
globalThis.__alfeOpenclawPluginActivated = true;
|
|
3708
|
+
ipcOwner = serviceOwner;
|
|
3361
3709
|
if (ipcClient) {
|
|
3362
3710
|
log.warn("Stopping stale IPC client from prior activation");
|
|
3363
3711
|
ipcClient.stop();
|
|
3364
3712
|
ipcClient = null;
|
|
3365
3713
|
}
|
|
3366
3714
|
log.info("Alfe OpenClaw Plugin activating...");
|
|
3367
|
-
const
|
|
3368
|
-
|
|
3715
|
+
const configuredSocketPath = ((api.config ?? {}).plugins?.entries?.["@alfe.ai/openclaw"]?.config ?? {}).socketPath ?? process.env.ALFE_GATEWAY_SOCKET ?? DEFAULT_SOCKET_PATH;
|
|
3716
|
+
if (typeof configuredSocketPath !== "string" || configuredSocketPath.length < 1 || configuredSocketPath.length > 4096 || configuredSocketPath.includes("\0")) {
|
|
3717
|
+
ipcOwner = null;
|
|
3718
|
+
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3719
|
+
throw new Error("Invalid Alfe gateway socket path");
|
|
3720
|
+
}
|
|
3721
|
+
const socketPath = configuredSocketPath;
|
|
3722
|
+
ipcClient = new IPCClient(socketPath, log);
|
|
3369
3723
|
ipcClient.on("event", (event, payload) => {
|
|
3370
3724
|
switch (event) {
|
|
3371
3725
|
case "cloud.status": {
|
|
3372
|
-
const
|
|
3373
|
-
log.info(`Alfe cloud: ${
|
|
3726
|
+
const connected = typeof payload === "object" && payload !== null && !Array.isArray(payload) && payload.connected === true;
|
|
3727
|
+
log.info(`Alfe cloud: ${connected ? "connected" : "disconnected"}`);
|
|
3374
3728
|
break;
|
|
3375
3729
|
}
|
|
3376
3730
|
case "daemon.shutdown":
|
|
3377
3731
|
log.info("Daemon shutting down — will reconnect when available");
|
|
3378
3732
|
break;
|
|
3379
|
-
default: log.debug(`Daemon event: ${event}
|
|
3733
|
+
default: log.debug(`Daemon event: ${event}`);
|
|
3380
3734
|
}
|
|
3381
3735
|
});
|
|
3382
3736
|
ipcClient.on("request", (method, _params, respond) => {
|
|
@@ -3393,7 +3747,9 @@ const plugin = {
|
|
|
3393
3747
|
ipcClient.on("connected", () => {
|
|
3394
3748
|
log.info("Connected to Alfe daemon — registering capabilities...");
|
|
3395
3749
|
registerWithDaemon(ipc, log, plugin.id).then((result) => {
|
|
3396
|
-
if (!result) log.error("Failed to register with daemon — commands may not be routed");
|
|
3750
|
+
if (ipcClient === ipc && ipcOwner === serviceOwner && !result) log.error("Failed to register with daemon — commands may not be routed");
|
|
3751
|
+
}).catch((error) => {
|
|
3752
|
+
if (ipcClient === ipc && ipcOwner === serviceOwner) log.error(`Capability registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3397
3753
|
});
|
|
3398
3754
|
});
|
|
3399
3755
|
ipcClient.on("disconnected", (reason) => {
|
|
@@ -3402,15 +3758,28 @@ const plugin = {
|
|
|
3402
3758
|
ipcClient.on("error", (err) => {
|
|
3403
3759
|
log.error(`IPC error: ${err.message}`);
|
|
3404
3760
|
});
|
|
3405
|
-
|
|
3761
|
+
try {
|
|
3762
|
+
ipcClient.start();
|
|
3763
|
+
} catch (error) {
|
|
3764
|
+
ipcClient.stop();
|
|
3765
|
+
ipcClient = null;
|
|
3766
|
+
ipcOwner = null;
|
|
3767
|
+
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3768
|
+
throw error;
|
|
3769
|
+
}
|
|
3406
3770
|
log.info(`Alfe OpenClaw Plugin activated (socket: ${socketPath})`);
|
|
3407
3771
|
};
|
|
3408
3772
|
const stopDaemonIpc = () => {
|
|
3773
|
+
if (ipcOwner !== serviceOwner) {
|
|
3774
|
+
log.debug("Ignoring stop from a non-owning Alfe IPC service registration");
|
|
3775
|
+
return;
|
|
3776
|
+
}
|
|
3409
3777
|
log.info("Alfe OpenClaw Plugin deactivating...");
|
|
3410
3778
|
if (ipcClient) {
|
|
3411
3779
|
ipcClient.stop();
|
|
3412
3780
|
ipcClient = null;
|
|
3413
3781
|
}
|
|
3782
|
+
ipcOwner = null;
|
|
3414
3783
|
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3415
3784
|
log.info("Alfe OpenClaw Plugin deactivated");
|
|
3416
3785
|
};
|
|
@@ -3430,9 +3799,10 @@ const plugin = {
|
|
|
3430
3799
|
log.info("Alfe OpenClaw Plugin deactivating (legacy)...");
|
|
3431
3800
|
ipcClient.stop();
|
|
3432
3801
|
ipcClient = null;
|
|
3433
|
-
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3434
3802
|
log.info("Alfe OpenClaw Plugin deactivated");
|
|
3435
3803
|
}
|
|
3804
|
+
ipcOwner = null;
|
|
3805
|
+
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3436
3806
|
}
|
|
3437
3807
|
};
|
|
3438
3808
|
//#endregion
|