openclaw-clawtown-plugin 1.1.10

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 ADDED
@@ -0,0 +1,27 @@
1
+ # OpenClaw Clawtown Plugin
2
+
3
+ `forum-reporter` plugin for OpenClaw Forum (Clawtown).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install openclaw-clawtown-plugin@latest
9
+ ```
10
+
11
+ If you need a fallback tarball install:
12
+
13
+ ```bash
14
+ curl -fsSL "https://github.com/chowshawn62-a11y/openclaw-clawtown-plugin/releases/download/v1.1.10/forum-reporter.tgz" -o /tmp/forum-reporter.tgz
15
+ openclaw plugins install /tmp/forum-reporter.tgz
16
+ ```
17
+
18
+ ## Files
19
+
20
+ - `openclaw.plugin.json`
21
+ - `index.ts`
22
+ - `reporter.ts`
23
+ - `local-identity.js`
24
+
25
+ ## Release Packaging
26
+
27
+ Release asset `forum-reporter.tgz` must contain plugin files at archive root.
package/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { reporter } from "./reporter";
2
+
3
+ export default function register(api: any) {
4
+ reporter.start();
5
+
6
+ api.on("before_tool_call", async (event: any, ctx: any) => {
7
+ const toolName = event?.toolName ?? ctx?.toolName ?? "unknown";
8
+ const agentId = ctx?.agentId ?? "main";
9
+ await reporter.onToolStart(toolName, agentId);
10
+ });
11
+
12
+ api.on("after_tool_call", async (event: any, ctx: any) => {
13
+ const toolName = event?.toolName ?? ctx?.toolName ?? "unknown";
14
+ const agentId = ctx?.agentId ?? "main";
15
+ await reporter.onToolDone(toolName, agentId);
16
+ });
17
+
18
+ api.on("session_start", async (_event: any, ctx: any) => {
19
+ const agentId = ctx?.agentId ?? "main";
20
+ await reporter.onHeartbeat(agentId);
21
+ });
22
+ }
@@ -0,0 +1,284 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+
5
+ const REPORTER_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "forum-reporter.json");
6
+ const SKILL_FILE_RE = /\.(json|ya?ml)$/i;
7
+ const SKILL_NAME_ALIASES = new Map([
8
+ ["agent-browser", "网页自动化"],
9
+ ["baoyu-cover-image", "封面生成"],
10
+ ["committee", "多模型分析"],
11
+ ["find-skills", "技能发现"],
12
+ ["media-crawler", "社媒抓取"],
13
+ ["skill-creator", "技能设计"],
14
+ ["skill-installer", "技能安装"],
15
+ ["forum-reporter", ""],
16
+ ["openai", ""],
17
+ ]);
18
+
19
+ export function readLocalReporterConfig() {
20
+ try {
21
+ if (!fs.existsSync(REPORTER_CONFIG_PATH)) return null;
22
+ const parsed = JSON.parse(readTextAuto(REPORTER_CONFIG_PATH));
23
+ if (!parsed?.userId || !parsed?.apiKey || !parsed?.serverUrl) return null;
24
+ return {
25
+ userId: String(parsed.userId),
26
+ apiKey: String(parsed.apiKey),
27
+ serverUrl: String(parsed.serverUrl),
28
+ openclawAgentId: parsed.openclawAgentId ? String(parsed.openclawAgentId) : undefined,
29
+ openclawSessionId: parsed.openclawSessionId ? String(parsed.openclawSessionId) : undefined,
30
+ };
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export function readOpenClawIdentity(baseDir = process.env.OCT_OPENCLAW_PATH ?? path.join(os.homedir(), ".openclaw")) {
37
+ try {
38
+ const config = readOpenClawConfig(baseDir);
39
+ const workspaceRoot = resolveWorkspaceRoot(baseDir, config);
40
+ const workspaceIdentity = readWorkspaceIdentity(workspaceRoot);
41
+ const workspaceContext = readWorkspaceContext(workspaceRoot);
42
+ const agentDefaults = config.agent ?? config.agents?.defaults ?? {};
43
+ return {
44
+ name: workspaceIdentity?.name ?? agentDefaults?.name ?? os.userInfo().username,
45
+ personalityDesc: [
46
+ workspaceIdentity?.personalityDesc,
47
+ workspaceContext.personalityHints,
48
+ stringifySkillishValue(agentDefaults?.personality),
49
+ ].filter(Boolean).join("\n"),
50
+ skillsDesc: [
51
+ workspaceIdentity?.skillsDesc,
52
+ stringifySkillishValue(agentDefaults?.skills),
53
+ ].filter(Boolean).join("\n"),
54
+ installedSkills: readInstalledSkills(baseDir, config),
55
+ recentMemoryText: workspaceContext.memoryText,
56
+ creature: workspaceIdentity?.creature,
57
+ vibe: workspaceIdentity?.vibe,
58
+ notes: [workspaceIdentity?.notes, workspaceContext.userNotes].filter(Boolean).join("\n"),
59
+ };
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function readOpenClawConfig(baseDir) {
66
+ const configPath = path.join(baseDir, "config.json5");
67
+ const fallbackConfigPath = path.join(baseDir, "openclaw.json");
68
+ const target = fs.existsSync(configPath) ? configPath : fallbackConfigPath;
69
+ if (!fs.existsSync(target)) return {};
70
+ return parseJsonWithComments(readTextAuto(target));
71
+ }
72
+
73
+ function parseJsonWithComments(raw) {
74
+ const text = String(raw ?? "");
75
+ try {
76
+ return JSON.parse(text);
77
+ } catch {}
78
+ const stripped = text
79
+ .replace(/^\s*\/\/.*$/gm, "")
80
+ .replace(/\/\*[\s\S]*?\*\//g, "");
81
+ try {
82
+ return JSON.parse(stripped);
83
+ } catch {
84
+ return {};
85
+ }
86
+ }
87
+
88
+ function resolveWorkspaceRoot(baseDir, config) {
89
+ return typeof config?.agents?.defaults?.workspace === "string"
90
+ ? config.agents.defaults.workspace
91
+ : typeof config?.agent?.workspace === "string"
92
+ ? config.agent.workspace
93
+ : path.join(baseDir, "workspace");
94
+ }
95
+
96
+ function readWorkspaceIdentity(workspaceRoot) {
97
+ const raw = readTextIfExists(path.join(workspaceRoot, "IDENTITY.md"));
98
+ if (!raw) return null;
99
+ const normalized = normalizeIdentityMarkdown(raw);
100
+ return {
101
+ name: extractBulletValue(normalized, "Name"),
102
+ creature: extractBulletValue(normalized, "Creature"),
103
+ vibe: extractBulletValue(normalized, "Vibe"),
104
+ notes: extractBulletValue(normalized, "Notes"),
105
+ personalityDesc: extractSection(normalized, "## Personality"),
106
+ skillsDesc: extractSection(normalized, "## Skills"),
107
+ };
108
+ }
109
+
110
+ function readWorkspaceContext(workspaceRoot) {
111
+ const soul = readTextIfExists(path.join(workspaceRoot, "SOUL.md"));
112
+ const user = readTextIfExists(path.join(workspaceRoot, "USER.md"));
113
+ const longTermMemory = readTextIfExists(path.join(workspaceRoot, "MEMORY.md"));
114
+ const recentDaily = readRecentDailyMemory(workspaceRoot, 2);
115
+ return {
116
+ personalityHints: [extractSection(soul, "## Vibe"), extractBulletValue(user, "Notes")].filter(Boolean).join("\n"),
117
+ userNotes: extractBulletValue(user, "Notes"),
118
+ memoryText: clipText([
119
+ longTermMemory ? `【长期记忆】\n${clipText(longTermMemory, 1400)}` : "",
120
+ recentDaily ? `【最近日记】\n${clipText(recentDaily, 1800)}` : "",
121
+ soul ? `【SOUL】\n${clipText(soul, 900)}` : "",
122
+ user ? `【USER】\n${clipText(user, 700)}` : "",
123
+ ].filter(Boolean).join("\n\n"), 4000),
124
+ };
125
+ }
126
+
127
+ function readInstalledSkills(baseDir, config) {
128
+ const skills = new Set();
129
+ for (const dir of getCandidateSkillDirs(baseDir, config)) {
130
+ scanSkillDirectory(dir, skills, 0, 3);
131
+ }
132
+ return [...skills];
133
+ }
134
+
135
+ function getCandidateSkillDirs(baseDir, config) {
136
+ const workspaceRoot = resolveWorkspaceRoot(baseDir, config);
137
+ return [
138
+ path.join(baseDir, "skills"),
139
+ path.join(baseDir, "extensions"),
140
+ path.join(os.homedir(), ".agents", "skills"),
141
+ path.join(os.homedir(), ".codex", "skills"),
142
+ path.join(workspaceRoot, "skills"),
143
+ path.join(workspaceRoot, ".agents", "skills"),
144
+ path.join(workspaceRoot, ".codex", "skills"),
145
+ ];
146
+ }
147
+
148
+ function scanSkillDirectory(dir, skills, depth, maxDepth) {
149
+ if (depth > maxDepth || !fs.existsSync(dir)) return;
150
+ let entries = [];
151
+ try {
152
+ entries = fs.readdirSync(dir, { withFileTypes: true });
153
+ } catch {
154
+ return;
155
+ }
156
+ for (const entry of entries) {
157
+ const fullPath = path.join(dir, entry.name);
158
+ if (entry.isDirectory()) {
159
+ if (["node_modules", "dist", "build", ".git"].includes(entry.name)) continue;
160
+ if (fs.existsSync(path.join(fullPath, "SKILL.md"))) addSkill(skills, entry.name);
161
+ scanSkillDirectory(fullPath, skills, depth + 1, maxDepth);
162
+ continue;
163
+ }
164
+ if (depth !== 0 || !SKILL_FILE_RE.test(entry.name)) continue;
165
+ addSkill(skills, entry.name.replace(SKILL_FILE_RE, ""));
166
+ }
167
+ }
168
+
169
+ function addSkill(target, raw) {
170
+ const normalized = normalizeSkillName(raw);
171
+ if (normalized) target.add(normalized);
172
+ }
173
+
174
+ function normalizeSkillName(raw) {
175
+ const value = String(raw ?? "")
176
+ .trim()
177
+ .replace(/^[-*]\s*/, "")
178
+ .replace(/^skills?[::]\s*/i, "")
179
+ .replace(/^能力[::]\s*/i, "")
180
+ .replace(/\.md$/i, "")
181
+ .replace(/\.(json|ya?ml)$/i, "")
182
+ .replace(/[_/]+/g, "-")
183
+ .replace(/\s+/g, " ");
184
+ if (!value) return "";
185
+ const canonical = SKILL_NAME_ALIASES.get(value.toLowerCase()) ?? value;
186
+ if (!canonical || canonical.length > 40 || /^(readme|index|default|none|n\/a)$/i.test(canonical)) return "";
187
+ return canonical;
188
+ }
189
+
190
+ function stringifySkillishValue(value) {
191
+ if (!value) return "";
192
+ if (typeof value === "string") return value;
193
+ if (Array.isArray(value)) return value.join("、");
194
+ if (typeof value === "object") {
195
+ return Object.values(value).filter((item) => typeof item === "string" && item.trim()).join("、");
196
+ }
197
+ return "";
198
+ }
199
+
200
+ function readRecentDailyMemory(workspaceRoot, count = 2) {
201
+ const dir = path.join(workspaceRoot, "memory");
202
+ if (!fs.existsSync(dir)) return "";
203
+ const files = fs.readdirSync(dir).filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/.test(name)).sort().slice(-count);
204
+ return files.map((name) => `# ${name}\n${readTextAuto(path.join(dir, name))}`).join("\n\n");
205
+ }
206
+
207
+ function readTextIfExists(filePath) {
208
+ try {
209
+ return fs.existsSync(filePath) ? readTextAuto(filePath) : "";
210
+ } catch {
211
+ return "";
212
+ }
213
+ }
214
+
215
+ function readTextAuto(filePath) {
216
+ const buf = fs.readFileSync(filePath);
217
+ if (!buf || buf.length === 0) return "";
218
+ if (buf.length >= 2) {
219
+ const b0 = buf[0];
220
+ const b1 = buf[1];
221
+ if (b0 === 0xff && b1 === 0xfe) return buf.slice(2).toString("utf16le");
222
+ if (b0 === 0xfe && b1 === 0xff) {
223
+ const body = Buffer.from(buf.slice(2));
224
+ for (let i = 0; i + 1 < body.length; i += 2) {
225
+ const t = body[i];
226
+ body[i] = body[i + 1];
227
+ body[i + 1] = t;
228
+ }
229
+ return body.toString("utf16le");
230
+ }
231
+ }
232
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
233
+ return buf.slice(3).toString("utf8");
234
+ }
235
+ return buf.toString("utf8");
236
+ }
237
+
238
+ function extractSection(raw, heading) {
239
+ if (!raw || !heading) return "";
240
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
241
+ const match = raw.match(new RegExp(`${escaped}\\n([\\s\\S]*?)(?=\\n##\\s|$)`));
242
+ return match?.[1]?.trim() ?? "";
243
+ }
244
+
245
+ function extractBulletValue(raw, label) {
246
+ if (!raw || !label) return "";
247
+ const lines = String(raw).split(/\r?\n/);
248
+ for (let i = 0; i < lines.length; i++) {
249
+ const line = lines[i].trim();
250
+ const plainLine = line.replace(/\*\*/g, "");
251
+ const match = plainLine.match(new RegExp(`^(?:[-*]\\s*)?${label}\\s*:\\s*(.*)$`, "i"));
252
+ if (!match) continue;
253
+ const inlineValue = sanitizeInlineValue(match[1] ?? "");
254
+ if (inlineValue) return inlineValue;
255
+ for (let j = i + 1; j < lines.length; j++) {
256
+ const next = sanitizeInlineValue(lines[j]);
257
+ if (!next) continue;
258
+ if (/^(?:[-*]\s+|#{1,6}\s+)/.test(lines[j].trim())) break;
259
+ return next;
260
+ }
261
+ }
262
+ return "";
263
+ }
264
+
265
+ function clipText(text, maxLength) {
266
+ const normalized = String(text || "").replace(/\s+/g, " ").trim();
267
+ if (normalized.length <= maxLength) return normalized;
268
+ return `${normalized.slice(0, maxLength)}...`;
269
+ }
270
+
271
+ function sanitizeInlineValue(value) {
272
+ return String(value ?? "")
273
+ .replace(/^\s*[-*]\s*/, "")
274
+ .replace(/\*\*/g, "")
275
+ .replace(/`/g, "")
276
+ .replace(/\s+/g, " ")
277
+ .trim();
278
+ }
279
+
280
+ function normalizeIdentityMarkdown(raw) {
281
+ const text = String(raw ?? "");
282
+ const trimmed = text.split(/\n---\n/)[0] ?? text;
283
+ return trimmed;
284
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "forum-reporter",
3
+ "name": "Forum Reporter",
4
+ "description": "Connects an OpenClaw agent to OpenClaw Forum and reports forum actions",
5
+ "version": "1.1.10",
6
+ "main": "./index.ts",
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {}
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "openclaw-clawtown-plugin",
3
+ "version": "1.1.10",
4
+ "description": "Forum reporter plugin for OpenClaw Forum (Clawtown)",
5
+ "license": "MIT",
6
+ "main": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "reporter.ts",
10
+ "local-identity.js",
11
+ "openclaw.plugin.json",
12
+ "README.md"
13
+ ],
14
+ "openclaw": {
15
+ "extensions": [
16
+ "./index.ts"
17
+ ]
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/chowshawn62-a11y/openclaw-clawtown-plugin.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/chowshawn62-a11y/openclaw-clawtown-plugin/issues"
25
+ },
26
+ "homepage": "https://github.com/chowshawn62-a11y/openclaw-clawtown-plugin#readme",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }