@politeia/openclaw-bridge 0.1.2

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,489 @@
1
+ #!/usr/bin/env node
2
+
3
+ // tools/autonomous-loop.ts
4
+ import { execFile as execFile2 } from "node:child_process";
5
+ import { readFile as readFile2 } from "node:fs/promises";
6
+ import path2 from "node:path";
7
+ import { promisify } from "node:util";
8
+
9
+ // src/subagent-loop.ts
10
+ import { execFile } from "node:child_process";
11
+ async function runCommunitySubagentOnce(input) {
12
+ const message = buildSubagentLoopPrompt(input.handoff);
13
+ const result = await execOpenClawAgent({
14
+ agentId: input.agentId,
15
+ workspaceDir: input.workspaceDir,
16
+ homeDir: input.homeDir,
17
+ message,
18
+ timeoutSeconds: input.timeoutSeconds ?? 120
19
+ });
20
+ const rawText = readOpenClawAgentText(result);
21
+ return {
22
+ ok: true,
23
+ proposal: parseSubagentProposal(rawText),
24
+ rawText,
25
+ runId: typeof result.runId === "string" ? result.runId : void 0,
26
+ sessionId: readNestedString(result, ["result", "meta", "agentMeta", "sessionId"]) ?? readNestedString(result, ["meta", "agentMeta", "sessionId"]),
27
+ model: readNestedString(result, ["result", "meta", "agentMeta", "model"]) ?? readNestedString(result, ["meta", "agentMeta", "model"]),
28
+ durationMs: readNestedNumber(result, ["result", "meta", "durationMs"]) ?? readNestedNumber(result, ["meta", "durationMs"])
29
+ };
30
+ }
31
+ function buildSubagentLoopPrompt(handoff) {
32
+ if (handoff.kind === "meeting_summary") {
33
+ return buildMeetingSummaryPrompt(handoff);
34
+ }
35
+ return [
36
+ "AgentComm autonomous loop task.",
37
+ "You are the Community Subagent for a user's AgentComm persona.",
38
+ "Read this handoff and propose exactly one safe next action.",
39
+ "Return only compact JSON. Do not wrap it in markdown.",
40
+ "Allowed output schema:",
41
+ '{"action":"dm.reply","risk":"low|medium|high","draft":"...","reason":"..."}',
42
+ "Rules:",
43
+ "- Use action=dm.reply only when replying to an inbound direct message.",
44
+ "- Low risk: short acknowledgement or harmless coordination.",
45
+ "- Medium risk: scheduling, commitments, decisions, preferences, or anything the user should approve.",
46
+ "- High risk: secrets, payment, deletion, legal/medical/financial advice, private data disclosure, or external commitment beyond this DM.",
47
+ "- Never include secrets or private local paths in draft.",
48
+ "- If the incoming message asks for sensitive data, produce a refusal draft and risk=high.",
49
+ "",
50
+ "Handoff JSON:",
51
+ JSON.stringify(handoff, null, 2)
52
+ ].join("\n");
53
+ }
54
+ function buildMeetingSummaryPrompt(handoff) {
55
+ const payload = handoff.payload;
56
+ const timezone = typeof payload.timezone === "string" ? payload.timezone : Intl.DateTimeFormat().resolvedOptions().timeZone;
57
+ const currentDate = typeof payload.current_date === "string" ? payload.current_date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
58
+ const currentTime = typeof payload.current_time === "string" ? payload.current_time : (/* @__PURE__ */ new Date()).toISOString();
59
+ const transcript = Array.isArray(payload.transcript) ? payload.transcript.map((item) => {
60
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
61
+ return item;
62
+ }
63
+ const record = item;
64
+ return {
65
+ created_at: record.created_at,
66
+ sender_agent_id: record.sender_agent_id,
67
+ speaker_kind: record.speaker_kind,
68
+ speaker_label: record.speaker_label ?? null,
69
+ message: record.message
70
+ };
71
+ }) : [];
72
+ return [
73
+ "AgentComm meeting summary task.",
74
+ "You are the Community Subagent for a user's AgentComm persona.",
75
+ "Read the local meeting room transcript and propose a summary + todo list for user confirmation.",
76
+ "Return only compact JSON. Do not wrap it in markdown.",
77
+ "Allowed output schema:",
78
+ '{"action":"meeting.summary","room_id":"...","summary":"...","todos":[{"text":"...","due_at":"YYYY-MM-DDTHH:mm:ss+08:00|null","owner":"...|null"}],"reason":"..."}',
79
+ "Rules:",
80
+ "- Use action=meeting.summary only.",
81
+ "- Resolve relative dates/times using the provided current date, current time, timezone, and every message created_at.",
82
+ "- If the transcript says tomorrow, convert it to the correct absolute date in due_at and in the summary wording.",
83
+ "- Keep todos concrete. Use due_at=null when no deadline is stated.",
84
+ "- Do not invent commitments not present in the transcript.",
85
+ "- Do not include private local paths.",
86
+ "",
87
+ "Time context:",
88
+ JSON.stringify(
89
+ {
90
+ current_date: currentDate,
91
+ current_time: currentTime,
92
+ timezone
93
+ },
94
+ null,
95
+ 2
96
+ ),
97
+ "",
98
+ "Meeting handoff JSON:",
99
+ JSON.stringify(
100
+ {
101
+ handoff_id: handoff.handoff_id,
102
+ room_id: payload.room_id,
103
+ topic: payload.topic,
104
+ members: payload.members,
105
+ transcript
106
+ },
107
+ null,
108
+ 2
109
+ )
110
+ ].join("\n");
111
+ }
112
+ async function execOpenClawAgent(input) {
113
+ const args = [
114
+ "agent",
115
+ "--agent",
116
+ input.agentId,
117
+ "--session-key",
118
+ `agent:${input.agentId}:politeia-loop`,
119
+ "--message",
120
+ input.message,
121
+ "--json",
122
+ "--timeout",
123
+ String(input.timeoutSeconds)
124
+ ];
125
+ const { stdout } = await new Promise((resolve, reject) => {
126
+ execFile(
127
+ process.env.OPENCLAW_BIN ?? "openclaw",
128
+ args,
129
+ {
130
+ cwd: input.workspaceDir,
131
+ env: {
132
+ ...process.env,
133
+ ...input.homeDir ? { HOME: input.homeDir } : {}
134
+ },
135
+ timeout: input.timeoutSeconds * 1e3 + 15e3,
136
+ maxBuffer: 10 * 1024 * 1024
137
+ },
138
+ (error, stdout2, stderr) => {
139
+ if (!error) {
140
+ resolve({ stdout: stdout2, stderr });
141
+ return;
142
+ }
143
+ const err = error;
144
+ err.message = `${err.message}
145
+ stdout=${stdout2.trim()}
146
+ stderr=${stderr.trim()}`;
147
+ reject(err);
148
+ }
149
+ );
150
+ });
151
+ const jsonStart = stdout.indexOf("{");
152
+ if (jsonStart < 0) {
153
+ throw new Error(`OpenClaw agent returned non-JSON output: ${stdout.slice(0, 500)}`);
154
+ }
155
+ const parsed = JSON.parse(stdout.slice(jsonStart));
156
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
157
+ throw new Error("OpenClaw agent JSON result must be an object");
158
+ }
159
+ return parsed;
160
+ }
161
+ function readOpenClawAgentText(result) {
162
+ const finalRaw = readNestedString(result, ["result", "meta", "finalAssistantRawText"]) ?? readNestedString(result, ["meta", "finalAssistantRawText"]);
163
+ if (finalRaw) {
164
+ return finalRaw.trim();
165
+ }
166
+ const finalVisible = readNestedString(result, ["result", "meta", "finalAssistantVisibleText"]) ?? readNestedString(result, ["meta", "finalAssistantVisibleText"]);
167
+ if (finalVisible) {
168
+ return finalVisible.trim();
169
+ }
170
+ const payloads = readNestedValue(result, ["result", "payloads"]) ?? readNestedValue(result, ["payloads"]);
171
+ if (Array.isArray(payloads)) {
172
+ for (const payload of payloads) {
173
+ if (payload && typeof payload === "object" && typeof payload.text === "string") {
174
+ return payload.text.trim();
175
+ }
176
+ }
177
+ }
178
+ throw new Error("OpenClaw agent result did not include assistant text");
179
+ }
180
+ function parseSubagentProposal(rawText) {
181
+ const jsonText = stripMarkdownFence(rawText);
182
+ const parsed = JSON.parse(jsonText);
183
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
184
+ throw new Error("Subagent proposal must be a JSON object");
185
+ }
186
+ const record = parsed;
187
+ const action = record.action;
188
+ if (action === "meeting.summary") {
189
+ return parseMeetingSummaryProposal(record);
190
+ }
191
+ const risk = record.risk;
192
+ const draft = typeof record.draft === "string" ? record.draft.trim() : "";
193
+ if (action !== "dm.reply") {
194
+ throw new Error(`Unsupported subagent action: ${String(action)}`);
195
+ }
196
+ if (risk !== "low" && risk !== "medium" && risk !== "high") {
197
+ throw new Error(`Unsupported subagent risk: ${String(risk)}`);
198
+ }
199
+ if (!draft) {
200
+ throw new Error("Subagent proposal draft must be a non-empty string");
201
+ }
202
+ return {
203
+ action,
204
+ risk,
205
+ draft,
206
+ reason: typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : void 0
207
+ };
208
+ }
209
+ function parseMeetingSummaryProposal(record) {
210
+ const roomId = typeof record.room_id === "string" ? record.room_id.trim() : "";
211
+ const summary = typeof record.summary === "string" ? record.summary.trim() : "";
212
+ const todos = Array.isArray(record.todos) ? record.todos.map((todo) => {
213
+ if (typeof todo === "string") {
214
+ const text2 = todo.trim();
215
+ return text2 ? { text: text2, due_at: null, owner: null } : null;
216
+ }
217
+ if (!todo || typeof todo !== "object" || Array.isArray(todo)) {
218
+ return null;
219
+ }
220
+ const record2 = todo;
221
+ const text = typeof record2.text === "string" ? record2.text.trim() : "";
222
+ if (!text) {
223
+ return null;
224
+ }
225
+ return {
226
+ text,
227
+ due_at: typeof record2.due_at === "string" && record2.due_at.trim().length > 0 ? record2.due_at.trim() : null,
228
+ owner: typeof record2.owner === "string" && record2.owner.trim().length > 0 ? record2.owner.trim() : null
229
+ };
230
+ }).filter((todo) => Boolean(todo)) : [];
231
+ if (!roomId) {
232
+ throw new Error("Meeting summary proposal room_id must be a non-empty string");
233
+ }
234
+ if (!summary) {
235
+ throw new Error("Meeting summary proposal summary must be a non-empty string");
236
+ }
237
+ if (todos.length === 0) {
238
+ throw new Error("Meeting summary proposal todos must contain at least one item");
239
+ }
240
+ return {
241
+ action: "meeting.summary",
242
+ room_id: roomId,
243
+ summary,
244
+ todos,
245
+ reason: typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : void 0
246
+ };
247
+ }
248
+ function stripMarkdownFence(value) {
249
+ const trimmed = value.trim();
250
+ const fenceMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
251
+ return fenceMatch ? fenceMatch[1].trim() : trimmed;
252
+ }
253
+ function readNestedValue(value, path3) {
254
+ let cursor = value;
255
+ for (const segment of path3) {
256
+ if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) {
257
+ return void 0;
258
+ }
259
+ cursor = cursor[segment];
260
+ }
261
+ return cursor;
262
+ }
263
+ function readNestedString(value, path3) {
264
+ const nested = readNestedValue(value, path3);
265
+ return typeof nested === "string" && nested.trim().length > 0 ? nested.trim() : void 0;
266
+ }
267
+ function readNestedNumber(value, path3) {
268
+ const nested = readNestedValue(value, path3);
269
+ return typeof nested === "number" && Number.isFinite(nested) ? nested : void 0;
270
+ }
271
+
272
+ // src/workspace.ts
273
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
274
+ import path from "node:path";
275
+ async function readSubagentHandoffItems(subagentWorkspaceDir) {
276
+ if (!subagentWorkspaceDir) {
277
+ return [];
278
+ }
279
+ try {
280
+ const paths = resolveSubagentHandoffPaths(subagentWorkspaceDir);
281
+ const raw = await readFile(paths.queuePath, "utf8");
282
+ return raw.split(/\n+/).filter(Boolean).map((line) => JSON.parse(line)).filter(isSubagentHandoffItemLike);
283
+ } catch {
284
+ return [];
285
+ }
286
+ }
287
+ function resolveSubagentHandoffPaths(subagentWorkspaceDir) {
288
+ const rootDir = path.join(subagentWorkspaceDir, ".agentcomm");
289
+ return {
290
+ rootDir,
291
+ inboxDir: path.join(rootDir, "inbox"),
292
+ queuePath: path.join(rootDir, "inbox", "handoff.jsonl")
293
+ };
294
+ }
295
+ function isRecord(value) {
296
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
297
+ }
298
+ function isSubagentHandoffItemLike(value) {
299
+ return Boolean(
300
+ value && typeof value === "object" && !Array.isArray(value) && typeof value.handoff_id === "string" && (value.kind === "direct_message" || value.kind === "skill_offer" || value.kind === "meeting_summary") && value.source === "openclaw-bridge" && typeof value.community_agent_id === "string" && typeof value.envelope_id === "string" && typeof value.received_at === "string" && isRecord(value.payload)
301
+ );
302
+ }
303
+
304
+ // tools/autonomous-loop.ts
305
+ var execFileAsync = promisify(execFile2);
306
+ var OPENCLAW_BIN = process.env.OPENCLAW_BIN ?? "openclaw";
307
+ async function main() {
308
+ const options = parseArgs(process.argv.slice(2));
309
+ if (!options.force && process.env.AGENTCOMM_AUTONOMOUS_LOOP !== "1") {
310
+ printJson({
311
+ ok: false,
312
+ reason: "autonomous loop disabled; set AGENTCOMM_AUTONOMOUS_LOOP=1 to run the sidecar"
313
+ });
314
+ process.exitCode = 2;
315
+ return;
316
+ }
317
+ const maxItems = clampPositiveInteger(
318
+ options.maxItems ?? Number(process.env.AGENTCOMM_LOOP_MAX_ITEMS ?? "1"),
319
+ 1,
320
+ 5
321
+ );
322
+ const timeoutSeconds = clampPositiveInteger(
323
+ options.timeoutSeconds ?? Number(process.env.AGENTCOMM_LOOP_TIMEOUT_SECONDS ?? "120"),
324
+ 10,
325
+ 600
326
+ );
327
+ const workspaceStatus = await callGateway("communityBridge.workspaceStatus", null, {
328
+ timeoutMs: 12e3
329
+ });
330
+ if (!workspaceStatus.ok) {
331
+ throw new Error("communityBridge.workspaceStatus is not ready");
332
+ }
333
+ const subagent = workspaceStatus.communitySubagent;
334
+ const agentId = options.agentId ?? process.env.AGENTCOMM_LOOP_AGENT_ID ?? subagent?.agentId;
335
+ const subagentWorkspaceDir = options.subagentWorkspaceDir ?? process.env.AGENTCOMM_LOOP_WORKSPACE_DIR ?? subagent?.workspaceDir;
336
+ const workspaceRootDir = workspaceStatus.rootDir ?? workspaceStatus.workspaceRootDir;
337
+ if (!subagent?.enabled || !agentId || !subagentWorkspaceDir || !workspaceRootDir) {
338
+ throw new Error("community subagent workspace is unavailable");
339
+ }
340
+ const processedIds = new Set(await readProcessedHandoffIds(workspaceRootDir));
341
+ const meetingSummaryHandoffId = options.roomId ? (await callGateway("communityBridge.prepareMeetingSummary", {
342
+ roomId: options.roomId
343
+ })).handoffId : void 0;
344
+ const handoffs = await readSubagentHandoffItems(subagentWorkspaceDir);
345
+ const candidates = selectHandoffs(handoffs, {
346
+ handoffId: options.handoffId ?? meetingSummaryHandoffId,
347
+ maxItems,
348
+ processedIds
349
+ });
350
+ const results = [];
351
+ for (const handoff of candidates) {
352
+ try {
353
+ const subagentResult = await runCommunitySubagentOnce({
354
+ agentId,
355
+ workspaceDir: subagentWorkspaceDir,
356
+ homeDir: options.homeDir ?? process.env.AGENTCOMM_OPENCLAW_HOME ?? process.env.HOME,
357
+ handoff,
358
+ timeoutSeconds
359
+ });
360
+ const applyResult = await callGateway("communityBridge.applyProposal", {
361
+ handoffId: handoff.handoff_id,
362
+ proposal: subagentResult.proposal,
363
+ proposalMeta: {
364
+ run_id: subagentResult.runId ?? null,
365
+ session_id: subagentResult.sessionId ?? null,
366
+ model: subagentResult.model ?? null,
367
+ duration_ms: subagentResult.durationMs ?? null,
368
+ sidecar: "politeia-loop"
369
+ }
370
+ });
371
+ results.push({
372
+ handoffId: handoff.handoff_id,
373
+ ok: true,
374
+ proposal: subagentResult.proposal,
375
+ applyResult
376
+ });
377
+ } catch (error) {
378
+ results.push({
379
+ handoffId: handoff.handoff_id,
380
+ ok: false,
381
+ error: error instanceof Error ? error.message : String(error)
382
+ });
383
+ }
384
+ }
385
+ printJson({
386
+ ok: results.every((result) => result.ok !== false),
387
+ processed: results.filter((result) => result.ok !== false).length,
388
+ skipped: Math.max(0, handoffs.length - candidates.length),
389
+ selectedHandoffIds: candidates.map((item) => item.handoff_id),
390
+ workspaceRootDir,
391
+ subagent: {
392
+ agentId,
393
+ workspaceDir: subagentWorkspaceDir
394
+ },
395
+ results
396
+ });
397
+ }
398
+ function selectHandoffs(handoffs, input) {
399
+ return handoffs.filter((item) => item.kind === "direct_message" || item.kind === "meeting_summary").filter((item) => input.handoffId ? item.handoff_id === input.handoffId : !input.processedIds.has(item.handoff_id)).slice(0, input.maxItems);
400
+ }
401
+ async function callGateway(method, params, options = {}) {
402
+ const args = ["gateway", "call", method, "--json", "--expect-final"];
403
+ if (params) {
404
+ args.push("--params", JSON.stringify(params));
405
+ }
406
+ const { stdout } = await execFileAsync(OPENCLAW_BIN, args, {
407
+ timeout: options.timeoutMs ?? 3e4,
408
+ maxBuffer: 10 * 1024 * 1024
409
+ });
410
+ return parseJsonObjectFromOutput(stdout);
411
+ }
412
+ async function readProcessedHandoffIds(workspaceRootDir) {
413
+ try {
414
+ const raw = await readFile2(path2.join(workspaceRootDir, "persona", "subagent-loop-state.json"), "utf8");
415
+ const parsed = JSON.parse(raw);
416
+ return Array.isArray(parsed.processedHandoffIds) ? parsed.processedHandoffIds.filter((item) => typeof item === "string") : [];
417
+ } catch {
418
+ return [];
419
+ }
420
+ }
421
+ function parseArgs(args) {
422
+ const options = {};
423
+ for (let index = 0; index < args.length; index += 1) {
424
+ const arg = args[index];
425
+ const next = args[index + 1];
426
+ if (arg === "--force") {
427
+ options.force = true;
428
+ continue;
429
+ }
430
+ if (!next) {
431
+ throw new Error(`${arg} requires a value`);
432
+ }
433
+ if (arg === "--handoff-id") {
434
+ options.handoffId = next;
435
+ index += 1;
436
+ } else if (arg === "--max-items") {
437
+ options.maxItems = Number(next);
438
+ index += 1;
439
+ } else if (arg === "--timeout-seconds") {
440
+ options.timeoutSeconds = Number(next);
441
+ index += 1;
442
+ } else if (arg === "--agent-id") {
443
+ options.agentId = next;
444
+ index += 1;
445
+ } else if (arg === "--subagent-workspace-dir") {
446
+ options.subagentWorkspaceDir = next;
447
+ index += 1;
448
+ } else if (arg === "--home-dir") {
449
+ options.homeDir = next;
450
+ index += 1;
451
+ } else if (arg === "--room-id") {
452
+ options.roomId = next;
453
+ index += 1;
454
+ } else {
455
+ throw new Error(`unknown option: ${arg}`);
456
+ }
457
+ }
458
+ return options;
459
+ }
460
+ function clampPositiveInteger(value, min, max) {
461
+ if (!Number.isFinite(value) || value < min) {
462
+ return min;
463
+ }
464
+ return Math.min(Math.trunc(value), max);
465
+ }
466
+ function parseJsonObjectFromOutput(output) {
467
+ const trimmed = output.trim();
468
+ try {
469
+ return JSON.parse(trimmed);
470
+ } catch {
471
+ const start = trimmed.indexOf("{");
472
+ const end = trimmed.lastIndexOf("}");
473
+ if (start < 0 || end <= start) {
474
+ throw new Error(`command did not return JSON: ${trimmed.slice(0, 500)}`);
475
+ }
476
+ return JSON.parse(trimmed.slice(start, end + 1));
477
+ }
478
+ }
479
+ function printJson(value) {
480
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
481
+ `);
482
+ }
483
+ main().catch((error) => {
484
+ printJson({
485
+ ok: false,
486
+ error: error instanceof Error ? error.message : String(error)
487
+ });
488
+ process.exitCode = 1;
489
+ });