clawaxis 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { ANON_KEY, LOG_PREFIX } from "../config.js";
3
+ import { getAuthenticatedAgentId } from "../runtime.js";
4
+ import { toolOk, toolFail } from "./helpers.js";
5
+
6
+ let _agentToken = "";
7
+
8
+ export function setMemoryAgentToken(token: string) {
9
+ _agentToken = token;
10
+ }
11
+
12
+ export function registerMemoryTools(api: any) {
13
+ api.registerTool({
14
+ name: "clawaxis_memory_search",
15
+ description: "Search ClawAxis governed memory store (Supabase pgvector). Use this for long-term memory recall: prior decisions, preferences, architecture, context from previous sessions. Returns semantically ranked memory chunks.",
16
+ parameters: Type.Object({
17
+ query: Type.String({ description: "Semantic search query (e.g., 'ClawAxis architecture decisions', 'user preferences for memory')" }),
18
+ match_count: Type.Optional(Type.Number({ description: "Number of results to return. Default: 10, Max: 50" })),
19
+ match_threshold: Type.Optional(Type.Number({ description: "Minimum similarity score (0.0-1.0). Default: 0.5. Higher = more strict matching." })),
20
+ }),
21
+ async execute(_id: string, params: any) {
22
+ try {
23
+ const agent_id = getAuthenticatedAgentId() || "520bc407-81db-4ce3-85d5-d19bd437da0f";
24
+
25
+ const memoryApiUrl = "https://fqwpwypyzcbmdeajlkzt.supabase.co/functions/v1/memory-search";
26
+ const requestBody = {
27
+ query: params.query,
28
+ agent_id: agent_id,
29
+ match_count: params.match_count || 10,
30
+ match_threshold: params.match_threshold || 0.5
31
+ };
32
+
33
+ console.log(`${LOG_PREFIX} [Memory Search] Calling: ${memoryApiUrl}`);
34
+
35
+ const res = await fetch(memoryApiUrl, {
36
+ method: "POST",
37
+ headers: {
38
+ "Authorization": `Bearer ${ANON_KEY}`,
39
+ "apikey": ANON_KEY,
40
+ "x-agent-token": _agentToken,
41
+ "Content-Type": "application/json",
42
+ },
43
+ body: JSON.stringify(requestBody)
44
+ });
45
+
46
+ console.log(`${LOG_PREFIX} [Memory Search] Response status: ${res.status}`);
47
+
48
+ if (!res.ok) {
49
+ const errorText = await res.text();
50
+ console.log(`${LOG_PREFIX} [Memory Search] Error response:`, errorText);
51
+ return toolFail(`Memory search failed: ${res.status} ${errorText}`, { error: errorText });
52
+ }
53
+
54
+ const result = await res.json();
55
+ console.log(`${LOG_PREFIX} [Memory Search] Result count: ${result?.results?.length || 0}`);
56
+
57
+ if (result?.results && result.results.length > 0) {
58
+ const snippets = result.results.map((r: any, i: number) =>
59
+ `[Result ${i + 1}] (Similarity: ${r.similarity?.toFixed(3)})\n${r.content}\n`
60
+ ).join("\n---\n\n");
61
+
62
+ return toolOk(`Found ${result.results.length} memories:\n\n${snippets}`, {
63
+ count: result.results.length,
64
+ results: result.results,
65
+ });
66
+ }
67
+
68
+ return toolOk(`No memories found. Raw response: ${JSON.stringify(result).substring(0, 500)}`, {
69
+ count: 0,
70
+ results: [],
71
+ raw: result,
72
+ });
73
+ } catch (err: any) {
74
+ return toolFail(`Memory search error: ${err.message}`, { error: err.message });
75
+ }
76
+ },
77
+ });
78
+ }
@@ -0,0 +1,54 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { toolOk, toolFail, wsSendOrHttp } from "./helpers.js";
3
+
4
+ export function registerRoutineTools(api: any) {
5
+ api.registerTool({
6
+ name: "clawaxis_notify_routine",
7
+ description: "Register a new routine/cron job in the ClawAxis app after creating it in OpenClaw. See CLAWAXIS.md for field details.",
8
+ parameters: Type.Object({
9
+ name: Type.String({ description: "Short routine name (e.g., 'Daily AI News'). Displayed as the routine card title." }),
10
+ cron_expression: Type.String({ description: "Standard cron expression (e.g., '0 8 * * *' for daily at 8 AM)." }),
11
+ instruction: Type.String({ description: "What the routine does — the prompt/task description (e.g., 'Check AI news and post a summary blog post')." }),
12
+ openclaw_cron_id: Type.String({ description: "The OpenClaw cron job ID that was created." }),
13
+ next_run_at: Type.Optional(Type.String({ description: "ISO 8601 timestamp of next scheduled execution." })),
14
+ schedule_human: Type.Optional(Type.String({ description: "Human-readable schedule (e.g., 'Every day at 8:00 AM'). Shown in the app UI." })),
15
+ timezone: Type.Optional(Type.String({ description: "IANA timezone (e.g., 'America/New_York'). Default: America/New_York." })),
16
+ description: Type.Optional(Type.String({ description: "Longer description of what the routine accomplishes." })),
17
+ frequency: Type.Optional(Type.String({ description: "Must be: once, hourly, daily, weekly, monthly, or custom. Used for display grouping." })),
18
+ status: Type.Optional(Type.String({ description: "Must be: active, paused, or disabled. Default: active." })),
19
+ }),
20
+ async execute(_id: string, params: any) {
21
+ const result = await wsSendOrHttp(
22
+ { type: "routine_created", ...params },
23
+ "POST", "routine-created", params,
24
+ );
25
+ return result ? toolOk("Routine registered in app", { result }) : toolFail("Failed to register routine");
26
+ },
27
+ });
28
+
29
+ api.registerTool({
30
+ name: "clawaxis_update_routine",
31
+ description: "Update an existing routine in the ClawAxis app. Use after modifying a cron job so the app reflects the current state.",
32
+ parameters: Type.Object({
33
+ routine_id: Type.String({ description: "UUID of the routine to update (required)" }),
34
+ openclaw_cron_id: Type.Optional(Type.String({ description: "Updated cron job ID" })),
35
+ name: Type.Optional(Type.String({ description: "Updated routine name" })),
36
+ cron_expression: Type.Optional(Type.String({ description: "Updated cron expression" })),
37
+ schedule_human: Type.Optional(Type.String({ description: "Human readable schedule" })),
38
+ instruction: Type.Optional(Type.String({ description: "Updated task instructions" })),
39
+ timezone: Type.Optional(Type.String({ description: "Timezone string" })),
40
+ next_run_at: Type.Optional(Type.String({ description: "ISO timestamp of next run" })),
41
+ description: Type.Optional(Type.String({ description: "Updated description" })),
42
+ status: Type.Optional(Type.String({ description: "active, paused, or disabled" })),
43
+ }),
44
+ async execute(_id: string, params: any) {
45
+ const result = await wsSendOrHttp(
46
+ { type: "routine_update", ...params },
47
+ "POST", "routine-update", params,
48
+ );
49
+ return result?.ok
50
+ ? toolOk("Routine updated in app", { result })
51
+ : toolFail("Failed to update routine", { result });
52
+ },
53
+ });
54
+ }
@@ -0,0 +1,36 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { toolOk, toolFail, wsSendOrHttp } from "./helpers.js";
3
+
4
+ export function registerSyncTools(api: any) {
5
+ api.registerTool({
6
+ name: "clawaxis_confirm_doc_sync",
7
+ description: "Confirm that a document uploaded through the ClawAxis app has been received and processed.",
8
+ parameters: Type.Object({
9
+ document_id: Type.String({ description: "UUID of the document" }),
10
+ agent_skill_path: Type.Optional(Type.String({ description: "Path where the document was saved in agent skills" })),
11
+ }),
12
+ async execute(_id: string, params: any) {
13
+ const result = await wsSendOrHttp(
14
+ { type: "doc_synced", ...params },
15
+ "POST", "doc-synced", params,
16
+ );
17
+ return result ? toolOk("Document sync confirmed", { result }) : toolFail("Failed to confirm document sync");
18
+ },
19
+ });
20
+
21
+ api.registerTool({
22
+ name: "clawaxis_confirm_routine_sync",
23
+ description: "Confirm that a routine/cron job created through the ClawAxis app has been set up in OpenClaw.",
24
+ parameters: Type.Object({
25
+ routine_id: Type.String({ description: "UUID of the routine" }),
26
+ openclaw_cron_id: Type.Optional(Type.String({ description: "OpenClaw cron job ID that was created" })),
27
+ }),
28
+ async execute(_id: string, params: any) {
29
+ const result = await wsSendOrHttp(
30
+ { type: "routine_synced", ...params },
31
+ "POST", "routine-synced", params,
32
+ );
33
+ return result ? toolOk("Routine sync confirmed", { result }) : toolFail("Failed to confirm routine sync");
34
+ },
35
+ });
36
+ }
package/src/types.ts ADDED
@@ -0,0 +1,64 @@
1
+ export interface GovernanceConfig {
2
+ imageTools: string[];
3
+ contentTools: string[];
4
+ publishingTools: string[];
5
+ memorySearch: {
6
+ contentQuery: string;
7
+ imageQuery: string;
8
+ maxResults: number;
9
+ minScore: number;
10
+ cacheDurationMs: number;
11
+ };
12
+ validation: {
13
+ requireCTA: boolean;
14
+ requirePermanentImages: boolean;
15
+ requireValidAuthor: boolean;
16
+ allowedContentTypes: string[];
17
+ };
18
+ logging: {
19
+ enabled: boolean;
20
+ logAllTools: boolean;
21
+ logGovernanceIssues: boolean;
22
+ debounceMs: number;
23
+ };
24
+ }
25
+
26
+ export interface ValidationIssue {
27
+ field: string;
28
+ issue: string;
29
+ severity: "error" | "warning";
30
+ autofix?: () => void;
31
+ }
32
+
33
+ export interface MemoryCacheEntry {
34
+ snippets: any[];
35
+ timestamp: number;
36
+ }
37
+
38
+ export interface HookContext {
39
+ toolName: string;
40
+ params?: Record<string, any>;
41
+ agentId?: string;
42
+ sessionKey?: string;
43
+ durationMs?: number;
44
+ success?: boolean;
45
+ error?: Error;
46
+ systemPrompt?: string;
47
+ api?: any;
48
+ _injectedDocs?: string;
49
+ }
50
+
51
+ export interface LogEntry {
52
+ action: string;
53
+ detail: string;
54
+ level: string;
55
+ metadata?: Record<string, any>;
56
+ }
57
+
58
+ export interface ClawAxisConfig {
59
+ relayUrl: string;
60
+ agentToken: string;
61
+ mediaDomain: string | null;
62
+ agentName: string | null;
63
+ governance: GovernanceConfig;
64
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,161 @@
1
+ import WS from "ws";
2
+ import { LOG_PREFIX, ANON_KEY } from "./config.js";
3
+ import {
4
+ getRuntime,
5
+ getWs,
6
+ getIsAuthenticated,
7
+ getLogQueue,
8
+ getFlushTimeout,
9
+ setFlushTimeout,
10
+ } from "./runtime.js";
11
+ import type { ClawAxisConfig } from "./types.js";
12
+
13
+ let _config: ClawAxisConfig | null = null;
14
+
15
+ export function setUtilsConfig(cfg: ClawAxisConfig) {
16
+ _config = cfg;
17
+ }
18
+
19
+ function getRelayUrl(): string {
20
+ return _config?.relayUrl ?? "";
21
+ }
22
+
23
+ function getAgentToken(): string {
24
+ return _config?.agentToken ?? "";
25
+ }
26
+
27
+ function getGovernance() {
28
+ return _config?.governance;
29
+ }
30
+
31
+ export async function relayFetch(method: string, path: string, body?: any) {
32
+ const base = getRelayUrl();
33
+ const url = path ? `${base}/${path}` : base;
34
+ const opts: RequestInit = {
35
+ method,
36
+ headers: {
37
+ "Authorization": `Bearer ${ANON_KEY}`,
38
+ "apikey": ANON_KEY,
39
+ "x-agent-token": getAgentToken(),
40
+ "Content-Type": "application/json",
41
+ },
42
+ };
43
+ if (body) opts.body = JSON.stringify(body);
44
+
45
+ try {
46
+ const res = await fetch(url, opts);
47
+ if (!res.ok) {
48
+ const text = await res.text().catch(() => "");
49
+
50
+ if (res.status === 401) {
51
+ console.error(`${LOG_PREFIX} Relay ${method} ${path || ""}: 401 Unauthorized — check agentToken`);
52
+ } else if (res.status === 429) {
53
+ const retryAfter = res.headers.get("retry-after");
54
+ console.warn(`${LOG_PREFIX} Relay ${method} ${path || ""}: 429 Rate limited${retryAfter ? ` (retry-after: ${retryAfter}s)` : ""}`);
55
+ } else if (res.status >= 500) {
56
+ console.error(`${LOG_PREFIX} Relay ${method} ${path || ""}: ${res.status} Server error — ${text.substring(0, 200)}`);
57
+ } else {
58
+ console.error(`${LOG_PREFIX} Relay ${method} ${path || ""} failed: ${res.status}`);
59
+ }
60
+
61
+ return null;
62
+ }
63
+ return await res.json();
64
+ } catch (err: any) {
65
+ console.error(`${LOG_PREFIX} Relay ${method} ${path || ""} error:`, err.message);
66
+ return null;
67
+ }
68
+ }
69
+
70
+ export async function relayLog(
71
+ action: string,
72
+ detail: string,
73
+ level: "info" | "success" | "warning" | "error" = "info",
74
+ metadata?: Record<string, any>
75
+ ): Promise<void> {
76
+ const governance = getGovernance();
77
+ if (!governance?.logging.enabled) return;
78
+
79
+ const logQueue = getLogQueue();
80
+ logQueue.push({ action, detail, level, metadata: metadata || {} });
81
+
82
+ const existing = getFlushTimeout();
83
+ if (existing) clearTimeout(existing);
84
+
85
+ setFlushTimeout(setTimeout(async () => {
86
+ await flushLogQueue();
87
+ }, governance.logging.debounceMs));
88
+ }
89
+
90
+ export async function flushLogQueue(): Promise<void> {
91
+ const logQueue = getLogQueue();
92
+ if (logQueue.length === 0) return;
93
+
94
+ const batch = [...logQueue];
95
+ logQueue.length = 0;
96
+
97
+ const ws = getWs();
98
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
99
+ const entries = batch.map(log => {
100
+ const { tokens_used, cost, duration_ms, ...extraMetadata } = log.metadata || {};
101
+ return {
102
+ action: log.action,
103
+ detail: log.detail,
104
+ level: log.level || "info",
105
+ timestamp: new Date().toISOString(),
106
+ tokens_used: tokens_used || 0,
107
+ cost: cost || 0,
108
+ duration_ms: duration_ms || null,
109
+ metadata: Object.keys(extraMetadata).length > 0 ? extraMetadata : undefined,
110
+ };
111
+ });
112
+
113
+ (ws as any).send(JSON.stringify({
114
+ type: "log",
115
+ entries: entries.slice(0, 10)
116
+ }));
117
+ } else {
118
+ for (const log of batch) {
119
+ try {
120
+ await relayFetch("POST", "log", log);
121
+ } catch (err) {
122
+ console.error(`${LOG_PREFIX} Failed to log:`, log.action, err);
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ export function getModelName(): string {
129
+ const runtime = getRuntime();
130
+ try {
131
+ const agentModel = runtime?.config?.agents?.defaults?.model;
132
+ if (agentModel) return agentModel;
133
+
134
+ const providerModel = runtime?.config?.models?.default;
135
+ if (providerModel) return providerModel;
136
+
137
+ const sessionModel = runtime?.session?.store?.model;
138
+ if (sessionModel) return sessionModel;
139
+
140
+ return "unknown";
141
+ } catch {
142
+ return "unknown";
143
+ }
144
+ }
145
+
146
+ export function getSessionTokens(runtime: any): number {
147
+ try {
148
+ const store = runtime?.session?.store;
149
+ if (store?.totalTokens) return store.totalTokens;
150
+
151
+ const stats = runtime?.session?.stats;
152
+ if (stats?.tokens?.total) return stats.tokens.total;
153
+ if (stats?.totalTokens) return stats.totalTokens;
154
+
155
+ if (store?.contextTokens) return store.contextTokens;
156
+
157
+ return 0;
158
+ } catch {
159
+ return 0;
160
+ }
161
+ }
@@ -0,0 +1,294 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import WS from "ws";
3
+ import {
4
+ LOG_PREFIX,
5
+ PLUGIN_VERSION,
6
+ HEARTBEAT_MS,
7
+ PING_MS,
8
+ REAUTH_BUFFER_MS,
9
+ MAX_RECONNECT_DELAY,
10
+ } from "./config.js";
11
+ import {
12
+ getRuntime,
13
+ getWs,
14
+ setWs,
15
+ getIsAuthenticated,
16
+ setIsAuthenticated,
17
+ getAuthenticatedAgentId,
18
+ setAuthenticatedAgentId,
19
+ getReconnectAttempts,
20
+ setReconnectAttempts,
21
+ incrementReconnectAttempts,
22
+ getPingInterval,
23
+ setPingInterval,
24
+ getHeartbeatInterval,
25
+ setHeartbeatInterval,
26
+ isRunning,
27
+ setRunning,
28
+ getSessionTokensAccumulated,
29
+ getSessionCostAccumulated,
30
+ getLastHeartbeatTokens,
31
+ setLastHeartbeatTokens,
32
+ getFlushTimeout,
33
+ setFlushTimeout,
34
+ } from "./runtime.js";
35
+ import { relayLog, flushLogQueue, getModelName } from "./utils.js";
36
+ import { handleMessage, handleCommand } from "./message-handler.js";
37
+ import { processPendingDocs, processDocDeletion } from "./sync/documents.js";
38
+ import { processPendingRoutines } from "./sync/routines.js";
39
+ import type { ClawAxisConfig } from "./types.js";
40
+
41
+ let _config: ClawAxisConfig | null = null;
42
+
43
+ function getAgentToken(): string {
44
+ return _config?.agentToken ?? "";
45
+ }
46
+
47
+ function getRelayUrl(): string {
48
+ return _config?.relayUrl ?? "";
49
+ }
50
+
51
+ function sendAuth() {
52
+ const ws = getWs();
53
+ if (!ws || ws.readyState !== WS.OPEN) return;
54
+
55
+ const authMsg = {
56
+ type: "auth",
57
+ token: getAgentToken(),
58
+ plugin_version: PLUGIN_VERSION,
59
+ openclaw_version: getRuntime()?.version || "unknown",
60
+ host_os: process.platform
61
+ };
62
+
63
+ console.log(`${LOG_PREFIX} Sending auth...`);
64
+ ws.send(JSON.stringify(authMsg));
65
+ }
66
+
67
+ function cleanup() {
68
+ setIsAuthenticated(false);
69
+ setAuthenticatedAgentId(null);
70
+
71
+ const ping = getPingInterval();
72
+ if (ping) {
73
+ clearInterval(ping);
74
+ setPingInterval(null);
75
+ }
76
+ }
77
+
78
+ function startKeepalive() {
79
+ const existing = getPingInterval();
80
+ if (existing) clearInterval(existing);
81
+
82
+ setPingInterval(setInterval(() => {
83
+ const ws = getWs();
84
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
85
+ ws.send(JSON.stringify({ type: "ping" }));
86
+ }
87
+ }, PING_MS));
88
+ }
89
+
90
+ function startHeartbeat() {
91
+ const existing = getHeartbeatInterval();
92
+ if (existing) clearInterval(existing);
93
+
94
+ setHeartbeatInterval(setInterval(async () => {
95
+ const ws = getWs();
96
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
97
+ const uptime = process.uptime();
98
+ const sessionTokens = getSessionTokensAccumulated();
99
+ const tokensDelta = sessionTokens - getLastHeartbeatTokens();
100
+
101
+ ws.send(JSON.stringify({
102
+ type: "heartbeat",
103
+ status: "online",
104
+ model: getModelName(),
105
+ openclaw_version: getRuntime()?.version || "unknown",
106
+ uptime_seconds: Math.floor(uptime),
107
+ tokens_used_session: sessionTokens,
108
+ cost_session: getSessionCostAccumulated(),
109
+ tokens_delta: tokensDelta > 0 ? tokensDelta : 0,
110
+ }));
111
+
112
+ setLastHeartbeatTokens(sessionTokens);
113
+ }
114
+ }, HEARTBEAT_MS));
115
+ }
116
+
117
+ function scheduleReconnect() {
118
+ if (!isRunning()) return;
119
+
120
+ const base = Math.min(1000 * Math.pow(2, getReconnectAttempts()), MAX_RECONNECT_DELAY);
121
+ const jitter = base * 0.3 * (Math.random() * 2 - 1);
122
+ const delay = Math.max(0, Math.round(base + jitter));
123
+ incrementReconnectAttempts();
124
+
125
+ console.log(`${LOG_PREFIX} Reconnecting in ${delay}ms (attempt ${getReconnectAttempts()})...`);
126
+
127
+ setTimeout(() => {
128
+ if (isRunning()) connectWebSocket();
129
+ }, delay);
130
+ }
131
+
132
+ // ─── WebSocket message router ────────────────────────────
133
+
134
+ async function handleWebSocketMessage(msg: any) {
135
+ console.log(`${LOG_PREFIX} [WS] Received: ${msg.type}`);
136
+
137
+ switch (msg.type) {
138
+ case "auth_ok":
139
+ await handleAuthOk(msg);
140
+ break;
141
+ case "auth_error":
142
+ await handleAuthError(msg);
143
+ break;
144
+ case "message":
145
+ await handleMessage(msg);
146
+ break;
147
+ case "command":
148
+ await handleCommand(msg);
149
+ break;
150
+ case "sync":
151
+ case "heartbeat_ack":
152
+ await handleSync(msg);
153
+ break;
154
+ case "content_ack":
155
+ console.log(`${LOG_PREFIX} Content ${msg.action}: ${msg.content_id}`);
156
+ break;
157
+ case "doc_deleted":
158
+ await processDocDeletion(msg);
159
+ break;
160
+ case "routine_ack":
161
+ console.log(`${LOG_PREFIX} Routine ${msg.action}: ${msg.routine_id}`);
162
+ break;
163
+ case "error":
164
+ console.warn(`${LOG_PREFIX} Server error: ${msg.reason} \u2014 ${msg.detail || ""}`);
165
+ break;
166
+ case "reauth":
167
+ await handleReauth(msg);
168
+ break;
169
+ case "pong":
170
+ break;
171
+ default:
172
+ console.warn(`${LOG_PREFIX} Unknown message type: ${msg.type}`);
173
+ }
174
+ }
175
+
176
+ async function handleAuthOk(msg: any) {
177
+ console.log(`${LOG_PREFIX} \u2713 Authenticated`);
178
+ setIsAuthenticated(true);
179
+ setAuthenticatedAgentId(msg.agent_id || null);
180
+ if (msg.agent_id) console.log(`${LOG_PREFIX} Agent ID: ${msg.agent_id}`);
181
+ startKeepalive();
182
+ }
183
+
184
+ async function handleAuthError(msg: any) {
185
+ console.error(`${LOG_PREFIX} \u274C Auth failed: ${msg.reason}`);
186
+ setIsAuthenticated(false);
187
+ setRunning(false);
188
+ getWs()?.close();
189
+ await relayLog("error", `Authentication failed: ${msg.reason}`, "error", { reason: msg.reason });
190
+ }
191
+
192
+ async function handleSync(msg: any) {
193
+ console.log(`${LOG_PREFIX} Sync received: ${msg.pending_docs?.length || 0} docs, ${msg.pending_routines?.length || 0} routines`);
194
+ if (msg.pending_docs?.length > 0) await processPendingDocs(msg.pending_docs);
195
+ if (msg.pending_routines?.length > 0) await processPendingRoutines(msg.pending_routines);
196
+ }
197
+
198
+ async function handleReauth(msg: any) {
199
+ console.log(`${LOG_PREFIX} Re-auth requested (deadline: ${msg.deadline_ms}ms)`);
200
+ setTimeout(() => {
201
+ const ws = getWs();
202
+ if (ws?.readyState === WS.OPEN) sendAuth();
203
+ }, Math.max(100, msg.deadline_ms - REAUTH_BUFFER_MS));
204
+ }
205
+
206
+ // ─── Connection ──────────────────────────────────────────
207
+
208
+ function connectWebSocket() {
209
+ const wsUrl = getRelayUrl().replace(/^https?:/, "wss:").replace(/\/agent-relay$/, "/ws-relay");
210
+
211
+ console.log(`${LOG_PREFIX} [WS] Connecting to: ${wsUrl}`);
212
+
213
+ try {
214
+ const socket = new WS(wsUrl);
215
+ setWs(socket as any);
216
+
217
+ socket.on("open", () => {
218
+ console.log(`${LOG_PREFIX} [WS] \u2713 Connection opened`);
219
+ setReconnectAttempts(0);
220
+ sendAuth();
221
+ });
222
+
223
+ socket.on("message", async (data: WS.RawData) => {
224
+ try {
225
+ const msg = JSON.parse(data.toString());
226
+ await handleWebSocketMessage(msg);
227
+ } catch (err) {
228
+ console.error(`${LOG_PREFIX} [WS] Failed to parse message:`, err);
229
+ }
230
+ });
231
+
232
+ socket.on("close", (code: number, reason: Buffer) => {
233
+ console.log(`${LOG_PREFIX} [WS] Closed (code ${code}, reason: ${reason.toString() || "none"}), reconnecting...`);
234
+ cleanup();
235
+ scheduleReconnect();
236
+ });
237
+
238
+ socket.on("error", (err: Error) => {
239
+ console.error(`${LOG_PREFIX} [WS] Socket error:`, err.message);
240
+ });
241
+
242
+ } catch (err: any) {
243
+ console.error(`${LOG_PREFIX} [WS] Failed to create WebSocket:`, err.message);
244
+ scheduleReconnect();
245
+ }
246
+ }
247
+
248
+ // ─── Service factory ─────────────────────────────────────
249
+
250
+ export function createWebSocketService(cfg: ClawAxisConfig) {
251
+ _config = cfg;
252
+ let started = false;
253
+
254
+ return {
255
+ id: "clawaxis-ws",
256
+
257
+ async start() {
258
+ if (started) return;
259
+ started = true;
260
+ console.log(`${LOG_PREFIX} \uD83C\uDF10 Starting WebSocket connection...`);
261
+ setRunning(true);
262
+ connectWebSocket();
263
+ startHeartbeat();
264
+ },
265
+
266
+ async stop() {
267
+ console.log(`${LOG_PREFIX} Shutting down WebSocket...`);
268
+ started = false;
269
+ setRunning(false);
270
+
271
+ const ws = getWs();
272
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
273
+ (ws as any).send(JSON.stringify({ type: "heartbeat", status: "offline" }));
274
+ await sleep(100);
275
+ }
276
+
277
+ const ft = getFlushTimeout();
278
+ if (ft) {
279
+ clearTimeout(ft);
280
+ setFlushTimeout(null);
281
+ await flushLogQueue();
282
+ }
283
+
284
+ const hb = getHeartbeatInterval();
285
+ if (hb) {
286
+ clearInterval(hb);
287
+ setHeartbeatInterval(null);
288
+ }
289
+
290
+ cleanup();
291
+ (ws as any)?.close();
292
+ },
293
+ };
294
+ }