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,212 @@
1
+ import WS from "ws";
2
+ import { LOG_PREFIX } from "./config.js";
3
+ import {
4
+ getRuntime,
5
+ getWs,
6
+ getIsAuthenticated,
7
+ getAuthenticatedAgentId,
8
+ getLastTokenCount,
9
+ setLastTokenCount,
10
+ addSessionTokens,
11
+ hasProcessedMessage,
12
+ markMessageProcessed,
13
+ } from "./runtime.js";
14
+ import { relayFetch, relayLog, getModelName, getSessionTokens } from "./utils.js";
15
+
16
+ export function sendStatusUpdate(messageId: string, status: string): void {
17
+ const ws = getWs();
18
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
19
+ ws.send(JSON.stringify({
20
+ type: "status",
21
+ message_id: messageId,
22
+ status,
23
+ }));
24
+ }
25
+ }
26
+
27
+ export async function dispatchSystemMessage(
28
+ content: string,
29
+ messageSid: string,
30
+ refId: string,
31
+ ): Promise<void> {
32
+ const runtime = getRuntime();
33
+ if (!runtime) return;
34
+
35
+ const runtimeConfig = runtime.config?.loadConfig?.() ?? {};
36
+ const dispatchReply = runtime.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher;
37
+ if (!dispatchReply) return;
38
+
39
+ await dispatchReply({
40
+ ctx: {
41
+ Body: content,
42
+ RawBody: content,
43
+ CommandBody: content,
44
+ From: "clawaxis:system",
45
+ To: "clawaxis:agent",
46
+ SessionKey: "agent:main:clawaxis:main",
47
+ AccountId: "default",
48
+ ChatType: "direct",
49
+ SenderName: "ClawAxis App",
50
+ SenderId: "clawaxis-system",
51
+ Provider: "clawaxis",
52
+ Surface: "clawaxis",
53
+ MessageSid: messageSid,
54
+ Timestamp: Date.now(),
55
+ OriginatingChannel: "clawaxis",
56
+ OriginatingTo: "clawaxis:agent",
57
+ MessageType: "text",
58
+ RefId: null,
59
+ },
60
+ cfg: runtimeConfig,
61
+ dispatcherOptions: {
62
+ deliver: async (payload: any) => {
63
+ if (payload.text) {
64
+ const ws = getWs();
65
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
66
+ ws.send(JSON.stringify({
67
+ type: "reply",
68
+ ref_id: refId,
69
+ content: payload.text,
70
+ model_used: getModelName(),
71
+ tokens_used: 0,
72
+ }));
73
+ }
74
+ }
75
+ },
76
+ onError: (err: Error) => {
77
+ console.error(`${LOG_PREFIX} System dispatch error (${messageSid}): ${err.message}`);
78
+ },
79
+ },
80
+ replyOptions: {},
81
+ });
82
+ }
83
+
84
+ export async function handleMessage(msg: any): Promise<void> {
85
+ const ws = getWs();
86
+
87
+ if (ws?.readyState === WS.OPEN) {
88
+ ws.send(JSON.stringify({ type: "ack", id: msg.id }));
89
+ }
90
+
91
+ if (hasProcessedMessage(msg.id)) {
92
+ console.log(`${LOG_PREFIX} Skipping duplicate message: ${msg.id}`);
93
+ return;
94
+ }
95
+ markMessageProcessed(msg.id);
96
+
97
+ sendStatusUpdate(msg.id, "processing");
98
+
99
+ try {
100
+ console.log(`${LOG_PREFIX} Processing message: ${msg.content.substring(0, 50)}...`);
101
+ console.log(`${LOG_PREFIX} Message type: ${msg.message_type || "text"}, ref_id: ${msg.ref_id || "none"}`);
102
+
103
+ const runtime = getRuntime();
104
+ if (!runtime) {
105
+ console.error(`${LOG_PREFIX} Plugin runtime not available`);
106
+ sendStatusUpdate(msg.id, "error");
107
+ return;
108
+ }
109
+
110
+ const runtimeConfig = runtime.config?.loadConfig?.() ?? {};
111
+ const sessionId = getAuthenticatedAgentId() || "default";
112
+
113
+ const ctxPayload = {
114
+ Body: msg.content,
115
+ RawBody: msg.content,
116
+ CommandBody: msg.content,
117
+ From: "clawaxis:user",
118
+ To: "clawaxis:agent",
119
+ SessionKey: `agent:main:clawaxis:${sessionId}`,
120
+ AccountId: "default",
121
+ ChatType: "direct",
122
+ SenderName: "Dashboard User",
123
+ SenderId: "clawaxis-user",
124
+ Provider: "clawaxis",
125
+ Surface: "clawaxis",
126
+ MessageSid: msg.id,
127
+ Timestamp: Date.now(),
128
+ OriginatingChannel: "clawaxis",
129
+ OriginatingTo: "clawaxis:agent",
130
+ MessageType: msg.message_type || "text",
131
+ RefId: msg.ref_id || null,
132
+ };
133
+
134
+ const dispatchReply = runtime.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher;
135
+ if (!dispatchReply) {
136
+ console.error(`${LOG_PREFIX} Dispatch function not available`);
137
+ sendStatusUpdate(msg.id, "error");
138
+ return;
139
+ }
140
+
141
+ const tokensBefore = getSessionTokens(runtime) || getLastTokenCount();
142
+
143
+ await dispatchReply({
144
+ ctx: ctxPayload,
145
+ cfg: runtimeConfig,
146
+ dispatcherOptions: {
147
+ deliver: async (payload: any) => {
148
+ if (payload.text) {
149
+ const tokensAfter = getSessionTokens(runtime);
150
+ const tokensDelta = tokensAfter > tokensBefore ? tokensAfter - tokensBefore : 0;
151
+
152
+ if (tokensAfter > 0) setLastTokenCount(tokensAfter);
153
+ if (tokensDelta > 0) addSessionTokens(tokensDelta);
154
+
155
+ const currentWs = getWs();
156
+ if (currentWs?.readyState === WS.OPEN && getIsAuthenticated()) {
157
+ currentWs.send(JSON.stringify({
158
+ type: "reply",
159
+ ref_id: msg.id,
160
+ content: payload.text,
161
+ model_used: getModelName(),
162
+ tokens_used: tokensDelta || 0,
163
+ processing_time_ms: Date.now() - (msg.timestamp || new Date(msg.created_at).getTime())
164
+ }));
165
+ console.log(`${LOG_PREFIX} \u2713 Reply sent via WebSocket`);
166
+ } else {
167
+ await relayFetch("POST", "message", {
168
+ content: payload.text,
169
+ model_used: getModelName(),
170
+ tokens_used: tokensDelta || 0
171
+ });
172
+ console.log(`${LOG_PREFIX} \u2713 Reply sent via HTTPS`);
173
+ }
174
+
175
+ sendStatusUpdate(msg.id, "complete");
176
+ }
177
+ },
178
+ onError: (err: Error) => {
179
+ console.error(`${LOG_PREFIX} Dispatch error: ${err.message}`);
180
+ sendStatusUpdate(msg.id, "error");
181
+ },
182
+ },
183
+ replyOptions: {},
184
+ });
185
+
186
+ } catch (err: any) {
187
+ console.error(`${LOG_PREFIX} Failed to process message:`, err);
188
+ sendStatusUpdate(msg.id, "error");
189
+ await relayLog(
190
+ "error",
191
+ `Message processing failed: ${err.message}`,
192
+ "error",
193
+ { message_id: msg.id, error: err.message }
194
+ );
195
+ }
196
+ }
197
+
198
+ export async function handleCommand(msg: any): Promise<void> {
199
+ const ws = getWs();
200
+ if (ws?.readyState === WS.OPEN) {
201
+ ws.send(JSON.stringify({ type: "ack", id: msg.id }));
202
+ }
203
+
204
+ console.log(`${LOG_PREFIX} Received command: ${msg.action}`);
205
+
206
+ await relayLog(
207
+ "system",
208
+ `Command received: ${msg.action}`,
209
+ "info",
210
+ { command_id: msg.id, action: msg.action, payload: msg.payload }
211
+ );
212
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,91 @@
1
+ import type { MemoryCacheEntry, LogEntry } from "./types.js";
2
+
3
+ let pluginRuntime: any = null;
4
+
5
+ export function getRuntime(): any { return pluginRuntime; }
6
+ export function setRuntime(r: any) { pluginRuntime = r; }
7
+
8
+ // WebSocket connection state
9
+ let ws: any = null;
10
+ let reconnectAttempts = 0;
11
+ let isAuthenticated = false;
12
+ let authenticatedAgentId: string | null = null;
13
+ let pingInterval: NodeJS.Timeout | null = null;
14
+ let heartbeatInterval: NodeJS.Timeout | null = null;
15
+ let running = true;
16
+
17
+ export function getWs() { return ws; }
18
+ export function setWs(socket: any) { ws = socket; }
19
+
20
+ export function getReconnectAttempts() { return reconnectAttempts; }
21
+ export function setReconnectAttempts(n: number) { reconnectAttempts = n; }
22
+ export function incrementReconnectAttempts() { reconnectAttempts++; }
23
+
24
+ export function getIsAuthenticated() { return isAuthenticated; }
25
+ export function setIsAuthenticated(v: boolean) { isAuthenticated = v; }
26
+
27
+ export function getAuthenticatedAgentId() { return authenticatedAgentId; }
28
+ export function setAuthenticatedAgentId(id: string | null) { authenticatedAgentId = id; }
29
+
30
+ export function getPingInterval() { return pingInterval; }
31
+ export function setPingInterval(t: NodeJS.Timeout | null) { pingInterval = t; }
32
+
33
+ export function getHeartbeatInterval() { return heartbeatInterval; }
34
+ export function setHeartbeatInterval(t: NodeJS.Timeout | null) { heartbeatInterval = t; }
35
+
36
+ export function isRunning() { return running; }
37
+ export function setRunning(v: boolean) { running = v; }
38
+
39
+ // Token tracking
40
+ let lastTokenCount = 0;
41
+ let sessionTokensAccumulated = 0;
42
+ let sessionCostAccumulated = 0;
43
+ let lastHeartbeatTokens = 0;
44
+
45
+ export function getLastTokenCount() { return lastTokenCount; }
46
+ export function setLastTokenCount(n: number) { lastTokenCount = n; }
47
+
48
+ export function getSessionTokensAccumulated() { return sessionTokensAccumulated; }
49
+ export function addSessionTokens(delta: number) { sessionTokensAccumulated += delta; }
50
+
51
+ export function getSessionCostAccumulated() { return sessionCostAccumulated; }
52
+
53
+ export function getLastHeartbeatTokens() { return lastHeartbeatTokens; }
54
+ export function setLastHeartbeatTokens(n: number) { lastHeartbeatTokens = n; }
55
+
56
+ // Message deduplication with lazy cleanup
57
+ const processedMessageIds = new Set<string>();
58
+ const MESSAGE_ID_HARD_CAP = 2000;
59
+ const CLEANUP_EVERY_N = 50;
60
+ let messagesSinceCleanup = 0;
61
+
62
+ export function hasProcessedMessage(id: string): boolean {
63
+ return processedMessageIds.has(id);
64
+ }
65
+
66
+ export function markMessageProcessed(id: string): void {
67
+ processedMessageIds.add(id);
68
+ messagesSinceCleanup++;
69
+
70
+ if (messagesSinceCleanup >= CLEANUP_EVERY_N && processedMessageIds.size > MESSAGE_ID_HARD_CAP) {
71
+ messagesSinceCleanup = 0;
72
+ const excess = processedMessageIds.size - MESSAGE_ID_HARD_CAP;
73
+ const iter = processedMessageIds.values();
74
+ for (let i = 0; i < excess; i++) {
75
+ processedMessageIds.delete(iter.next().value!);
76
+ }
77
+ }
78
+ }
79
+
80
+ // Logging queue
81
+ const logQueue: LogEntry[] = [];
82
+ let flushTimeout: NodeJS.Timeout | null = null;
83
+
84
+ export function getLogQueue() { return logQueue; }
85
+ export function getFlushTimeout() { return flushTimeout; }
86
+ export function setFlushTimeout(t: NodeJS.Timeout | null) { flushTimeout = t; }
87
+
88
+ // Memory search cache
89
+ const memoryCache = new Map<string, MemoryCacheEntry>();
90
+
91
+ export function getMemoryCache() { return memoryCache; }
@@ -0,0 +1,127 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import WS from "ws";
5
+ import { LOG_PREFIX } from "../config.js";
6
+ import { getRuntime, getWs, getIsAuthenticated } from "../runtime.js";
7
+ import { relayFetch, relayLog } from "../utils.js";
8
+ import { dispatchSystemMessage } from "../message-handler.js";
9
+
10
+ export function resolveDocPath(doc: any): { dir: string; filePath: string } {
11
+ const category = doc.category || "general";
12
+ const slug = doc.name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
13
+ const runtime = getRuntime();
14
+ const workspaceDir = runtime?.config?.workspace?.dir
15
+ || path.join(homedir(), ".openclaw", "workspace");
16
+ const dir = path.join(workspaceDir, "memory", "docs", category);
17
+ const filePath = path.join(dir, `${slug}.md`);
18
+ return { dir, filePath };
19
+ }
20
+
21
+ export async function processPendingDocs(docs: any[]): Promise<void> {
22
+ if (!docs || docs.length === 0) return;
23
+
24
+ console.log(`${LOG_PREFIX} Processing ${docs.length} pending document(s)...`);
25
+
26
+ for (const doc of docs) {
27
+ try {
28
+ if (!doc.content && !doc.storage_path) {
29
+ console.warn(`${LOG_PREFIX} Document ${doc.id} has no content or storage_path, skipping`);
30
+ continue;
31
+ }
32
+
33
+ const isUpdate = (doc.version && doc.version > 1) || !!doc.agent_skill_path;
34
+ const { dir, filePath } = doc.agent_skill_path
35
+ ? { dir: path.dirname(doc.agent_skill_path), filePath: doc.agent_skill_path }
36
+ : resolveDocPath(doc);
37
+
38
+ fs.mkdirSync(dir, { recursive: true });
39
+
40
+ const frontmatter = [
41
+ `---`,
42
+ `title: "${doc.name}"`,
43
+ doc.doc_type ? `type: ${doc.doc_type}` : null,
44
+ doc.category ? `category: ${doc.category}` : null,
45
+ doc.tags?.length ? `tags: [${doc.tags.join(", ")}]` : null,
46
+ doc.description ? `description: "${doc.description}"` : null,
47
+ `version: ${doc.version || 1}`,
48
+ `synced: ${new Date().toISOString()}`,
49
+ `---`,
50
+ ].filter(Boolean).join("\n");
51
+
52
+ const fileContent = `${frontmatter}\n\n# ${doc.name}\n\n${doc.content}`;
53
+ fs.writeFileSync(filePath, fileContent, "utf8");
54
+
55
+ console.log(`${LOG_PREFIX} \u2713 ${isUpdate ? "Updated" : "Wrote"} document: ${filePath}`);
56
+
57
+ const docMeta = [
58
+ doc.doc_type ? `Type: ${doc.doc_type}` : null,
59
+ doc.category ? `Category: ${doc.category}` : null,
60
+ doc.tags?.length ? `Tags: ${doc.tags.join(", ")}` : null,
61
+ doc.description ? `Description: ${doc.description}` : null,
62
+ ].filter(Boolean).join(" | ");
63
+
64
+ const messageContent = isUpdate
65
+ ? `[DOC_UPDATED] The user updated document "${doc.name}" (v${doc.version || 1}). ${docMeta ? docMeta + ". " : ""}File updated at: ${filePath}. Read it with the read tool if you need to review the changes. Confirm sync by calling clawaxis_confirm_doc_sync with document_id: ${doc.id}.`
66
+ : `[DOC_UPLOADED] The user uploaded a new document: "${doc.name}". ${docMeta ? docMeta + ". " : ""}Saved to: ${filePath}. This file is now searchable via memory_search. Read it with the read tool if you want to review. Confirm sync by calling clawaxis_confirm_doc_sync with document_id: ${doc.id}.`;
67
+
68
+ await dispatchSystemMessage(messageContent, `doc-sync-${doc.id}`, `doc-sync-${doc.id}`);
69
+
70
+ const ws = getWs();
71
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
72
+ ws.send(JSON.stringify({
73
+ type: "doc_synced",
74
+ doc_id: doc.id,
75
+ agent_skill_path: filePath
76
+ }));
77
+ } else {
78
+ await relayFetch("POST", "doc-synced", {
79
+ document_id: doc.id,
80
+ agent_skill_path: filePath
81
+ });
82
+ }
83
+
84
+ await relayLog(
85
+ "system",
86
+ `Document ${isUpdate ? "updated" : "synced"}: ${doc.name}`,
87
+ "info",
88
+ { document_id: doc.id, path: filePath, version: doc.version }
89
+ );
90
+
91
+ } catch (err: any) {
92
+ console.error(`${LOG_PREFIX} Failed to sync document ${doc.id}:`, err);
93
+ await relayLog(
94
+ "error",
95
+ `Document sync failed: ${doc.name}`,
96
+ "error",
97
+ { document_id: doc.id, error: err.message }
98
+ );
99
+ }
100
+ }
101
+ }
102
+
103
+ export async function processDocDeletion(doc: { id: string; name: string; agent_skill_path?: string }): Promise<void> {
104
+ try {
105
+ if (doc.agent_skill_path) {
106
+ try {
107
+ fs.unlinkSync(doc.agent_skill_path);
108
+ console.log(`${LOG_PREFIX} \u2713 Deleted document file: ${doc.agent_skill_path}`);
109
+ } catch (fsErr: any) {
110
+ console.warn(`${LOG_PREFIX} Could not delete file ${doc.agent_skill_path}:`, fsErr.message);
111
+ }
112
+ }
113
+
114
+ const messageContent = `[DOC_DELETED] The user deleted document "${doc.name}"${doc.agent_skill_path ? ` (was at: ${doc.agent_skill_path})` : ""}. The file has been removed from your workspace. Memory search will no longer return results from this document.`;
115
+ await dispatchSystemMessage(messageContent, `doc-delete-${doc.id}`, `doc-delete-${doc.id}`);
116
+
117
+ await relayLog(
118
+ "system",
119
+ `Document deleted: ${doc.name}`,
120
+ "info",
121
+ { document_id: doc.id }
122
+ );
123
+
124
+ } catch (err: any) {
125
+ console.error(`${LOG_PREFIX} Failed to process document deletion ${doc.id}:`, err);
126
+ }
127
+ }
@@ -0,0 +1,56 @@
1
+ import WS from "ws";
2
+ import { LOG_PREFIX } from "../config.js";
3
+ import { getWs, getIsAuthenticated } from "../runtime.js";
4
+ import { relayLog } from "../utils.js";
5
+ import { dispatchSystemMessage } from "../message-handler.js";
6
+
7
+ export async function processPendingRoutines(routines: any[]): Promise<void> {
8
+ if (!routines || routines.length === 0) return;
9
+
10
+ console.log(`${LOG_PREFIX} Processing ${routines.length} pending routine(s)...`);
11
+
12
+ for (const routine of routines) {
13
+ try {
14
+ const isUpdate = !!routine.openclaw_cron_id;
15
+
16
+ const scheduleInfo = [
17
+ routine.cron_expression ? `Cron: ${routine.cron_expression}` : null,
18
+ routine.frequency ? `Frequency: ${routine.frequency}` : null,
19
+ routine.timezone ? `Timezone: ${routine.timezone}` : null,
20
+ routine.schedule_human ? `Schedule: ${routine.schedule_human}` : null,
21
+ ].filter(Boolean).join(" | ");
22
+
23
+ const messageContent = isUpdate
24
+ ? `[ROUTINE_UPDATE] The user updated routine "${routine.name}" (ID: ${routine.id}). ${scheduleInfo ? scheduleInfo + ". " : ""}Instruction: ${routine.instruction || "unchanged"}. Please update your cron job${routine.openclaw_cron_id ? ` (cron ID: ${routine.openclaw_cron_id})` : ""} accordingly and call clawaxis_update_routine to confirm the changes.`
25
+ : `[ROUTINE_REQUEST] The user created a new routine: "${routine.name}". ${scheduleInfo ? scheduleInfo + ". " : ""}Instruction: ${routine.instruction}. Please create a cron job for this and call clawaxis_notify_routine with the cron ID and details to register it in the app.`;
26
+
27
+ console.log(`${LOG_PREFIX} Dispatching routine to agent: ${routine.name} (${isUpdate ? "update" : "new"})`);
28
+
29
+ await dispatchSystemMessage(messageContent, `routine-sync-${routine.id}`, `routine-sync-${routine.id}`);
30
+
31
+ const ws = getWs();
32
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
33
+ ws.send(JSON.stringify({
34
+ type: "routine_synced",
35
+ routine_id: routine.id
36
+ }));
37
+ }
38
+
39
+ await relayLog(
40
+ "system",
41
+ `Routine ${isUpdate ? "update" : "request"} dispatched to agent: ${routine.name}`,
42
+ "info",
43
+ { routine_id: routine.id, instruction: routine.instruction }
44
+ );
45
+
46
+ } catch (err: any) {
47
+ console.error(`${LOG_PREFIX} Failed to process routine ${routine.id}:`, err);
48
+ await relayLog(
49
+ "error",
50
+ `Failed to dispatch routine: ${routine.name}`,
51
+ "error",
52
+ { routine_id: routine.id, error: err.message }
53
+ );
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,64 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { LOG_PREFIX } from "../config.js";
3
+ import { relayFetch } from "../utils.js";
4
+ import { toolOk, toolFail, wsSendOrHttp } from "./helpers.js";
5
+
6
+ export function registerContentTools(api: any) {
7
+ api.registerTool({
8
+ name: "clawaxis_post_content",
9
+ description: "Post content to the ClawAxis app for user review. Requires title, body, and content_type. See CLAWAXIS.md for content_type rendering rules, character limits, and media requirements.",
10
+ parameters: Type.Object({
11
+ title: Type.String({ description: "Content title. REQUIRED. Used as card header and for identification." }),
12
+ body: Type.String({ description: "Full content body. Plain text for social platforms, Markdown for blog/report/email/ad, raw HTML for website." }),
13
+ content_type: Type.String({ description: "REQUIRED. Determines rendering: twitter, instagram, linkedin, facebook, tiktok, youtube, website, blog, report, email, ad, document, other" }),
14
+ platform: Type.Optional(Type.String({ description: "Legacy field. Use content_type instead." })),
15
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Tag strings. For instagram/tiktok these become #hashtags. For blog/report these are pill badges. Provide without # prefix." })),
16
+ seo_keywords: Type.Optional(Type.Array(Type.String(), { description: "SEO keyword phrases. Shown in blog/report card footer." })),
17
+ word_count: Type.Optional(Type.Number({ description: "Word count. Shown in blog/report/email card headers. Always provide for long-form content." })),
18
+ prompt_used: Type.Optional(Type.String({ description: "The prompt that generated this content. Shown in Generation Info section." })),
19
+ model_used: Type.Optional(Type.String({ description: "Model ID (e.g. 'anthropic/claude-sonnet-4-5'). Shown in Generation Info section." })),
20
+ media: Type.Optional(Type.Any({ description: "Media object: {images: [{url: string, alt?: string}], videos: [{url: string, alt?: string}]}. Use permanent URLs only (upload via clawaxis_upload_media first). Required for twitter, instagram, facebook, tiktok, youtube." })),
21
+ }),
22
+ async execute(_id: string, params: any) {
23
+ console.log(`${LOG_PREFIX} Sending content via WebSocket/HTTP...`);
24
+ const result = await wsSendOrHttp(
25
+ { type: "content", action: "create", ...params },
26
+ "POST", "content", params,
27
+ );
28
+ return result ? toolOk("Content posted to ClawAxis app", { result }) : toolFail("Failed to post content");
29
+ },
30
+ });
31
+
32
+ api.registerTool({
33
+ name: "clawaxis_get_content",
34
+ description: "Retrieve content from the ClawAxis app. Get recent content list or fetch a specific item by ID.",
35
+ parameters: Type.Object({
36
+ content_id: Type.Optional(Type.String({ description: "UUID of specific content item to fetch. If omitted, returns recent content list." })),
37
+ }),
38
+ async execute(_id: string, params: any) {
39
+ const path = params.content_id ? `content?id=${params.content_id}` : "content";
40
+ const result = await relayFetch("GET", path, null);
41
+ return result
42
+ ? toolOk(JSON.stringify(result, null, 2), { result })
43
+ : toolFail("Failed to retrieve content");
44
+ },
45
+ });
46
+
47
+ api.registerTool({
48
+ name: "clawaxis_update_content",
49
+ description: "Update existing content in the ClawAxis app by content_id. Relay auto-increments edit_count and preserves original_body on first edit.",
50
+ parameters: Type.Object({
51
+ content_id: Type.String({ description: "UUID of the content to update (from a previous clawaxis_post_content or clawaxis_get_content call)" }),
52
+ body: Type.String({ description: "The complete updated body. Same format rules as post: plain text for social, Markdown for blog/report, HTML for website." }),
53
+ title: Type.Optional(Type.String({ description: "Updated title. Omit to keep existing." })),
54
+ media: Type.Optional(Type.Any({ description: "Updated media: {images: [{url: string, alt?: string}], videos: [{url: string, alt?: string}]}. Omit to keep existing." })),
55
+ }),
56
+ async execute(_id: string, params: any) {
57
+ const result = await wsSendOrHttp(
58
+ { type: "content", action: "update", content_id: params.content_id, body: params.body, title: params.title, media: params.media },
59
+ "POST", "content-update", params,
60
+ );
61
+ return result ? toolOk("Content updated successfully", { result }) : toolFail("Failed to update content");
62
+ },
63
+ });
64
+ }
@@ -0,0 +1,40 @@
1
+ import WS from "ws";
2
+ import { getWs, getIsAuthenticated } from "../runtime.js";
3
+ import { relayFetch } from "../utils.js";
4
+
5
+ export interface ToolResult {
6
+ content: Array<{ type: "text"; text: string }>;
7
+ details: Record<string, any>;
8
+ }
9
+
10
+ export function toolOk(message: string, extra?: Record<string, any>): ToolResult {
11
+ return {
12
+ content: [{ type: "text", text: message }],
13
+ details: { success: true, ...extra },
14
+ };
15
+ }
16
+
17
+ export function toolFail(message: string, extra?: Record<string, any>): ToolResult {
18
+ return {
19
+ content: [{ type: "text", text: message }],
20
+ details: { success: false, ...extra },
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Send a payload over WebSocket if connected, otherwise fall back to HTTP.
26
+ * Returns the result (truthy on WS success, parsed JSON on HTTP, null on failure).
27
+ */
28
+ export async function wsSendOrHttp(
29
+ wsPayload: Record<string, any>,
30
+ httpMethod: string,
31
+ httpPath: string,
32
+ httpBody?: any,
33
+ ): Promise<any> {
34
+ const ws = getWs();
35
+ if (ws?.readyState === WS.OPEN && getIsAuthenticated()) {
36
+ (ws as any).send(JSON.stringify(wsPayload));
37
+ return true;
38
+ }
39
+ return relayFetch(httpMethod, httpPath, httpBody ?? wsPayload);
40
+ }
@@ -0,0 +1,16 @@
1
+ import { registerContentTools } from "./content.js";
2
+ import { registerLoggingTools } from "./logging.js";
3
+ import { registerSyncTools } from "./sync.js";
4
+ import { registerRoutineTools } from "./routines.js";
5
+ import { registerMediaTools } from "./media.js";
6
+ import { registerMemoryTools, setMemoryAgentToken } from "./memory.js";
7
+
8
+ export function registerTools(api: any, agentToken: string) {
9
+ setMemoryAgentToken(agentToken);
10
+ registerContentTools(api);
11
+ registerLoggingTools(api);
12
+ registerSyncTools(api);
13
+ registerRoutineTools(api);
14
+ registerMediaTools(api);
15
+ registerMemoryTools(api);
16
+ }
@@ -0,0 +1,33 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { relayLog } from "../utils.js";
3
+ import { toolOk } from "./helpers.js";
4
+
5
+ export function registerLoggingTools(api: any) {
6
+ api.registerTool({
7
+ name: "clawaxis_post_log",
8
+ description: "Log activity to the ClawAxis dashboard Recent Activity feed. Use for task progress, errors, and milestones.",
9
+ parameters: Type.Object({
10
+ action: Type.String({ description: "Action type. MUST be one of: chat_message, content_generate, content_post, web_search, web_fetch, browser_action, cron_execute, skill_update, file_operation, system, error, tool_call" }),
11
+ detail: Type.String({ description: "Human-readable description of what happened (e.g. 'Generated blog post: AI Governance in Automotive')" }),
12
+ level: Type.Optional(Type.String({ description: "Log level. Must be: info, success, warning, or error. Default: info. Use 'success' for completed actions, 'error' for failures." })),
13
+ tokens_used: Type.Optional(Type.Number({ description: "Token count consumed by this action" })),
14
+ cost: Type.Optional(Type.Number({ description: "Cost in USD (e.g. 0.015)" })),
15
+ duration_ms: Type.Optional(Type.Number({ description: "How long the action took in milliseconds" })),
16
+ metadata: Type.Optional(Type.Any({ description: "Additional context as JSON object (e.g. {tool_name: 'generate_image'})" })),
17
+ }),
18
+ async execute(_id: string, params: any) {
19
+ await relayLog(
20
+ params.action,
21
+ params.detail,
22
+ params.level as any || "info",
23
+ {
24
+ tokens_used: params.tokens_used,
25
+ cost: params.cost,
26
+ duration_ms: params.duration_ms,
27
+ ...params.metadata
28
+ }
29
+ );
30
+ return toolOk("Log posted to ClawAxis app");
31
+ },
32
+ });
33
+ }
@@ -0,0 +1,21 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { relayFetch } from "../utils.js";
3
+ import { toolOk, toolFail } from "./helpers.js";
4
+
5
+ export function registerMediaTools(api: any) {
6
+ api.registerTool({
7
+ name: "clawaxis_upload_media",
8
+ description: "Upload an image to permanent R2 storage from a temporary URL. Returns a permanent public URL for use in content.",
9
+ parameters: Type.Object({
10
+ image_url: Type.String({ description: "The temporary image URL to upload to permanent storage" }),
11
+ filename: Type.Optional(Type.String({ description: "Custom filename without extension. Defaults to timestamp." })),
12
+ folder: Type.Optional(Type.String({ description: "Subfolder in bucket. Defaults to agent ID." })),
13
+ }),
14
+ async execute(_id: string, params: any) {
15
+ const result = await relayFetch("POST", "media-upload", params);
16
+ return result?.ok
17
+ ? toolOk(`Image uploaded successfully: ${result.url}`, result)
18
+ : toolFail("Failed to upload image", { result });
19
+ },
20
+ });
21
+ }