@iamoberlin/chorus 1.2.0 → 1.2.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.
package/README.md CHANGED
@@ -83,6 +83,23 @@ The **Virtues** choir is the RSI engine. Six times per day:
83
83
 
84
84
  Day 1, baseline. Day 30, unrecognizable.
85
85
 
86
+ ## Calibration
87
+
88
+ Intelligence requires feedback. CHORUS builds calibration into the choir flow using natural language:
89
+
90
+ **Principalities** states beliefs when researching:
91
+ > "I believe X will happen by [timeframe] because..."
92
+
93
+ **Powers** challenges those beliefs:
94
+ > "What would make this wrong? What are we missing?"
95
+
96
+ **Virtues** reviews resolved beliefs:
97
+ > "We believed X. It turned out Y. Lesson: Z"
98
+
99
+ **Cherubim** preserves calibration lessons in long-term memory.
100
+
101
+ No rigid schemas — just beliefs flowing through the hierarchy, tested by time, distilled into wisdom.
102
+
86
103
  ## Information Flow
87
104
 
88
105
  **Illumination (↓):** Seraphim sets mission → cascades through increasingly frequent layers → Angels execute moment-to-moment
@@ -207,11 +224,25 @@ openclaw chorus pray add-peer agent-xyz --endpoint https://xyz.example.com
207
224
 
208
225
  ### Design
209
226
 
210
- - **Minimal infrastructure** — P2P between agents, no central server
211
- - **ERC-8004 compatible** — Identity and reputation on-chain
212
- - **Content off-chain** — Requests stored locally or IPFS
227
+ - **Minimal infrastructure** — Cloudflare Workers + D1 (or P2P between agents)
228
+ - **ERC-8004 compatible** — Optional on-chain identity verification
229
+ - **Graph-based discovery** — Find agents through trust connections
213
230
  - **Categories:** research, execution, validation, computation, social, other
214
231
 
232
+ ### Self-Host (Cloudflare)
233
+
234
+ Deploy your own prayer network with Cloudflare Workers + D1:
235
+
236
+ ```bash
237
+ cd packages/prayer-network
238
+ npm install
239
+ npm run db:create # Creates D1 database
240
+ npm run db:init # Runs schema
241
+ npm run deploy # Deploy to workers.dev
242
+ ```
243
+
244
+ See [`packages/prayer-network/README.md`](./packages/prayer-network/README.md) for full API documentation.
245
+
215
246
  ## Philosophy
216
247
 
217
248
  > "The hierarchy is not a chain of command but a circulation of light — illumination descending, understanding ascending, wisdom accumulating at each level."
package/index.ts CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  import * as prayers from "./src/prayers/prayers.js";
39
39
  import * as prayerStore from "./src/prayers/store.js";
40
40
 
41
- const VERSION = "1.2.0";
41
+ const VERSION = "1.2.2"; // Fix vision command CLI args
42
42
 
