@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 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:** `DZuj1ZcX4H6THBSgW4GhKA7SbZNXtPDE5xPkW2jN53PQ`
283
+ - **Program ID:** `Af61jGnh2AceK3E8FAxCh9j7Jt6JWtJz6PUtbciDjVJS`
284
284
 
285
285
  ## Philosophy
286
286
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "address": "DZuj1ZcX4H6THBSgW4GhKA7SbZNXtPDE5xPkW2jN53PQ",
2
+ "address": "Af61jGnh2AceK3E8FAxCh9j7Jt6JWtJz6PUtbciDjVJS",
3
3
  "metadata": {
4
4
  "name": "chorus_prayers",
5
5
  "version": "0.1.0",
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: ${choir.prompt.slice(0, 100)}...`);
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: choir.prompt,
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', choir.prompt,
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
- try {
243
- // Extract JSON from output (may have plugin logs before it)
244
- const stdout = result.stdout || '';
245
- const jsonStart = stdout.indexOf('{');
246
- const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
247
- const json = JSON.parse(jsonStr);
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 scheduled.");
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
- contextStore.set(`${choirId}:d${day}`, `[${choir.name} would run]`);
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', choir.prompt,
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 jsonStart = stdout.indexOf('{');
386
- const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
387
- const json = JSON.parse(jsonStr);
388
- const payloads = json.result?.payloads || [];
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(` ✓ (${(duration/1000).toFixed(1)}s)`);
483
+ console.log(` ✓`);
394
484
 
395
- // Note: Archangels handles its own delivery via OpenClaw messaging tools
485
+ // Deliver output to user via OpenClaw messaging if choir is marked for delivery
486
+ deliverIfNeeded(choir, text);
396
487
  } catch {
397
- contextStore.set(`${choirId}:d${day}`, result.stdout?.slice(-2000) || `[${choir.name} completed]`);
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
  }
@@ -2,11 +2,17 @@
2
2
  "id": "chorus",
3
3
  "name": "CHORUS",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement",
5
- "version": "1.3.4",
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": ["cognitive-architecture", "rsi", "self-improvement", "nine-choirs", "purposes"],
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement — with on-chain Prayer Chain (Solana)",
5
5
  "author": "Oberlin <iam@oberlin.ai>",
6
6
  "license": "MIT",
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, then send it to Brandon via iMessage.`,
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
  },
@@ -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("DZuj1ZcX4H6THBSgW4GhKA7SbZNXtPDE5xPkW2jN53PQ");
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(", ")}`);