@iamoberlin/chorus 1.1.4

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/index.ts ADDED
@@ -0,0 +1,724 @@
1
+ /**
2
+ * CHORUS Extension
3
+ *
4
+ * CHORUS: Hierarchy Of Recursive Unified Self-improvement
5
+ * Recursive illumination through the Nine Choirs.
6
+ * Config via openclaw.yaml: plugins.entries.chorus.config
7
+ */
8
+
9
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
10
+ import { spawnSync } from "child_process";
11
+ import { loadChorusConfig, type ChorusPluginConfig } from "./src/config.js";
12
+ import { createSecurityHooks } from "./src/security.js";
13
+ import { createChoirScheduler } from "./src/scheduler.js";
14
+ import { CHOIRS, formatFrequency } from "./src/choirs.js";
15
+ import {
16
+ getTodayMetrics,
17
+ getMetricsForDate,
18
+ getRecentMetrics,
19
+ getTotals,
20
+ setQualityScore,
21
+ formatMetricsSummary,
22
+ formatWeeklySummary,
23
+ } from "./src/metrics.js";
24
+ import { createDaemon, DEFAULT_DAEMON_CONFIG, type DaemonConfig } from "./src/daemon.js";
25
+ import { getInboxPath } from "./src/senses.js";
26
+ import {
27
+ loadPurposes,
28
+ addPurpose,
29
+ updatePurpose,
30
+ removePurpose,
31
+ formatPurposesList,
32
+ } from "./src/purposes.js";
33
+ import {
34
+ createPurposeResearchScheduler,
35
+ DEFAULT_PURPOSE_RESEARCH_CONFIG,
36
+ type PurposeResearchConfig,
37
+ } from "./src/purpose-research.js";
38
+
39
+ const VERSION = "1.1.3";
40
+
41
+ const plugin = {
42
+ id: "chorus",
43
+ name: "CHORUS",
44
+ description: "CHORUS: Hierarchy Of Recursive Unified Self-improvement",
45
+
46
+ register(api: OpenClawPluginApi) {
47
+ // Standard OpenClaw config: plugins.entries.chorus.config
48
+ const pluginConfig = api.config.plugins?.entries?.chorus?.config as ChorusPluginConfig | undefined;
49
+ const config = loadChorusConfig(pluginConfig);
50
+
51
+ api.logger.info(`[chorus] šŸŽµ CHORUS v${VERSION}`);
52
+
53
+ // Register security hooks (Powers choir handles security)
54
+ createSecurityHooks(api, config);
55
+
56
+ // Register choir scheduler service
57
+ if (config.choirs.enabled) {
58
+ api.registerService(createChoirScheduler(config, api.logger, api));
59
+ api.logger.info("[chorus] Choirs enabled — scheduler registered");
60
+ } else {
61
+ api.logger.info("[chorus] Choirs disabled — set enabled: true in openclaw.yaml");
62
+ }
63
+
64
+ // Register daemon service
65
+ const daemonConfig: DaemonConfig = {
66
+ ...DEFAULT_DAEMON_CONFIG,
67
+ enabled: (pluginConfig as any)?.daemon?.enabled ?? true,
68
+ ...(pluginConfig as any)?.daemon,
69
+ };
70
+
71
+ let daemon: ReturnType<typeof createDaemon> | null = null;
72
+ if (daemonConfig.enabled) {
73
+ daemon = createDaemon(daemonConfig, api.logger, api);
74
+ api.registerService(daemon);
75
+ api.logger.info("[chorus] Daemon enabled — autonomous attention active");
76
+ } else {
77
+ api.logger.info("[chorus] Daemon disabled");
78
+ }
79
+
80
+ // Register purpose research service
81
+ const purposeResearchConfig: PurposeResearchConfig = {
82
+ ...DEFAULT_PURPOSE_RESEARCH_CONFIG,
83
+ enabled: config.purposeResearch.enabled,
84
+ dailyRunCap: config.purposeResearch.dailyRunCap,
85
+ defaultFrequency: config.purposeResearch.defaultFrequency,
86
+ defaultMaxFrequency: config.purposeResearch.defaultMaxFrequency,
87
+ };
88
+
89
+ let purposeResearch: ReturnType<typeof createPurposeResearchScheduler> | null = null;
90
+ if (purposeResearchConfig.enabled) {
91
+ purposeResearch = createPurposeResearchScheduler(purposeResearchConfig, api.logger, api);
92
+ api.registerService(purposeResearch);
93
+ api.logger.info("[chorus] Purpose research enabled — adaptive frequency active");
94
+ } else {
95
+ api.logger.info("[chorus] Purpose research disabled");
96
+ }
97
+
98
+ // Register CLI
99
+ api.registerCli((ctx) => {
100
+ const program = ctx.program.command("chorus").description("CHORUS Nine Choirs management");
101
+
102
+ // Status command
103
+ program.command("status").description("Show CHORUS status").action(async () => {
104
+ const purposes = await loadPurposes();
105
+ const activePurposes = purposes.filter(p => p.progress < 100);
106
+ const researchPurposes = purposes.filter(p =>
107
+ p.progress < 100 &&
108
+ p.research?.enabled !== false &&
109
+ (p.criteria?.length || p.research?.domains?.length)
110
+ );
111
+
112
+ console.log("");
113
+ console.log("šŸŽµ CHORUS — Hierarchy Of Recursive Unified Self-improvement");
114
+ console.log("═".repeat(55));
115
+ console.log("");
116
+ console.log(` Version: ${VERSION}`);
117
+ console.log(` Choirs: ${config.choirs.enabled ? "āœ… enabled" : "āŒ disabled"}`);
118
+ console.log(` Daemon: ${daemonConfig.enabled ? "āœ… enabled" : "āŒ disabled"}`);
119
+ console.log(` Purpose Research: ${purposeResearchConfig.enabled ? "āœ… enabled" : "āŒ disabled"}`);
120
+ console.log(` Active Purposes: ${activePurposes.length}`);
121
+ console.log(` Research Purposes: ${researchPurposes.length}`);
122
+ if (daemon) {
123
+ console.log(` Attention Queue: ${daemon.getQueueSize()} items`);
124
+ }
125
+ if (purposeResearch) {
126
+ console.log(` Research Runs: ${purposeResearch.getDailyRunCount()}/${purposeResearch.getDailyCap()} today`);
127
+ }
128
+ console.log(` Timezone: ${config.choirs.timezone}`);
129
+ console.log("");
130
+
131
+ if (!config.choirs.enabled && !daemonConfig.enabled && !purposeResearchConfig.enabled) {
132
+ console.log(" šŸ’” Enable choirs, daemon, or purposeResearch in openclaw.yaml");
133
+ console.log("");
134
+ }
135
+ });
136
+
137
+ // List choirs command
138
+ program.command("list").description("List all choirs and their schedules").action(() => {
139
+ console.log("");
140
+ console.log("šŸŽµ Nine Choirs");
141
+ console.log("═".repeat(50));
142
+ console.log("");
143
+ console.log("FIRST TRIAD — Contemplation");
144
+ console.log("─".repeat(50));
145
+ printChoir("seraphim", config);
146
+ printChoir("cherubim", config);
147
+ printChoir("thrones", config);
148
+ console.log("");
149
+ console.log("SECOND TRIAD — Governance");
150
+ console.log("─".repeat(50));
151
+ printChoir("dominions", config);
152
+ printChoir("virtues", config);
153
+ printChoir("powers", config);
154
+ console.log("");
155
+ console.log("THIRD TRIAD — Action");
156
+ console.log("─".repeat(50));
157
+ printChoir("principalities", config);
158
+ printChoir("archangels", config);
159
+ printChoir("angels", config);
160
+ console.log("");
161
+ });
162
+
163
+ // Run a specific choir manually (or all if none specified)
164
+ program
165
+ .command("run [choir]")
166
+ .description("Manually trigger a choir (or all choirs if none specified)")
167
+ .option("--preview", "Preview prompt without running")
168
+ .action(async (choirId?: string, options?: { preview?: boolean }) => {
169
+ const choirsToRun = choirId
170
+ ? [choirId]
171
+ : ["seraphim", "cherubim", "thrones", "dominions", "virtues", "powers", "principalities", "archangels", "angels"];
172
+
173
+ if (choirId) {
174
+ const choir = CHOIRS[choirId];
175
+ if (!choir) {
176
+ console.error(`Unknown choir: ${choirId}`);
177
+ console.log("Available: seraphim, cherubim, thrones, dominions, virtues, powers, principalities, archangels, angels");
178
+ return;
179
+ }
180
+ }
181
+
182
+ console.log("");
183
+ if (!choirId) {
184
+ console.log("šŸŽµ Running all Nine Choirs in cascade order...");
185
+ console.log("");
186
+ }
187
+
188
+ for (const id of choirsToRun) {
189
+ const choir = CHOIRS[id];
190
+ if (!choir) continue;
191
+
192
+ console.log(`Running ${choir.name}...`);
193
+
194
+ // Preview mode - just show the prompt
195
+ if (options?.preview) {
196
+ console.log(` Prompt: ${choir.prompt.slice(0, 100)}...`);
197
+ continue;
198
+ }
199
+
200
+ // Try gateway-connected runAgentTurn first (available when loaded as plugin)
201
+ if (typeof api.runAgentTurn === 'function') {
202
+ try {
203
+ const result = await api.runAgentTurn({
204
+ sessionLabel: `chorus:${id}`,
205
+ message: choir.prompt,
206
+ isolated: true,
207
+ timeoutSeconds: 300,
208
+ });
209
+ console.log(` āœ“ ${choir.name} complete`);
210
+ } catch (err) {
211
+ console.error(` āœ— ${choir.name} failed:`, err);
212
+ }
213
+ } else {
214
+ // CLI context: use openclaw agent for direct execution via gateway
215
+ try {
216
+ const result = spawnSync('openclaw', [
217
+ 'agent',
218
+ '--session-id', `chorus:${id}`,
219
+ '--message', choir.prompt,
220
+ '--json',
221
+ ], {
222
+ encoding: 'utf-8',
223
+ timeout: 300000, // 5 min
224
+ });
225
+
226
+ if (result.status === 0) {
227
+ try {
228
+ const json = JSON.parse(result.stdout || '{}');
229
+ const text = json.result?.payloads?.[0]?.text || '';
230
+ const duration = json.result?.meta?.durationMs || 0;
231
+ console.log(` āœ“ ${choir.name} complete (${(duration/1000).toFixed(1)}s)`);
232
+ if (text) {
233
+ const preview = text.slice(0, 150).replace(/\n/g, ' ');
234
+ console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
235
+ }
236
+ } catch {
237
+ console.log(` āœ“ ${choir.name} complete`);
238
+ }
239
+ } else {
240
+ const errMsg = result.stderr || result.stdout || 'Unknown error';
241
+ if (errMsg.includes('ECONNREFUSED') || errMsg.includes('connect')) {
242
+ console.log(` ⚠ Gateway not running. Start with: openclaw gateway start`);
243
+ } else {
244
+ console.error(` āœ— ${choir.name} failed:`, errMsg.trim().slice(0, 200));
245
+ }
246
+ }
247
+ } catch (err: any) {
248
+ console.error(` āœ— ${choir.name} failed:`, err.message || err);
249
+ }
250
+ }
251
+ }
252
+
253
+ console.log("");
254
+ if (!choirId) {
255
+ console.log("šŸŽµ All choirs scheduled.");
256
+ }
257
+ console.log("");
258
+ });
259
+
260
+ // Metrics command
261
+ const metricsCmd = program.command("metrics").description("View CHORUS execution metrics");
262
+
263
+ metricsCmd
264
+ .command("today")
265
+ .description("Show today's metrics")
266
+ .action(() => {
267
+ const metrics = getTodayMetrics();
268
+ if (!metrics) {
269
+ console.log("\nNo metrics recorded for today yet.\n");
270
+ return;
271
+ }
272
+ console.log("");
273
+ console.log(formatMetricsSummary(metrics));
274
+ console.log("");
275
+ });
276
+
277
+ metricsCmd
278
+ .command("week")
279
+ .description("Show weekly summary")
280
+ .action(() => {
281
+ console.log("");
282
+ console.log(formatWeeklySummary());
283
+ console.log("");
284
+ });
285
+
286
+ metricsCmd
287
+ .command("date <date>")
288
+ .description("Show metrics for a specific date (YYYY-MM-DD)")
289
+ .action((date: string) => {
290
+ const metrics = getMetricsForDate(date);
291
+ if (!metrics) {
292
+ console.log(`\nNo metrics recorded for ${date}.\n`);
293
+ return;
294
+ }
295
+ console.log("");
296
+ console.log(formatMetricsSummary(metrics));
297
+ console.log("");
298
+ });
299
+
300
+ metricsCmd
301
+ .command("rate <date> <score>")
302
+ .description("Set quality score (1-5) for a date")
303
+ .option("-n, --notes <notes>", "Add notes")
304
+ .action((date: string, score: string, options: { notes?: string }) => {
305
+ const scoreNum = parseInt(score, 10);
306
+ if (isNaN(scoreNum) || scoreNum < 1 || scoreNum > 5) {
307
+ console.error("Score must be 1-5");
308
+ return;
309
+ }
310
+ setQualityScore(date, scoreNum, options.notes);
311
+ console.log(`\nāœ“ Quality score for ${date} set to ${scoreNum}/5\n`);
312
+ });
313
+
314
+ metricsCmd
315
+ .command("totals")
316
+ .description("Show all-time totals")
317
+ .action(() => {
318
+ const totals = getTotals();
319
+ console.log("");
320
+ console.log("šŸ“Š CHORUS All-Time Totals");
321
+ console.log("═".repeat(40));
322
+ console.log(` Total Runs: ${totals.allTimeRuns.toLocaleString()}`);
323
+ console.log(` Successes: ${totals.allTimeSuccesses.toLocaleString()} (${totals.allTimeRuns > 0 ? ((totals.allTimeSuccesses / totals.allTimeRuns) * 100).toFixed(1) : 0}%)`);
324
+ console.log(` Findings: ${totals.allTimeFindings}`);
325
+ console.log(` Alerts: ${totals.allTimeAlerts}`);
326
+ console.log(` Improvements: ${totals.allTimeImprovements}`);
327
+ console.log("");
328
+ });
329
+
330
+ metricsCmd
331
+ .command("purposes")
332
+ .description("Show metrics for purpose-derived research")
333
+ .action(() => {
334
+ const todayMetrics = getTodayMetrics();
335
+ if (!todayMetrics) {
336
+ console.log("\nNo metrics recorded for today yet.\n");
337
+ return;
338
+ }
339
+
340
+ // Filter executions for purpose-derived research
341
+ const purposeExecs = todayMetrics.executions.filter(e => e.choirId.startsWith("purpose:"));
342
+
343
+ console.log("");
344
+ console.log("šŸ“Š Purpose Research Metrics — Today");
345
+ console.log("═".repeat(40));
346
+ console.log(` Total runs: ${purposeExecs.length}`);
347
+ console.log(` Successful: ${purposeExecs.filter(e => e.success).length}`);
348
+ console.log(` Findings: ${purposeExecs.reduce((sum, e) => sum + (e.findings || 0), 0)}`);
349
+ console.log(` Alerts: ${purposeExecs.reduce((sum, e) => sum + (e.alerts || 0), 0)}`);
350
+ console.log("");
351
+
352
+ if (purposeExecs.length > 0) {
353
+ console.log("By purpose:");
354
+ console.log("─".repeat(40));
355
+ const byPurpose = new Map<string, typeof purposeExecs>();
356
+ for (const exec of purposeExecs) {
357
+ const purposeId = exec.choirId.replace("purpose:", "");
358
+ if (!byPurpose.has(purposeId)) byPurpose.set(purposeId, []);
359
+ byPurpose.get(purposeId)!.push(exec);
360
+ }
361
+ for (const [purposeId, execs] of byPurpose) {
362
+ const findings = execs.reduce((sum, e) => sum + (e.findings || 0), 0);
363
+ const avgDuration = execs.reduce((sum, e) => sum + e.durationMs, 0) / execs.length;
364
+ console.log(` ${purposeId}: ${execs.length} runs, ${findings} findings, ${(avgDuration/1000).toFixed(1)}s avg`);
365
+ }
366
+ console.log("");
367
+ }
368
+ });
369
+
370
+ // Daemon commands
371
+ const daemonCmd = program.command("daemon").description("Autonomous attention daemon");
372
+
373
+ daemonCmd
374
+ .command("status")
375
+ .description("Show daemon status")
376
+ .action(() => {
377
+ console.log("");
378
+ console.log("šŸ‘ļø CHORUS Daemon");
379
+ console.log("═".repeat(40));
380
+ console.log(` Enabled: ${daemonConfig.enabled ? "āœ… yes" : "āŒ no"}`);
381
+ console.log(` Threshold: ${daemonConfig.thinkThreshold}`);
382
+ console.log(` Poll interval: ${daemonConfig.pollIntervalMs / 1000}s`);
383
+ console.log(` Quiet hours: ${daemonConfig.quietHoursStart}:00 - ${daemonConfig.quietHoursEnd}:00`);
384
+ console.log(` Inbox: ${getInboxPath()}`);
385
+ if (daemon) {
386
+ console.log(` Queue size: ${daemon.getQueueSize()}`);
387
+ }
388
+ console.log("");
389
+ });
390
+
391
+ daemonCmd
392
+ .command("queue")
393
+ .description("Show current attention queue")
394
+ .action(() => {
395
+ if (!daemon) {
396
+ console.log("\nDaemon not running.\n");
397
+ return;
398
+ }
399
+ const queue = daemon.getQueue();
400
+ console.log("");
401
+ console.log("šŸ‘ļø Attention Queue");
402
+ console.log("═".repeat(50));
403
+ if (queue.length === 0) {
404
+ console.log(" (empty)");
405
+ } else {
406
+ for (const item of queue) {
407
+ console.log(` [${item.salienceScore}] ${item.source}: ${item.content.slice(0, 60)}...`);
408
+ }
409
+ }
410
+ console.log("");
411
+ });
412
+
413
+ daemonCmd
414
+ .command("poll")
415
+ .description("Force poll all senses now")
416
+ .action(async () => {
417
+ if (!daemon) {
418
+ console.log("\nDaemon not running.\n");
419
+ return;
420
+ }
421
+ console.log("\nPolling senses...");
422
+ await daemon.forcePoll();
423
+ console.log(`Queue size: ${daemon.getQueueSize()}\n`);
424
+ });
425
+
426
+ daemonCmd
427
+ .command("process")
428
+ .description("Process highest priority item now")
429
+ .action(async () => {
430
+ if (!daemon) {
431
+ console.log("\nDaemon not running.\n");
432
+ return;
433
+ }
434
+ const size = daemon.getQueueSize();
435
+ if (size === 0) {
436
+ console.log("\nQueue empty.\n");
437
+ return;
438
+ }
439
+ console.log("\nProcessing top item...");
440
+ await daemon.forceProcess();
441
+ console.log("Done.\n");
442
+ });
443
+
444
+ // Research commands
445
+ const researchCmd = program.command("research").description("Purpose-derived research");
446
+
447
+ researchCmd
448
+ .command("status")
449
+ .description("Show research scheduler status")
450
+ .action(async () => {
451
+ const purposes = await loadPurposes();
452
+ const researchPurposes = purposes.filter(p =>
453
+ p.progress < 100 &&
454
+ p.research?.enabled !== false &&
455
+ (p.criteria?.length || p.research?.domains?.length)
456
+ );
457
+
458
+ console.log("");
459
+ console.log("šŸ”¬ Purpose Research Status");
460
+ console.log("═".repeat(50));
461
+ console.log(` Enabled: ${purposeResearchConfig.enabled ? "āœ… yes" : "āŒ no"}`);
462
+ console.log(` Daily cap: ${purposeResearchConfig.dailyRunCap}`);
463
+ console.log(` Default freq: ${purposeResearchConfig.defaultFrequency}/day`);
464
+ if (purposeResearch) {
465
+ console.log(` Today's runs: ${purposeResearch.getDailyRunCount()}/${purposeResearch.getDailyCap()}`);
466
+ }
467
+ console.log(` Active purposes: ${researchPurposes.length}`);
468
+ console.log("");
469
+
470
+ if (researchPurposes.length > 0) {
471
+ console.log("Research-enabled purposes:");
472
+ console.log("─".repeat(50));
473
+ for (const purpose of researchPurposes) {
474
+ const freq = purpose.research?.frequency ?? purposeResearchConfig.defaultFrequency;
475
+ const lastRun = purpose.research?.lastRun
476
+ ? new Date(purpose.research.lastRun).toLocaleString()
477
+ : "never";
478
+ const runCount = purpose.research?.runCount ?? 0;
479
+ console.log(` ${purpose.name}`);
480
+ console.log(` Frequency: ${freq}/day | Last: ${lastRun} | Runs: ${runCount}`);
481
+ }
482
+ console.log("");
483
+ }
484
+ });
485
+
486
+ researchCmd
487
+ .command("run <purposeId>")
488
+ .description("Manually trigger research for a purpose")
489
+ .action(async (purposeId: string) => {
490
+ if (!purposeResearch) {
491
+ console.log("\nPurpose research not enabled.\n");
492
+ return;
493
+ }
494
+ console.log(`\nRunning research for "${purposeId}"...`);
495
+ try {
496
+ await purposeResearch.forceRun(purposeId);
497
+ console.log("Done.\n");
498
+ } catch (err: any) {
499
+ console.error(`\nāœ— ${err.message}\n`);
500
+ }
501
+ });
502
+
503
+ researchCmd
504
+ .command("list")
505
+ .description("List purposes with research enabled")
506
+ .action(async () => {
507
+ const purposes = await loadPurposes();
508
+ const researchPurposes = purposes.filter(p =>
509
+ p.research?.enabled !== false &&
510
+ (p.criteria?.length || p.research?.domains?.length)
511
+ );
512
+
513
+ console.log("");
514
+ console.log("šŸ”¬ Research-Enabled Purposes");
515
+ console.log("═".repeat(50));
516
+
517
+ if (researchPurposes.length === 0) {
518
+ console.log(" No purposes with research enabled.");
519
+ console.log(" Add criteria to a purpose to enable research.");
520
+ } else {
521
+ for (const purpose of researchPurposes) {
522
+ const status = purpose.progress >= 100 ? "āœ“" : "ā—‹";
523
+ const freq = purpose.research?.frequency ?? purposeResearchConfig.defaultFrequency;
524
+ console.log(` ${status} ${purpose.name} (${freq}/day)`);
525
+ if (purpose.criteria?.length) {
526
+ for (const c of purpose.criteria.slice(0, 3)) {
527
+ console.log(` • ${c}`);
528
+ }
529
+ if (purpose.criteria.length > 3) {
530
+ console.log(` ... +${purpose.criteria.length - 3} more`);
531
+ }
532
+ }
533
+ }
534
+ }
535
+ console.log("");
536
+ });
537
+
538
+ // Purpose commands
539
+ const purposeCmd = program.command("purpose").description("Manage autonomous purposes");
540
+
541
+ purposeCmd
542
+ .command("list")
543
+ .description("List all purposes")
544
+ .action(async () => {
545
+ const purposes = await loadPurposes();
546
+ console.log("");
547
+ console.log(formatPurposesList(purposes));
548
+ console.log("");
549
+ });
550
+
551
+ purposeCmd
552
+ .command("add <id> <name>")
553
+ .description("Add a new purpose")
554
+ .option("-d, --deadline <date>", "Deadline (YYYY-MM-DD or ISO)")
555
+ .option("-c, --criteria <items>", "Success criteria (comma-separated)")
556
+ .option("--domains <items>", "Research domains (comma-separated)")
557
+ .option("--frequency <n>", "Research runs per day")
558
+ .option("--no-research", "Disable auto-research for this purpose")
559
+ .option("--curiosity <n>", "Curiosity score 0-100 (for exploration purposes)")
560
+ .action(async (id: string, name: string, options: any) => {
561
+ try {
562
+ const criteria = options.criteria
563
+ ? options.criteria.split(",").map((s: string) => s.trim())
564
+ : undefined;
565
+ const domains = options.domains
566
+ ? options.domains.split(",").map((s: string) => s.trim())
567
+ : undefined;
568
+
569
+ // Build research config if criteria or domains provided
570
+ let research = undefined;
571
+ if (options.research === false) {
572
+ research = { enabled: false };
573
+ } else if (criteria?.length || domains?.length) {
574
+ research = {
575
+ enabled: true,
576
+ domains,
577
+ frequency: options.frequency ? parseInt(options.frequency) : undefined,
578
+ };
579
+ }
580
+
581
+ const purpose = await addPurpose({
582
+ id,
583
+ name,
584
+ deadline: options.deadline ? Date.parse(options.deadline) : undefined,
585
+ criteria,
586
+ curiosity: options.curiosity ? parseInt(options.curiosity) : undefined,
587
+ research,
588
+ });
589
+
590
+ console.log(`\nāœ“ Purpose added: ${purpose.name}`);
591
+ if (purpose.research?.enabled) {
592
+ const freq = purpose.research.frequency ?? purposeResearchConfig.defaultFrequency;
593
+ console.log(` Research: ${freq}/day`);
594
+ if (purpose.research.domains?.length) {
595
+ console.log(` Domains: ${purpose.research.domains.join(", ")}`);
596
+ }
597
+ }
598
+ console.log("");
599
+ } catch (err: any) {
600
+ console.error(`\nāœ— ${err.message}\n`);
601
+ }
602
+ });
603
+
604
+ purposeCmd
605
+ .command("progress <id> <percent>")
606
+ .description("Update purpose progress (0-100)")
607
+ .action(async (id: string, percent: string) => {
608
+ const progress = parseInt(percent);
609
+ if (isNaN(progress) || progress < 0 || progress > 100) {
610
+ console.error("\nProgress must be 0-100\n");
611
+ return;
612
+ }
613
+ const purpose = await updatePurpose(id, { progress });
614
+ if (purpose) {
615
+ console.log(`\nāœ“ ${purpose.name}: ${progress}%\n`);
616
+ } else {
617
+ console.error(`\nāœ— Purpose "${id}" not found\n`);
618
+ }
619
+ });
620
+
621
+ purposeCmd
622
+ .command("done <id>")
623
+ .description("Mark purpose as complete (100%)")
624
+ .action(async (id: string) => {
625
+ const purpose = await updatePurpose(id, { progress: 100 });
626
+ if (purpose) {
627
+ console.log(`\nāœ“ ${purpose.name}: Complete!\n`);
628
+ } else {
629
+ console.error(`\nāœ— Purpose "${id}" not found\n`);
630
+ }
631
+ });
632
+
633
+ purposeCmd
634
+ .command("remove <id>")
635
+ .description("Remove a purpose")
636
+ .action(async (id: string) => {
637
+ const removed = await removePurpose(id);
638
+ if (removed) {
639
+ console.log(`\nāœ“ Purpose "${id}" removed\n`);
640
+ } else {
641
+ console.error(`\nāœ— Purpose "${id}" not found\n`);
642
+ }
643
+ });
644
+
645
+ purposeCmd
646
+ .command("research <id>")
647
+ .description("Configure research for a purpose")
648
+ .option("--enable", "Enable research")
649
+ .option("--disable", "Disable research")
650
+ .option("--domains <items>", "Set research domains (comma-separated)")
651
+ .option("--frequency <n>", "Set research frequency (runs/day)")
652
+ .option("--criteria <items>", "Set success criteria (comma-separated)")
653
+ .action(async (id: string, options: any) => {
654
+ const purposes = await loadPurposes();
655
+ const purpose = purposes.find(p => p.id === id);
656
+ if (!purpose) {
657
+ console.error(`\nāœ— Purpose "${id}" not found\n`);
658
+ return;
659
+ }
660
+
661
+ const updates: any = {};
662
+
663
+ if (options.criteria) {
664
+ updates.criteria = options.criteria.split(",").map((s: string) => s.trim());
665
+ }
666
+
667
+ const researchUpdates: any = { ...purpose.research };
668
+
669
+ if (options.enable) {
670
+ researchUpdates.enabled = true;
671
+ } else if (options.disable) {
672
+ researchUpdates.enabled = false;
673
+ }
674
+
675
+ if (options.domains) {
676
+ researchUpdates.domains = options.domains.split(",").map((s: string) => s.trim());
677
+ }
678
+
679
+ if (options.frequency) {
680
+ researchUpdates.frequency = parseInt(options.frequency);
681
+ }
682
+
683
+ updates.research = researchUpdates;
684
+
685
+ const updated = await updatePurpose(id, updates);
686
+ if (updated) {
687
+ console.log(`\nāœ“ ${updated.name} research config updated`);
688
+ if (updated.research?.enabled === false) {
689
+ console.log(" Research: disabled");
690
+ } else {
691
+ const freq = updated.research?.frequency ?? purposeResearchConfig.defaultFrequency;
692
+ console.log(` Research: ${freq}/day`);
693
+ if (updated.research?.domains?.length) {
694
+ console.log(` Domains: ${updated.research.domains.join(", ")}`);
695
+ }
696
+ }
697
+ console.log("");
698
+ }
699
+ });
700
+
701
+ // Inbox command (shortcut)
702
+ program
703
+ .command("inbox")
704
+ .description("Show inbox path for daemon signals")
705
+ .action(() => {
706
+ console.log(`\nDrop files here to trigger daemon attention:\n ${getInboxPath()}\n`);
707
+ });
708
+
709
+ }, { commands: ["chorus"] });
710
+
711
+ api.logger.info("[chorus] šŸŽµ Registered");
712
+ },
713
+ };
714
+
715
+ function printChoir(id: string, config: any) {
716
+ const choir = CHOIRS[id];
717
+ if (!choir) return;
718
+ const enabled = config.choirs.overrides[id] !== false;
719
+ const status = enabled ? "āœ…" : "āŒ";
720
+ const freq = formatFrequency(choir).padEnd(8);
721
+ console.log(` ${status} ${choir.name.padEnd(16)} ${freq} ${choir.function}`);
722
+ }
723
+
724
+ export default plugin;