@iamoberlin/chorus 2.2.0 → 2.2.1
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 +1 -1
- package/idl/chorus_prayers.json +1 -1
- package/index.ts +125 -32
- package/openclaw.plugin.json +8 -2
- package/package.json +1 -1
- package/src/choirs.ts +3 -2
- package/src/prayers/solana.ts +1 -1
- package/src/scheduler.ts +62 -0
package/README.md
CHANGED
|
@@ -280,7 +280,7 @@ When `autonomous: false` (default), all prayer chain interactions require explic
|
|
|
280
280
|
- **TypeScript client** — wraps Anchor IDL with PDA derivation helpers
|
|
281
281
|
- **Anchor events** — `PrayerPosted`, `PrayerAnswered`, `PrayerConfirmed`, `PrayerClaimed`, `PrayerCancelled` for off-chain indexing
|
|
282
282
|
- **Local text cache** — CLI stores full text in `.prayer-texts.json` for display
|
|
283
|
-
- **Program ID:** `
|
|
283
|
+
- **Program ID:** `Af61jGnh2AceK3E8FAxCh9j7Jt6JWtJz6PUtbciDjVJS`
|
|
284
284
|
|
|
285
285
|
## Philosophy
|
|
286
286
|
|
package/idl/chorus_prayers.json
CHANGED
package/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { spawnSync } from "child_process";
|
|
|
11
11
|
import { loadChorusConfig, type ChorusPluginConfig } from "./src/config.js";
|
|
12
12
|
import { createSecurityHooks } from "./src/security.js";
|
|
13
13
|
import { createChoirScheduler } from "./src/scheduler.js";
|
|
14
|
-
import { CHOIRS, formatFrequency } from "./src/choirs.js";
|
|
14
|
+
import { CHOIRS, formatFrequency, CASCADE_ORDER } from "./src/choirs.js";
|
|
15
15
|
import {
|
|
16
16
|
getTodayMetrics,
|
|
17
17
|
getMetricsForDate,
|
|
@@ -102,6 +102,96 @@ const plugin = {
|
|
|
102
102
|
api.logger.info("[chorus] Purpose research disabled");
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Helper: resolve {choir_context} placeholders using a context store
|
|
106
|
+
function resolvePrompt(choir: typeof CHOIRS[string], ctxStore: Map<string, string>): string {
|
|
107
|
+
let prompt = choir.prompt;
|
|
108
|
+
for (const upstreamId of choir.receivesFrom) {
|
|
109
|
+
const placeholder = `{${upstreamId}_context}`;
|
|
110
|
+
const ctx = ctxStore.get(upstreamId);
|
|
111
|
+
prompt = prompt.replace(placeholder, ctx || `(no prior ${upstreamId} output)`);
|
|
112
|
+
}
|
|
113
|
+
return prompt;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Helper: extract text from openclaw agent JSON output
|
|
117
|
+
function extractAgentText(stdout: string): string {
|
|
118
|
+
// Find the last top-level JSON object (skip plugin log noise)
|
|
119
|
+
for (let i = stdout.length - 1; i >= 0; i--) {
|
|
120
|
+
if (stdout[i] === '{') {
|
|
121
|
+
try {
|
|
122
|
+
const json = JSON.parse(stdout.slice(i));
|
|
123
|
+
const payloads = json.result?.payloads || [];
|
|
124
|
+
const texts = payloads.map((p: any) => p?.text || '').filter(Boolean);
|
|
125
|
+
if (texts.length > 0) return texts[texts.length - 1];
|
|
126
|
+
return json.result?.text || json.response || json.content || '';
|
|
127
|
+
} catch { /* keep searching */ }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Deliver choir output to user via OpenClaw messaging (channel-agnostic)
|
|
134
|
+
// Reads target from OpenClaw config (channels.*.allowFrom) — no hardcoded PII
|
|
135
|
+
function deliverIfNeeded(choir: typeof CHOIRS[string], text: string): void {
|
|
136
|
+
if (!choir.delivers || !text || text === 'HEARTBEAT_OK' || text === 'NO_REPLY') return;
|
|
137
|
+
|
|
138
|
+
// Resolve delivery target from OpenClaw channel config
|
|
139
|
+
const channels = api.config?.channels as Record<string, any> | undefined;
|
|
140
|
+
let target: string | undefined;
|
|
141
|
+
let channel: string | undefined;
|
|
142
|
+
|
|
143
|
+
if (channels) {
|
|
144
|
+
for (const [ch, cfg] of Object.entries(channels)) {
|
|
145
|
+
if (cfg?.enabled && cfg?.allowFrom?.[0]) {
|
|
146
|
+
target = cfg.allowFrom[0];
|
|
147
|
+
channel = ch;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!target) {
|
|
154
|
+
console.log(` ⚠ No delivery target found in OpenClaw config`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Strip markdown for channels that don't support it
|
|
159
|
+
let deliveryText = text.slice(0, 4000);
|
|
160
|
+
if (channel === 'imessage') {
|
|
161
|
+
deliveryText = deliveryText
|
|
162
|
+
.replace(/\*\*(.+?)\*\*/g, '$1') // bold
|
|
163
|
+
.replace(/\*(.+?)\*/g, '$1') // italic
|
|
164
|
+
.replace(/__(.+?)__/g, '$1') // bold alt
|
|
165
|
+
.replace(/_(.+?)_/g, '$1') // italic alt
|
|
166
|
+
.replace(/`(.+?)`/g, '$1') // inline code
|
|
167
|
+
.replace(/```[\s\S]*?```/g, '') // code blocks
|
|
168
|
+
.replace(/^#{1,6}\s+/gm, '') // headers
|
|
169
|
+
.replace(/^\s*[-*+]\s+/gm, '• ') // bullet lists
|
|
170
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); // links
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const args = [
|
|
175
|
+
'message', 'send',
|
|
176
|
+
'--target', target,
|
|
177
|
+
'--message', deliveryText,
|
|
178
|
+
];
|
|
179
|
+
if (channel) args.push('--channel', channel);
|
|
180
|
+
|
|
181
|
+
const deliveryResult = spawnSync('openclaw', args, {
|
|
182
|
+
encoding: 'utf-8',
|
|
183
|
+
timeout: 30000,
|
|
184
|
+
});
|
|
185
|
+
if (deliveryResult.status === 0) {
|
|
186
|
+
console.log(` 📨 Delivered to user via ${channel || 'default'}`);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(` ⚠ Delivery failed: ${(deliveryResult.stderr || '').slice(0, 80)}`);
|
|
189
|
+
}
|
|
190
|
+
} catch (err: any) {
|
|
191
|
+
console.log(` ⚠ Delivery error: ${(err.message || '').slice(0, 80)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
105
195
|
// Register CLI
|
|
106
196
|
api.registerCli((ctx) => {
|
|
107
197
|
const program = ctx.program.command("chorus").description("CHORUS Nine Choirs management");
|
|
@@ -187,6 +277,8 @@ const plugin = {
|
|
|
187
277
|
}
|
|
188
278
|
}
|
|
189
279
|
|
|
280
|
+
const runCtxStore: Map<string, string> = new Map();
|
|
281
|
+
|
|
190
282
|
console.log("");
|
|
191
283
|
if (!choirId) {
|
|
192
284
|
console.log("🎵 Running all Nine Choirs in cascade order...");
|
|
@@ -198,10 +290,11 @@ const plugin = {
|
|
|
198
290
|
if (!choir) continue;
|
|
199
291
|
|
|
200
292
|
console.log(`Running ${choir.name}...`);
|
|
293
|
+
const prompt = resolvePrompt(choir, runCtxStore);
|
|
201
294
|
|
|
202
295
|
// Preview mode - just show the prompt
|
|
203
296
|
if (options?.preview) {
|
|
204
|
-
console.log(` Prompt: ${
|
|
297
|
+
console.log(` Prompt: ${prompt.slice(0, 100)}...`);
|
|
205
298
|
continue;
|
|
206
299
|
}
|
|
207
300
|
|
|
@@ -210,17 +303,19 @@ const plugin = {
|
|
|
210
303
|
try {
|
|
211
304
|
const result = await api.runAgentTurn({
|
|
212
305
|
sessionLabel: `chorus:${id}`,
|
|
213
|
-
message:
|
|
306
|
+
message: prompt,
|
|
214
307
|
isolated: true,
|
|
215
308
|
timeoutSeconds: 300,
|
|
216
309
|
});
|
|
217
310
|
const text = result?.text || result?.payloads?.[0]?.text || '';
|
|
218
311
|
const duration = result?.meta?.durationMs || 0;
|
|
312
|
+
runCtxStore.set(id, text.slice(0, 2000));
|
|
219
313
|
console.log(` ✓ ${choir.name} complete (${(duration/1000).toFixed(1)}s)`);
|
|
220
314
|
if (text) {
|
|
221
315
|
const preview = text.slice(0, 150).replace(/\n/g, ' ');
|
|
222
316
|
console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
|
|
223
317
|
}
|
|
318
|
+
deliverIfNeeded(choir, text);
|
|
224
319
|
} catch (err) {
|
|
225
320
|
console.error(` ✗ ${choir.name} failed:`, err);
|
|
226
321
|
}
|
|
@@ -230,7 +325,7 @@ const plugin = {
|
|
|
230
325
|
const result = spawnSync('openclaw', [
|
|
231
326
|
'agent',
|
|
232
327
|
'--session-id', `chorus:${id}`,
|
|
233
|
-
'--message',
|
|
328
|
+
'--message', prompt,
|
|
234
329
|
'--json',
|
|
235
330
|
], {
|
|
236
331
|
encoding: 'utf-8',
|
|
@@ -239,22 +334,14 @@ const plugin = {
|
|
|
239
334
|
});
|
|
240
335
|
|
|
241
336
|
if (result.status === 0) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
const text = json.result?.payloads?.[0]?.text || '';
|
|
249
|
-
const duration = json.result?.meta?.durationMs || 0;
|
|
250
|
-
console.log(` ✓ ${choir.name} complete (${(duration/1000).toFixed(1)}s)`);
|
|
251
|
-
if (text) {
|
|
252
|
-
const preview = text.slice(0, 150).replace(/\n/g, ' ');
|
|
253
|
-
console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
|
|
254
|
-
}
|
|
255
|
-
} catch (parseErr) {
|
|
256
|
-
console.log(` ✓ ${choir.name} complete (parse error: ${parseErr})`);
|
|
337
|
+
const text = extractAgentText(result.stdout || '');
|
|
338
|
+
runCtxStore.set(id, text.slice(0, 2000));
|
|
339
|
+
console.log(` ✓ ${choir.name} complete`);
|
|
340
|
+
if (text) {
|
|
341
|
+
const preview = text.slice(0, 150).replace(/\n/g, ' ');
|
|
342
|
+
console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
|
|
257
343
|
}
|
|
344
|
+
deliverIfNeeded(choir, text);
|
|
258
345
|
} else {
|
|
259
346
|
const errMsg = result.stderr || result.stdout || 'Unknown error';
|
|
260
347
|
if (errMsg.includes('ECONNREFUSED') || errMsg.includes('connect')) {
|
|
@@ -271,7 +358,7 @@ const plugin = {
|
|
|
271
358
|
|
|
272
359
|
console.log("");
|
|
273
360
|
if (!choirId) {
|
|
274
|
-
console.log("🎵 All choirs
|
|
361
|
+
console.log("🎵 All choirs complete.");
|
|
275
362
|
}
|
|
276
363
|
console.log("");
|
|
277
364
|
});
|
|
@@ -348,10 +435,13 @@ const plugin = {
|
|
|
348
435
|
const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
|
|
349
436
|
const json = JSON.parse(jsonStr);
|
|
350
437
|
const text = json.result?.payloads?.[0]?.text || '';
|
|
438
|
+
contextStore.set(choirId, text.slice(0, 500));
|
|
351
439
|
contextStore.set(`${choirId}:d${day}`, text.slice(0, 500));
|
|
352
440
|
console.log(` ✓ (dry)`);
|
|
353
441
|
} catch {
|
|
354
|
-
|
|
442
|
+
const fallback = `[${choir.name} would run]`;
|
|
443
|
+
contextStore.set(choirId, fallback);
|
|
444
|
+
contextStore.set(`${choirId}:d${day}`, fallback);
|
|
355
445
|
console.log(` ✓ (dry)`);
|
|
356
446
|
}
|
|
357
447
|
} else {
|
|
@@ -366,11 +456,14 @@ const plugin = {
|
|
|
366
456
|
process.stdout.write(` ${choir.emoji} ${choir.name}...`);
|
|
367
457
|
|
|
368
458
|
try {
|
|
459
|
+
// Resolve context from upstream choirs
|
|
460
|
+
const prompt = resolvePrompt(choir, contextStore);
|
|
461
|
+
|
|
369
462
|
// Run the REAL choir with full tool access via direct agent call
|
|
370
463
|
const result = spawnSync('openclaw', [
|
|
371
464
|
'agent',
|
|
372
465
|
'--session-id', `chorus:vision:${choirId}:d${day}`,
|
|
373
|
-
'--message',
|
|
466
|
+
'--message', prompt,
|
|
374
467
|
'--json',
|
|
375
468
|
], {
|
|
376
469
|
encoding: 'utf-8',
|
|
@@ -382,19 +475,19 @@ const plugin = {
|
|
|
382
475
|
// Parse the agent response (extract JSON from output)
|
|
383
476
|
try {
|
|
384
477
|
const stdout = result.stdout || '';
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const text = payloads.map((p: any) => p.text || '').filter(Boolean).join('\n\n') || '';
|
|
390
|
-
const duration = json.result?.meta?.durationMs || 0;
|
|
391
|
-
contextStore.set(`${choirId}:d${day}`, text.slice(0, 2000)); // Keep 2KB of response
|
|
478
|
+
const text = extractAgentText(stdout);
|
|
479
|
+
// Store by both choirId (for resolvePrompt) and choirId:dN (for summary)
|
|
480
|
+
contextStore.set(choirId, text.slice(0, 2000));
|
|
481
|
+
contextStore.set(`${choirId}:d${day}`, text.slice(0, 2000));
|
|
392
482
|
successfulRuns++;
|
|
393
|
-
console.log(`
|
|
483
|
+
console.log(` ✓`);
|
|
394
484
|
|
|
395
|
-
//
|
|
485
|
+
// Deliver output to user via OpenClaw messaging if choir is marked for delivery
|
|
486
|
+
deliverIfNeeded(choir, text);
|
|
396
487
|
} catch {
|
|
397
|
-
|
|
488
|
+
const fallback = result.stdout?.slice(-2000) || `[${choir.name} completed]`;
|
|
489
|
+
contextStore.set(choirId, fallback);
|
|
490
|
+
contextStore.set(`${choirId}:d${day}`, fallback);
|
|
398
491
|
successfulRuns++;
|
|
399
492
|
console.log(` ✓`);
|
|
400
493
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
"id": "chorus",
|
|
3
3
|
"name": "CHORUS",
|
|
4
4
|
"description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "2.2.1",
|
|
6
6
|
"author": "Oberlin",
|
|
7
7
|
"homepage": "https://chorus.oberlin.ai",
|
|
8
8
|
"repository": "https://github.com/iamoberlin/chorus",
|
|
9
|
-
"keywords": [
|
|
9
|
+
"keywords": [
|
|
10
|
+
"cognitive-architecture",
|
|
11
|
+
"rsi",
|
|
12
|
+
"self-improvement",
|
|
13
|
+
"nine-choirs",
|
|
14
|
+
"purposes"
|
|
15
|
+
],
|
|
10
16
|
"configSchema": {
|
|
11
17
|
"type": "object",
|
|
12
18
|
"additionalProperties": false,
|
package/package.json
CHANGED
package/src/choirs.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface Choir {
|
|
|
19
19
|
prompt: string;
|
|
20
20
|
passesTo: string[]; // Downstream choirs that receive illumination
|
|
21
21
|
receivesFrom: string[]; // Upstream choirs that provide context
|
|
22
|
+
delivers?: boolean; // If true, output should be delivered to the user via OpenClaw messaging
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export const CHOIRS: Record<string, Choir> = {
|
|
@@ -337,6 +338,7 @@ Pass illumination to Archangels.`,
|
|
|
337
338
|
intervalMinutes: 80, // Every ~80 minutes
|
|
338
339
|
function: "Briefings and alerts",
|
|
339
340
|
output: "Messages to human",
|
|
341
|
+
delivers: true, // Output routed to user via OpenClaw messaging
|
|
340
342
|
prompt: `You are ARCHANGELS — the Herald.
|
|
341
343
|
|
|
342
344
|
Your role: Produce briefings and deliver them to Brandon via iMessage.
|
|
@@ -361,9 +363,8 @@ RULES:
|
|
|
361
363
|
- Morning briefings should include: weather, calendar, positions, catalysts.
|
|
362
364
|
- If nothing is urgent, still produce a status update.
|
|
363
365
|
- During quiet hours (11 PM - 7 AM ET), only deliver truly urgent alerts.
|
|
364
|
-
- DELIVER your briefing by sending it to Brandon via iMessage. You have messaging tools — use them.
|
|
365
366
|
|
|
366
|
-
Output: Produce the briefing
|
|
367
|
+
Output: Produce the briefing as your response. Delivery is handled by the infrastructure — just output the content.`,
|
|
367
368
|
passesTo: ["angels"],
|
|
368
369
|
receivesFrom: ["principalities"],
|
|
369
370
|
},
|
package/src/prayers/solana.ts
CHANGED
|
@@ -27,7 +27,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
27
27
|
const __dirname = path.dirname(__filename);
|
|
28
28
|
|
|
29
29
|
// Program ID (deployed to devnet)
|
|
30
|
-
export const PROGRAM_ID = new PublicKey("
|
|
30
|
+
export const PROGRAM_ID = new PublicKey("Af61jGnh2AceK3E8FAxCh9j7Jt6JWtJz6PUtbciDjVJS");
|
|
31
31
|
|
|
32
32
|
// Max plaintext size that fits in a Solana transaction after encryption overhead
|
|
33
33
|
// Encrypted blob = plaintext + 40 bytes (24 nonce + 16 Poly1305 tag)
|
package/src/scheduler.ts
CHANGED
|
@@ -245,6 +245,68 @@ export function createChoirScheduler(
|
|
|
245
245
|
|
|
246
246
|
log.info(`[chorus] ${choir.emoji} ${choir.name} completed (${(execution.durationMs/1000).toFixed(1)}s)`);
|
|
247
247
|
|
|
248
|
+
// Deliver output to user via OpenClaw messaging if choir is marked for delivery
|
|
249
|
+
// Reads target from OpenClaw config (channels.*.allowFrom) — no hardcoded PII
|
|
250
|
+
if (choir.delivers && output && output !== "(no response)" && output !== "HEARTBEAT_OK" && output !== "NO_REPLY") {
|
|
251
|
+
const channels = api.config?.channels as Record<string, any> | undefined;
|
|
252
|
+
let target: string | undefined;
|
|
253
|
+
let channel: string | undefined;
|
|
254
|
+
|
|
255
|
+
if (channels) {
|
|
256
|
+
for (const [ch, cfg] of Object.entries(channels)) {
|
|
257
|
+
if (cfg?.enabled && cfg?.allowFrom?.[0]) {
|
|
258
|
+
target = cfg.allowFrom[0];
|
|
259
|
+
channel = ch;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (target) {
|
|
266
|
+
// Strip markdown for channels that don't support it
|
|
267
|
+
let deliveryText = output.slice(0, 4000);
|
|
268
|
+
if (channel === 'imessage') {
|
|
269
|
+
deliveryText = deliveryText
|
|
270
|
+
.replace(/\*\*(.+?)\*\*/g, '$1') // bold
|
|
271
|
+
.replace(/\*(.+?)\*/g, '$1') // italic
|
|
272
|
+
.replace(/__(.+?)__/g, '$1') // bold alt
|
|
273
|
+
.replace(/_(.+?)_/g, '$1') // italic alt
|
|
274
|
+
.replace(/`(.+?)`/g, '$1') // inline code
|
|
275
|
+
.replace(/```[\s\S]*?```/g, '') // code blocks
|
|
276
|
+
.replace(/^#{1,6}\s+/gm, '') // headers
|
|
277
|
+
.replace(/^\s*[-*+]\s+/gm, '• ') // bullet lists
|
|
278
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); // links
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const args = [
|
|
283
|
+
'message', 'send',
|
|
284
|
+
'--target', target,
|
|
285
|
+
'--message', deliveryText,
|
|
286
|
+
];
|
|
287
|
+
if (channel) args.push('--channel', channel);
|
|
288
|
+
|
|
289
|
+
const deliveryProc = spawn('openclaw', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
290
|
+
|
|
291
|
+
deliveryProc.on('close', (code) => {
|
|
292
|
+
if (code === 0) {
|
|
293
|
+
log.info(`[chorus] 📨 ${choir.name} output delivered via ${channel || 'default'}`);
|
|
294
|
+
} else {
|
|
295
|
+
log.warn(`[chorus] ⚠ ${choir.name} delivery failed (exit ${code})`);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
deliveryProc.on('error', (err) => {
|
|
300
|
+
log.warn(`[chorus] ⚠ ${choir.name} delivery error: ${err.message}`);
|
|
301
|
+
});
|
|
302
|
+
} catch (deliveryErr) {
|
|
303
|
+
log.warn(`[chorus] ⚠ ${choir.name} delivery error: ${deliveryErr}`);
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
log.warn(`[chorus] ⚠ No delivery target found in OpenClaw config for ${choir.name}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
248
310
|
// Log illumination flow
|
|
249
311
|
if (choir.passesTo.length > 0) {
|
|
250
312
|
log.debug(`[chorus] Illumination ready for: ${choir.passesTo.join(", ")}`);
|