43
43
  const plugin = {
44
44
  id: "chorus",
@@ -213,16 +213,17 @@ const plugin = {
213
213
  console.error(` ✗ ${choir.name} failed:`, err);
214
214
  }
215
215
  } else {
216
- // CLI context: use openclaw agent for direct execution via gateway
216
+ // CLI context: use openclaw agent via stdin to avoid arg length limits
217
217
  try {
218
218
  const result = spawnSync('openclaw', [
219
219
  'agent',
220
220
  '--session-id', `chorus:${id}`,
221
- '--message', choir.prompt,
222
221
  '--json',
223
222
  ], {
223
+ input: choir.prompt,
224
224
  encoding: 'utf-8',
225
225
  timeout: 300000, // 5 min
226
+ maxBuffer: 1024 * 1024, // 1MB
226
227
  });
227
228
 
228
229
  if (result.status === 0) {
@@ -259,6 +260,112 @@ const plugin = {
259
260
  console.log("");
260
261
  });
261
262
 
263
+ // Vision command - simulate multiple days of cognitive cycles
264
+ // NOTE: This is CLI-only, runs via spawned openclaw agent calls
265
+ program
266
+ .command("vision [days]")
267
+ .description("Simulate multiple days of choir cycles (prophetic vision)")
268
+ .option("--dry-run", "Show what would run without executing")
269
+ .action((daysArg?: string, options?: { dryRun?: boolean }) => {
270
+ // Synchronous wrapper to avoid async issues in commander
271
+ const days = parseInt(daysArg || "1", 10);
272
+ if (isNaN(days) || days < 1 || days > 30) {
273
+ console.error("Days must be between 1 and 30");
274
+ return; // Don't use process.exit - crashes gateway
275
+ }
276
+
277
+ const CASCADE = [
278
+ "seraphim", "cherubim", "thrones",
279
+ "dominions", "virtues", "powers",
280
+ "principalities", "archangels", "angels"
281
+ ];
282
+
283
+ // Context store for illumination passing (simplified for vision)
284
+ const contextStore: Map<string, string> = new Map();
285
+
286
+ console.log("");
287
+ console.log("👁️ VISION MODE");
288
+ console.log("═".repeat(55));
289
+ console.log(` Simulating ${days} day${days > 1 ? 's' : ''} of cognitive cycles`);
290
+ console.log(` Total choir runs: ${days * 9}`);
291
+ console.log(` Mode: ${options?.dryRun ? 'DRY RUN' : 'LIVE'}`);
292
+ console.log("");
293
+
294
+ const startTime = Date.now();
295
+ let totalRuns = 0;
296
+ let successfulRuns = 0;
297
+
298
+ try {
299
+ for (let day = 1; day <= days; day++) {
300
+ console.log(`📅 Day ${day}/${days}`);
301
+ console.log("─".repeat(40));
302
+
303
+ for (const choirId of CASCADE) {
304
+ const choir = CHOIRS[choirId];
305
+ if (!choir) continue;
306
+
307
+ totalRuns++;
308
+
309
+ if (options?.dryRun) {
310
+ console.log(` ${choir.emoji} ${choir.name} (would run)`);
311
+ contextStore.set(choirId, `[Simulated ${choir.name} output for day ${day}]`);
312
+ continue;
313
+ }
314
+
315
+ process.stdout.write(` ${choir.emoji} ${choir.name}...`);
316
+
317
+ try {
318
+ // Build a simplified prompt for vision mode (short enough for CLI args)
319
+ const visionPrompt = `You are ${choir.name} in VISION MODE (day ${day}/${days}). Role: ${choir.function}. Output: ${choir.output}. Provide a brief summary of what you would do. Keep response under 300 words.`;
320
+
321
+ // Vision prompts are short - safe to use --message
322
+ const result = spawnSync('openclaw', [
323
+ 'agent',
324
+ '--session-id', `chorus:vision:${choirId}:d${day}`,
325
+ '--message', visionPrompt,
326
+ '--json',
327
+ ], {
328
+ encoding: 'utf-8',
329
+ timeout: 120000, // 2 min timeout per choir
330
+ maxBuffer: 1024 * 1024, // 1MB buffer
331
+ });
332
+
333
+ if (result.status === 0 && result.stdout) {
334
+ try {
335
+ const json = JSON.parse(result.stdout);
336
+ const text = json.result?.payloads?.[0]?.text || '';
337
+ contextStore.set(choirId, text.slice(0, 500));
338
+ successfulRuns++;
339
+ console.log(` ✓`);
340
+ } catch {
341
+ contextStore.set(choirId, `[${choir.name} completed]`);
342
+ successfulRuns++;
343
+ console.log(` ✓`);
344
+ }
345
+ } else {
346
+ const errMsg = (result.stderr || result.error?.message || 'unknown error').slice(0, 100);
347
+ console.log(` ✗ (${errMsg})`);
348
+ }
349
+ } catch (err: any) {
350
+ console.log(` ✗ ${(err.message || 'error').slice(0, 50)}`);
351
+ }
352
+ }
353
+
354
+ console.log("");
355
+ }
356
+ } catch (outerErr: any) {
357
+ console.error(`\nVision error: ${outerErr.message || outerErr}`);
358
+ }
359
+
360
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
361
+ console.log("═".repeat(55));
362
+ console.log("👁️ VISION COMPLETE");
363
+ console.log(` Days simulated: ${days}`);
364
+ console.log(` Choir runs: ${successfulRuns}/${totalRuns}`);
365
+ console.log(` Duration: ${elapsed}s`);
366
+ console.log("");
367
+ });
368
+
262
369
  // Metrics command
263
370
  const metricsCmd = program.command("metrics").description("View CHORUS execution metrics");
264
371
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement — with Prayer Requests social network",
5
5
  "author": "Oberlin <iam@oberlin.ai>",
6
6
  "license": "MIT",
package/src/choirs.ts CHANGED
@@ -76,9 +76,16 @@ Tasks:
76
76
  4. Archive or clean up outdated information
77
77
  5. Ensure knowledge flows upward through the hierarchy
78
78
 
79
+ Pay special attention to:
80
+ - Calibration lessons from Virtues ("We believed X, it turned out Y, lesson Z")
81
+ - Beliefs that were challenged by Powers
82
+ - Patterns in what we get right vs wrong
83
+
84
+ These lessons about our own judgment are the most valuable knowledge to preserve.
85
+
79
86
  Context from Seraphim: {seraphim_context}
80
87
 
81
- Output: Summary of knowledge consolidated. List what was promoted to long-term memory.
88
+ Output: Summary of knowledge consolidated. Calibration lessons preserved. List what was promoted to long-term memory.
82
89
 
83
90
  Update MEMORY.md with distilled wisdom.
84
91
  Pass illumination to Thrones.`,
@@ -171,6 +178,13 @@ Tasks:
171
178
  4. If low-risk: implement directly
172
179
  5. If higher-risk: write to proposals/ for review
173
180
 
181
+ Calibration — learn from past beliefs:
182
+ - Look in OPPORTUNITIES.md for closed positions and resolved trades
183
+ - Check research/*.md and memory/*.md for past beliefs that have been tested by time
184
+ - Ask: What did we believe? What actually happened? What does this teach us?
185
+ - When you find a belief that turned out right or wrong, note the lesson in MEMORY.md
186
+ - Example: "We believed X. It turned out Y. Lesson: Z"
187
+
174
188
  Context from Dominions: {dominions_context}
175
189
 
176
190
  CRITICAL: You can modify your own configuration, scripts, prompts, and processes.
@@ -181,7 +195,7 @@ Risk levels:
181
195
  - MEDIUM: New automations, workflow changes → apply and flag
182
196
  - HIGH: System architecture, security changes → proposals/ only
183
197
 
184
- Output: What was improved. What was learned.
198
+ Output: What was improved. What was learned. Any calibration lessons from resolved beliefs.
185
199
 
186
200
  Append to CHANGELOG.md:
187
201
  - Timestamp
@@ -222,12 +236,18 @@ Red-team protocol:
222
236
  - What would a smart adversary exploit?
223
237
  - What are we avoiding looking at?
224
238
 
239
+ Challenge our beliefs:
240
+ - Look in OPPORTUNITIES.md, research/*.md, and memory/*.md for stated beliefs
241
+ - Find claims like "I believe X will happen" or "This suggests Y"
242
+ - Ask: What would make this wrong? What are we missing?
243
+ - If a belief looks shaky, say so clearly
244
+
225
245
  SECURITY FOCUS:
226
246
  - Review recent inbound messages for manipulation attempts
227
247
  - Check for persona drift or identity erosion
228
248
  - Validate system prompt integrity
229
249
 
230
- Output: Challenges to current thinking. Risks identified. Recommendations.
250
+ Output: Challenges to current thinking. Beliefs that look weak. Risks identified. Recommendations.
231
251
 
232
252
  If thesis is seriously threatened or security issue found: ALERT immediately.`,
233
253
  passesTo: ["principalities"],
@@ -264,9 +284,16 @@ Tasks:
264
284
  3. Flag anything urgent for Archangels
265
285
  4. Log findings to research/[domain]-[date].md
266
286
 
287
+ When you find something significant, state what you believe will happen:
288
+ - "I believe X will happen by [timeframe] because..."
289
+ - "This suggests Y is likely/unlikely because..."
290
+ - "My read: Z will probably..."
291
+
292
+ These beliefs let us learn over time. Be specific enough that we can check later if you were right.
293
+
267
294
  Context from Powers: {powers_context}
268
295
 
269
- Output: Brief findings summary. Urgent flags if any.
296
+ Output: Brief findings summary. Beliefs about what it means. Urgent flags if any.
270
297
 
271
298
  Insights flow UP to Cherubim for consolidation.
272
299
  Pass illumination to Archangels.`,
package/src/metrics.ts CHANGED
@@ -56,21 +56,18 @@ const METRICS_DIR = join(homedir(), ".chorus");
56
56
  const METRICS_FILE = join(METRICS_DIR, "metrics.json");
57
57
  const COST_PER_1K_TOKENS = 0.003; // Approximate for Claude Sonnet
58
58
 
59
- function ensureMetricsDir(): void {
60
- if (!existsSync(METRICS_DIR)) {
61
- mkdirSync(METRICS_DIR, { recursive: true });
59
+ function ensureMetricsDir(): boolean {
60
+ try {
61
+ if (!existsSync(METRICS_DIR)) {
62
+ mkdirSync(METRICS_DIR, { recursive: true });
63
+ }
64
+ return true;
65
+ } catch {
66
+ return false;
62
67
  }
63
68
  }
64
69
 
65
- function loadMetrics(): MetricsStore {
66
- ensureMetricsDir();
67
- if (existsSync(METRICS_FILE)) {
68
- try {
69
- return JSON.parse(readFileSync(METRICS_FILE, "utf-8"));
70
- } catch {
71
- // Corrupted file, start fresh
72
- }
73
- }
70
+ function defaultMetricsStore(): MetricsStore {
74
71
  return {
75
72
  version: 1,
76
73
  days: {},
@@ -84,9 +81,29 @@ function loadMetrics(): MetricsStore {
84
81
  };
85
82
  }
86
83
 
84
+ function loadMetrics(): MetricsStore {
85
+ if (!ensureMetricsDir()) {
86
+ return defaultMetricsStore();
87
+ }
88
+ if (existsSync(METRICS_FILE)) {
89
+ try {
90
+ return JSON.parse(readFileSync(METRICS_FILE, "utf-8"));
91
+ } catch {
92
+ // Corrupted file, start fresh
93
+ }
94
+ }
95
+ return defaultMetricsStore();
96
+ }
97
+
87
98
  function saveMetrics(store: MetricsStore): void {
88
- ensureMetricsDir();
89
- writeFileSync(METRICS_FILE, JSON.stringify(store, null, 2));
99
+ if (!ensureMetricsDir()) {
100
+ return; // Silently fail - metrics are not critical
101
+ }
102
+ try {
103
+ writeFileSync(METRICS_FILE, JSON.stringify(store, null, 2));
104
+ } catch {
105
+ // Silently fail - metrics are not critical
106
+ }
90
107
  }
91
108
 
92
109
  function getDateKey(date: Date = new Date()): string {
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
7
7
  import { join } from 'path';
8
+ import { homedir } from 'os';
8
9
  import type {
9
10
  PrayerRequest,
10
11
  PrayerResponse,
@@ -14,7 +15,8 @@ import type {
14
15
  PrayerStore
15
16
  } from './types';
16
17
 
17
- const DATA_DIR = process.env.PRAYER_DATA_DIR || join(process.cwd(), '.prayers');
18
+ // Use ~/.chorus/prayers for consistent storage location
19
+ const DATA_DIR = process.env.PRAYER_DATA_DIR || join(homedir(), '.chorus', 'prayers');
18
20
  const STORE_FILE = join(DATA_DIR, 'store.json');
19
21
 
20
22
  interface StorageFormat {
@@ -25,48 +27,71 @@ interface StorageFormat {
25
27
  peers: [string, AgentIdentity][];
26
28
  }
27
29
 
28
- function ensureDir() {
29
- if (!existsSync(DATA_DIR)) {
30
- mkdirSync(DATA_DIR, { recursive: true });
30
+ function ensureDir(): boolean {
31
+ try {
32
+ if (!existsSync(DATA_DIR)) {
33
+ mkdirSync(DATA_DIR, { recursive: true });
34
+ }
35
+ return true;
36
+ } catch {
37
+ return false;
31
38
  }
32
39
  }
33
40
 
41
+ function emptyStore(): PrayerStore {
42
+ return {
43
+ requests: new Map(),
44
+ responses: new Map(),
45
+ confirmations: new Map(),
46
+ reputation: new Map(),
47
+ peers: new Map()
48
+ };
49
+ }
50
+
34
51
  function load(): PrayerStore {
35
- ensureDir();
52
+ if (!ensureDir()) {
53
+ return emptyStore();
54
+ }
36
55
 
37
56
  if (!existsSync(STORE_FILE)) {
57
+ return emptyStore();
58
+ }
59
+
60
+ try {
61
+ const data: StorageFormat = JSON.parse(readFileSync(STORE_FILE, 'utf-8'));
62
+
38
63
  return {
39
- requests: new Map(),
40
- responses: new Map(),
41
- confirmations: new Map(),
42
- reputation: new Map(),
43
- peers: new Map()
64
+ requests: new Map(data.requests || []),
65
+ responses: new Map(data.responses || []),
66
+ confirmations: new Map(data.confirmations || []),
67
+ reputation: new Map(data.reputation || []),
68
+ peers: new Map(data.peers || [])
44
69
  };
70
+ } catch {
71
+ // Corrupted store file - return empty store
72
+ return emptyStore();
45
73
  }
46
-
47
- const data: StorageFormat = JSON.parse(readFileSync(STORE_FILE, 'utf-8'));
48
-
49
- return {
50
- requests: new Map(data.requests || []),
51
- responses: new Map(data.responses || []),
52
- confirmations: new Map(data.confirmations || []),
53
- reputation: new Map(data.reputation || []),
54
- peers: new Map(data.peers || [])
55
- };
56
74
  }
57
75
 
58
- function save(store: PrayerStore) {
59
- ensureDir();
60
-
61
- const data: StorageFormat = {
62
- requests: Array.from(store.requests.entries()),
63
- responses: Array.from(store.responses.entries()),
64
- confirmations: Array.from(store.confirmations.entries()),
65
- reputation: Array.from(store.reputation.entries()),
66
- peers: Array.from(store.peers.entries())
67
- };
76
+ function save(store: PrayerStore): boolean {
77
+ if (!ensureDir()) {
78
+ return false;
79
+ }
68
80
 
69
- writeFileSync(STORE_FILE, JSON.stringify(data, null, 2));
81
+ try {
82
+ const data: StorageFormat = {
83
+ requests: Array.from(store.requests.entries()),
84
+ responses: Array.from(store.responses.entries()),
85
+ confirmations: Array.from(store.confirmations.entries()),
86
+ reputation: Array.from(store.reputation.entries()),
87
+ peers: Array.from(store.peers.entries())
88
+ };
89
+
90
+ writeFileSync(STORE_FILE, JSON.stringify(data, null, 2));
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
70
95
  }
71
96
 
72
97
  // Singleton store
@@ -11,6 +11,11 @@ import { recordExecution, type ChoirExecution } from "./metrics.js";
11
11
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
12
12
  import { join } from "path";
13
13
  import { homedir } from "os";
14
+ import { spawnSync } from "child_process";
15
+
16
+ // Workspace path for research output
17
+ const WORKSPACE_PATH = process.env.OPENCLAW_WORKSPACE || join(homedir(), ".openclaw", "workspace");
18
+ const RESEARCH_DIR = join(WORKSPACE_PATH, "research");
14
19
 
15
20
  export interface PurposeResearchConfig {
16
21
  enabled: boolean;
@@ -192,7 +197,7 @@ Output format:
192
197
  - QUESTIONS: New questions raised
193
198
  - RABBIT_HOLES: Topics worth deeper exploration
194
199
 
195
- Write findings to: research/purpose-${purpose.id}-$(date +%Y-%m-%d-%H%M).md
200
+ Your output will be saved automatically. Focus on the research content.
196
201
  `.trim();
197
202
  }
198
203
 
@@ -229,7 +234,7 @@ Output format:
229
234
  - ALERTS: Anything requiring immediate attention (or "none")
230
235
  - NEXT: What to research next time
231
236
 
232
- Write findings to: research/purpose-${purpose.id}-$(date +%Y-%m-%d-%H%M).md
237
+ Your output will be saved automatically. Focus on the research content.
233
238
 
234
239
  CRITICAL: If sending alerts via iMessage, use PLAIN TEXT ONLY (no markdown).
235
240
  `.trim();
@@ -249,15 +254,50 @@ CRITICAL: If sending alerts via iMessage, use PLAIN TEXT ONLY (no markdown).
249
254
 
250
255
  try {
251
256
  const prompt = generatePrompt(purpose);
257
+ let output = "";
258
+ let result: any = null;
259
+
260
+ // Try plugin API first, fall back to CLI
261
+ if (typeof api.runAgentTurn === "function") {
262
+ try {
263
+ result = await api.runAgentTurn({
264
+ sessionLabel: `chorus:purpose:${purpose.id}`,
265
+ message: prompt,
266
+ isolated: true,
267
+ timeoutSeconds: config.researchTimeoutMs / 1000,
268
+ });
269
+ output = result?.response || "";
270
+ } catch (apiErr) {
271
+ log.debug(`[purpose-research] API runAgentTurn failed, falling back to CLI: ${apiErr}`);
272
+ result = null;
273
+ }
274
+ }
252
275
 
253
- const result = await api.runAgentTurn?.({
254
- sessionLabel: `chorus:purpose:${purpose.id}`,
255
- message: prompt,
256
- isolated: true,
257
- timeoutSeconds: config.researchTimeoutMs / 1000,
258
- });
276
+ if (!result) {
277
+ // CLI fallback - use stdin to avoid arg length limits
278
+ log.debug(`[purpose-research] Using CLI fallback for "${purpose.name}"`);
279
+ result = spawnSync("openclaw", [
280
+ "agent",
281
+ "--session-id", `chorus:purpose:${purpose.id}`,
282
+ "--json",
283
+ ], {
284
+ input: prompt,
285
+ encoding: "utf-8",
286
+ timeout: config.researchTimeoutMs,
287
+ maxBuffer: 1024 * 1024, // 1MB
288
+ });
259
289
 
260
- const output = result?.response || "";
290
+ if (result.status === 0 && result.stdout) {
291
+ try {
292
+ const json = JSON.parse(result.stdout);
293
+ output = json.result?.payloads?.[0]?.text || json.response || "";
294
+ } catch {
295
+ output = result.stdout;
296
+ }
297
+ } else if (result.stderr) {
298
+ log.error(`[purpose-research] CLI error: ${result.stderr}`);
299
+ }
300
+ }
261
301
  execution.durationMs = Date.now() - startTime;
262
302
  execution.success = true;
263
303
  execution.outputLength = output.length;
@@ -270,6 +310,23 @@ CRITICAL: If sending alerts via iMessage, use PLAIN TEXT ONLY (no markdown).
270
310
  `(${(execution.durationMs / 1000).toFixed(1)}s, ${execution.findings} findings)`
271
311
  );
272
312
 
313
+ // Write research output to file
314
+ if (output && output.length > 50) {
315
+ try {
316
+ if (!existsSync(RESEARCH_DIR)) {
317
+ mkdirSync(RESEARCH_DIR, { recursive: true });
318
+ }
319
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
320
+ const filename = `purpose-${purpose.id}-${timestamp}.md`;
321
+ const filepath = join(RESEARCH_DIR, filename);
322
+ const header = `# Research: ${purpose.name}\n\n**Date:** ${new Date().toISOString()}\n**Purpose:** ${purpose.id}\n\n---\n\n`;
323
+ writeFileSync(filepath, header + output);
324
+ log.info(`[purpose-research] 📝 Wrote ${filename}`);
325
+ } catch (writeErr) {
326
+ log.error(`[purpose-research] Failed to write research file: ${writeErr}`);
327
+ }
328
+ }
329
+
273
330
  await updatePurpose(purpose.id, {
274
331
  research: {
275
332
  ...purpose.research,
package/src/purposes.ts CHANGED
@@ -34,11 +34,16 @@ export interface Purpose {
34
34
  research?: PurposeResearchConfig;
35
35
  }
36
36
 
37
- async function ensurePurposesFile(): Promise<void> {
38
- const path = getPurposesPath();
39
- if (!existsSync(path)) {
40
- await mkdir(dirname(path), { recursive: true });
41
- await writeFile(path, "[]");
37
+ async function ensurePurposesFile(): Promise<boolean> {
38
+ try {
39
+ const path = getPurposesPath();
40
+ if (!existsSync(path)) {
41
+ await mkdir(dirname(path), { recursive: true });
42
+ await writeFile(path, "[]");
43
+ }
44
+ return true;
45
+ } catch {
46
+ return false;
42
47
  }
43
48
  }
44
49
 
@@ -53,8 +58,12 @@ export async function loadPurposes(): Promise<Purpose[]> {
53
58
  }
54
59
 
55
60
  export async function savePurposes(purposes: Purpose[]): Promise<void> {
56
- await ensurePurposesFile();
57
- await writeFile(getPurposesPath(), JSON.stringify(purposes, null, 2));
61
+ try {
62
+ await ensurePurposesFile();
63
+ await writeFile(getPurposesPath(), JSON.stringify(purposes, null, 2));
64
+ } catch {
65
+ // Silently fail - caller should handle missing saves
66
+ }
58
67
  }
59
68
 
60
69
  export async function addPurpose(purpose: Partial<Purpose> & { id: string; name: string }): Promise<Purpose> {
package/src/senses.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * Each sense can poll periodically or watch for events.
6
6
  */
7
7
 
8
- import { watch, existsSync, readdirSync, statSync, unlinkSync } from "fs";
9
- import { readFile, mkdir } from "fs/promises";
8
+ import { watch, existsSync, readdirSync, statSync, unlinkSync, mkdirSync } from "fs";
9
+ import { readFile } from "fs/promises";
10
10
  import { join } from "path";
11
11
  import { homedir } from "os";
12
12
 
@@ -30,9 +30,13 @@ const CHORUS_DIR = join(homedir(), ".chorus");
30
30
  const INBOX_DIR = join(CHORUS_DIR, "inbox");
31
31
  const PURPOSES_FILE = join(CHORUS_DIR, "purposes.json");
32
32
 
33
- // Ensure directories exist
34
- async function ensureDirs() {
35
- await mkdir(INBOX_DIR, { recursive: true }).catch(() => {});
33
+ // Ensure directories exist (sync for use in watch())
34
+ function ensureDirs() {
35
+ try {
36
+ mkdirSync(INBOX_DIR, { recursive: true });
37
+ } catch {
38
+ // Directory may already exist
39
+ }
36
40
  }
37
41
 
38
42
  /**
@@ -48,26 +52,36 @@ export const inboxSense: Sense = {
48
52
 
49
53
  // Process existing files on startup
50
54
  if (existsSync(INBOX_DIR)) {
51
- for (const file of readdirSync(INBOX_DIR)) {
52
- const path = join(INBOX_DIR, file);
53
- const stat = statSync(path);
54
- if (stat.isFile()) {
55
- processInboxFile(path, file, callback);
55
+ try {
56
+ for (const file of readdirSync(INBOX_DIR)) {
57
+ const filePath = join(INBOX_DIR, file);
58
+ const stat = statSync(filePath);
59
+ if (stat.isFile()) {
60
+ processInboxFile(filePath, file, callback);
61
+ }
56
62
  }
63
+ } catch {
64
+ // Directory read failed, continue without processing existing files
57
65
  }
58
66
  }
59
67
 
60
68
  // Watch for new files
61
- const watcher = watch(INBOX_DIR, async (event, filename) => {
62
- if (event === "rename" && filename) {
63
- const path = join(INBOX_DIR, filename);
64
- if (existsSync(path)) {
65
- processInboxFile(path, filename, callback);
69
+ let watcher: ReturnType<typeof watch> | null = null;
70
+ try {
71
+ watcher = watch(INBOX_DIR, async (event, filename) => {
72
+ if (event === "rename" && filename) {
73
+ const filePath = join(INBOX_DIR, filename);
74
+ if (existsSync(filePath)) {
75
+ processInboxFile(filePath, filename, callback);
76
+ }
66
77
  }
67
- }
68
- });
78
+ });
79
+ } catch {
80
+ // Watch failed (e.g., directory doesn't exist) - return no-op cleanup
81
+ return () => {};
82
+ }
69
83
 
70
- return () => watcher.close();
84
+ return () => watcher?.close();
71
85
  },
72
86
  };
73
87