@clearlysh/beehaven 0.8.16

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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
@@ -0,0 +1,518 @@
1
+ /**
2
+ * .clearly/ Bidirectional Sync
3
+ *
4
+ * Keeps a project-scoped .clearly/ directory in sync with the cloud DO.
5
+ * This is how Claude Code gets project context (reads CLAUDE.md) and
6
+ * pushes content back (writes notes, edits tasks, stores memories).
7
+ *
8
+ * Directory layout:
9
+ * .clearly/
10
+ * CLAUDE.md ← daemon-generated context (read-only for Claude)
11
+ * notes/ ← bidirectional: local .md ↔ DO notes
12
+ * tasks.yaml ← bidirectional: local YAML ↔ DO board items
13
+ * memories.yaml ← bidirectional: local YAML ↔ DO agent memory
14
+ * .sync.json ← sync state (hashes, timestamps, versions)
15
+ *
16
+ * Sync is PROJECT-SCOPED: only data for the linked project syncs.
17
+ * Does NOT sync: all notes, compositions, SVG blobs, R2 files, chat, billing.
18
+ */
19
+ import { EventEmitter } from 'events';
20
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, watch as fsWatch, } from 'fs';
21
+ import { join, basename } from 'path';
22
+ import { createHash } from 'crypto';
23
+ const CLEARLY_DIR = '.clearly';
24
+ const SYNC_FILE = '.sync.json';
25
+ const CONTEXT_FILE = 'CLAUDE.md';
26
+ const TASKS_FILE = 'tasks.yaml';
27
+ const MEMORIES_FILE = 'memories.yaml';
28
+ const NOTES_DIR = 'notes';
29
+ /** How often to pull context from the DO (ms) */
30
+ const PULL_INTERVAL = 60_000; // 1 minute
31
+ /** Debounce for local file changes before pushing (ms) */
32
+ const PUSH_DEBOUNCE = 2_000;
33
+ export class ClearlySync extends EventEmitter {
34
+ projectPath;
35
+ projectId;
36
+ callAction;
37
+ clearlyDir;
38
+ state;
39
+ pullTimer = null;
40
+ pushTimer = null;
41
+ watchers = [];
42
+ pushing = false;
43
+ pulling = false;
44
+ /** Suppress push while we're writing from a pull (avoid echo) */
45
+ suppressPush = false;
46
+ constructor(opts) {
47
+ super();
48
+ this.projectPath = opts.projectPath;
49
+ this.projectId = opts.projectId;
50
+ this.callAction = opts.callAction;
51
+ this.clearlyDir = join(this.projectPath, CLEARLY_DIR);
52
+ this.state = this.loadState();
53
+ }
54
+ /** Initialize directory structure and start sync */
55
+ async start() {
56
+ this.ensureDirectories();
57
+ this.state = this.loadState();
58
+ // Initial pull — get context from cloud
59
+ await this.pull();
60
+ // Watch local .clearly/ for changes
61
+ this.watchLocal();
62
+ // Periodic pull
63
+ this.pullTimer = setInterval(() => this.pull(), PULL_INTERVAL);
64
+ this.emit('started');
65
+ }
66
+ /** Stop sync — clean up watchers and timers */
67
+ stop() {
68
+ if (this.pullTimer)
69
+ clearInterval(this.pullTimer);
70
+ if (this.pushTimer)
71
+ clearTimeout(this.pushTimer);
72
+ for (const w of this.watchers)
73
+ w.close();
74
+ this.watchers = [];
75
+ this.pullTimer = null;
76
+ this.pushTimer = null;
77
+ this.emit('stopped');
78
+ }
79
+ /** Force a full pull from cloud → local */
80
+ async pull() {
81
+ if (this.pulling)
82
+ return;
83
+ this.pulling = true;
84
+ try {
85
+ const context = (await this.callAction('project-context', {
86
+ projectId: this.projectId,
87
+ }));
88
+ if (!context || !context.project) {
89
+ this.emit('pull-error', 'No project context returned');
90
+ return;
91
+ }
92
+ this.suppressPush = true;
93
+ // Write CLAUDE.md
94
+ this.writeContextFile(context);
95
+ // Sync tasks
96
+ this.writeTasksFile(context.tasks || []);
97
+ // Sync memories
98
+ this.writeMemoriesFile(context.memories || []);
99
+ // Sync notes (cloud → local, don't overwrite local-only files)
100
+ this.syncNotesDown(context.recentNotes || []);
101
+ this.state.lastPullAt = Date.now();
102
+ this.saveState();
103
+ this.suppressPush = false;
104
+ this.emit('pulled', context);
105
+ }
106
+ catch (err) {
107
+ this.suppressPush = false;
108
+ this.emit('pull-error', err.message);
109
+ }
110
+ finally {
111
+ this.pulling = false;
112
+ }
113
+ }
114
+ /** Push local changes → cloud */
115
+ async push() {
116
+ if (this.pushing || this.suppressPush)
117
+ return;
118
+ this.pushing = true;
119
+ try {
120
+ await this.pushTasks();
121
+ await this.pushMemories();
122
+ await this.pushNotes();
123
+ this.state.lastPushAt = Date.now();
124
+ this.saveState();
125
+ this.emit('pushed');
126
+ }
127
+ catch (err) {
128
+ this.emit('push-error', err.message);
129
+ }
130
+ finally {
131
+ this.pushing = false;
132
+ }
133
+ }
134
+ /** Get the .clearly directory path */
135
+ get dirPath() {
136
+ return this.clearlyDir;
137
+ }
138
+ // ── Context File Generation ──────────────────────────────────────────
139
+ writeContextFile(context) {
140
+ const lines = [];
141
+ lines.push(`# ${context.project.title}`);
142
+ if (context.project.description) {
143
+ lines.push(`${context.project.description}`);
144
+ }
145
+ lines.push('');
146
+ // Active tasks
147
+ const activeTasks = (context.tasks || []).filter((t) => t.status === 'active');
148
+ const ideaTasks = (context.tasks || []).filter((t) => t.status === 'idea');
149
+ const shippedTasks = (context.tasks || []).filter((t) => t.status === 'shipped').slice(0, 5);
150
+ if (activeTasks.length > 0 || ideaTasks.length > 0) {
151
+ lines.push('## Tasks');
152
+ for (const t of activeTasks) {
153
+ lines.push(`- [ ] ${t.title}${t.description ? ` — ${t.description}` : ''}`);
154
+ }
155
+ for (const t of ideaTasks) {
156
+ lines.push(`- [ ] ${t.title} *(idea)*`);
157
+ }
158
+ for (const t of shippedTasks) {
159
+ lines.push(`- [x] ${t.title}`);
160
+ }
161
+ lines.push('');
162
+ }
163
+ // Memories
164
+ if (context.memories && context.memories.length > 0) {
165
+ lines.push('## Memories');
166
+ for (const m of context.memories) {
167
+ lines.push(`- **${m.key}**: ${m.value}`);
168
+ }
169
+ lines.push('');
170
+ }
171
+ // Recent notes
172
+ if (context.recentNotes && context.recentNotes.length > 0) {
173
+ lines.push('## Recent Notes');
174
+ for (const n of context.recentNotes) {
175
+ const date = new Date(n.updatedAt).toISOString().slice(0, 10);
176
+ lines.push(`- ${n.title} *(${date})*${n.preview ? ` — ${n.preview}` : ''}`);
177
+ }
178
+ lines.push('');
179
+ }
180
+ // Agent
181
+ if (context.agent) {
182
+ lines.push('## Agent');
183
+ lines.push(`State: ${context.agent.state}`);
184
+ if (context.agent.lastThought) {
185
+ lines.push(`Last thought: ${context.agent.lastThought}`);
186
+ }
187
+ lines.push('');
188
+ }
189
+ // Workspace CLI
190
+ lines.push('## Workspace CLI');
191
+ lines.push('Access the Clearly workspace via `beehaven` (if the daemon is running):');
192
+ lines.push('- `beehaven actions` — list available workspace actions');
193
+ lines.push("- `beehaven call <action> '<json>'` — call any action");
194
+ lines.push('- `beehaven call ticket-list \'{"projectId":"<id>"}\'` — list a project board\'s cards (a card is a ticket)');
195
+ lines.push('- `beehaven call note-create \'{"noteData":{"title":"..."}}\' ` — create a note');
196
+ lines.push('');
197
+ // Footer
198
+ lines.push('---');
199
+ lines.push(`*Synced from Clearly workspace · ${new Date().toISOString().slice(0, 16)}*`);
200
+ lines.push('*Edit tasks.yaml, memories.yaml, or notes/ — changes sync to cloud.*');
201
+ lines.push('');
202
+ const content = lines.join('\n');
203
+ const filePath = join(this.clearlyDir, CONTEXT_FILE);
204
+ const hash = md5(content);
205
+ // Only write if changed
206
+ if (this.state.fileHashes[CONTEXT_FILE] !== hash) {
207
+ writeFileSync(filePath, content, 'utf8');
208
+ this.state.fileHashes[CONTEXT_FILE] = hash;
209
+ }
210
+ }
211
+ // ── Tasks Sync ───────────────────────────────────────────────────────
212
+ writeTasksFile(tasks) {
213
+ const lines = ['# Tasks — edit status to sync changes to Clearly', ''];
214
+ for (const t of tasks) {
215
+ lines.push(`- id: ${t.id}`);
216
+ lines.push(` title: "${escapeYaml(t.title)}"`);
217
+ lines.push(` status: ${t.status}`);
218
+ if (t.description)
219
+ lines.push(` description: "${escapeYaml(t.description)}"`);
220
+ if (t.tags && t.tags.length > 0)
221
+ lines.push(` tags: [${t.tags.join(', ')}]`);
222
+ lines.push('');
223
+ }
224
+ const content = lines.join('\n');
225
+ const filePath = join(this.clearlyDir, TASKS_FILE);
226
+ const hash = md5(content);
227
+ if (this.state.fileHashes[TASKS_FILE] !== hash) {
228
+ writeFileSync(filePath, content, 'utf8');
229
+ this.state.fileHashes[TASKS_FILE] = hash;
230
+ }
231
+ // Store remote version markers
232
+ for (const t of tasks) {
233
+ this.state.remoteVersions[`task:${t.id}`] = t.updatedAt || 0;
234
+ }
235
+ }
236
+ async pushTasks() {
237
+ const filePath = join(this.clearlyDir, TASKS_FILE);
238
+ if (!existsSync(filePath))
239
+ return;
240
+ const content = readFileSync(filePath, 'utf8');
241
+ const hash = md5(content);
242
+ if (this.state.fileHashes[TASKS_FILE] === hash)
243
+ return; // unchanged
244
+ const tasks = parseTasksYaml(content);
245
+ for (const task of tasks) {
246
+ try {
247
+ if (task.id) {
248
+ // Update existing task
249
+ await this.callAction('note-update', {
250
+ id: task.id,
251
+ updates: { title: task.title, status: task.status, description: task.description },
252
+ });
253
+ }
254
+ else {
255
+ // New task (no id) — create it
256
+ await this.callAction('note-create', {
257
+ title: task.title,
258
+ fileType: 'board',
259
+ status: task.status || 'idea',
260
+ description: task.description,
261
+ projectId: this.projectId,
262
+ });
263
+ }
264
+ }
265
+ catch (err) {
266
+ this.emit('push-error', `Task sync failed for "${task.title}": ${err.message}`);
267
+ }
268
+ }
269
+ this.state.fileHashes[TASKS_FILE] = hash;
270
+ }
271
+ // ── Memories Sync ────────────────────────────────────────────────────
272
+ writeMemoriesFile(memories) {
273
+ const lines = ['# Memories — key-value pairs synced with Clearly agent', ''];
274
+ for (const m of memories) {
275
+ lines.push(`${m.key}: "${escapeYaml(m.value)}"`);
276
+ }
277
+ lines.push('');
278
+ const content = lines.join('\n');
279
+ const filePath = join(this.clearlyDir, MEMORIES_FILE);
280
+ const hash = md5(content);
281
+ if (this.state.fileHashes[MEMORIES_FILE] !== hash) {
282
+ writeFileSync(filePath, content, 'utf8');
283
+ this.state.fileHashes[MEMORIES_FILE] = hash;
284
+ }
285
+ }
286
+ async pushMemories() {
287
+ const filePath = join(this.clearlyDir, MEMORIES_FILE);
288
+ if (!existsSync(filePath))
289
+ return;
290
+ const content = readFileSync(filePath, 'utf8');
291
+ const hash = md5(content);
292
+ if (this.state.fileHashes[MEMORIES_FILE] === hash)
293
+ return;
294
+ const memories = parseMemoriesYaml(content);
295
+ for (const mem of memories) {
296
+ try {
297
+ await this.callAction('remember', { key: mem.key, value: mem.value });
298
+ }
299
+ catch (err) {
300
+ this.emit('push-error', `Memory sync failed for "${mem.key}": ${err.message}`);
301
+ }
302
+ }
303
+ this.state.fileHashes[MEMORIES_FILE] = hash;
304
+ }
305
+ // ── Notes Sync ───────────────────────────────────────────────────────
306
+ syncNotesDown(notes) {
307
+ const notesDir = join(this.clearlyDir, NOTES_DIR);
308
+ for (const note of notes) {
309
+ const slug = slugify(note.title);
310
+ const filePath = join(notesDir, `${slug}.md`);
311
+ // Don't overwrite locally-created files that have no remote version
312
+ const remoteVersion = note.updatedAt || 0;
313
+ const knownVersion = this.state.remoteVersions[`note:${note.id}`] || 0;
314
+ if (existsSync(filePath) && remoteVersion <= knownVersion)
315
+ continue;
316
+ // Write note stub (title + preview — full content stays in cloud)
317
+ const content = `# ${note.title}\n\n${note.preview || ''}\n\n---\n*Note ID: ${note.id} · Full content at clearly.sh*\n`;
318
+ writeFileSync(filePath, content, 'utf8');
319
+ this.state.remoteVersions[`note:${note.id}`] = remoteVersion;
320
+ this.state.fileHashes[`notes/${slug}.md`] = md5(content);
321
+ }
322
+ }
323
+ async pushNotes() {
324
+ const notesDir = join(this.clearlyDir, NOTES_DIR);
325
+ if (!existsSync(notesDir))
326
+ return;
327
+ const files = readdirSync(notesDir).filter((f) => f.endsWith('.md'));
328
+ for (const file of files) {
329
+ const filePath = join(notesDir, file);
330
+ const content = readFileSync(filePath, 'utf8');
331
+ const hash = md5(content);
332
+ const key = `notes/${file}`;
333
+ if (this.state.fileHashes[key] === hash)
334
+ continue; // unchanged
335
+ // Extract title from first heading or filename
336
+ const titleMatch = content.match(/^#\s+(.+)$/m);
337
+ const title = titleMatch ? titleMatch[1].trim() : file.replace('.md', '');
338
+ // Check if this is a stub from a cloud note (has Note ID in footer)
339
+ const idMatch = content.match(/Note ID:\s*(\S+)/);
340
+ if (idMatch) {
341
+ // This is a cloud note stub — don't push stubs back up
342
+ // Only push if user added substantial content
343
+ const lines = content
344
+ .split('\n')
345
+ .filter((l) => l.trim() && !l.startsWith('#') && !l.startsWith('*') && !l.startsWith('---'));
346
+ if (lines.length <= 1) {
347
+ this.state.fileHashes[key] = hash;
348
+ continue;
349
+ }
350
+ }
351
+ try {
352
+ await this.callAction('note-create', {
353
+ title,
354
+ content,
355
+ fileType: 'note',
356
+ projectId: this.projectId,
357
+ source: 'beehaven-sync',
358
+ });
359
+ this.state.fileHashes[key] = hash;
360
+ }
361
+ catch (err) {
362
+ this.emit('push-error', `Note sync failed for "${title}": ${err.message}`);
363
+ }
364
+ }
365
+ }
366
+ // ── File Watching ────────────────────────────────────────────────────
367
+ watchLocal() {
368
+ // Watch the .clearly/ directory for changes
369
+ try {
370
+ const watcher = fsWatch(this.clearlyDir, { recursive: true }, (_event, filename) => {
371
+ if (!filename)
372
+ return;
373
+ // Ignore sync state file and CLAUDE.md (we write those)
374
+ if (filename === SYNC_FILE || filename === CONTEXT_FILE)
375
+ return;
376
+ // Ignore hidden files
377
+ if (basename(filename).startsWith('.'))
378
+ return;
379
+ this.schedulePush();
380
+ });
381
+ this.watchers.push(watcher);
382
+ }
383
+ catch {
384
+ // recursive watch not supported on all platforms — fall back to individual watches
385
+ this.watchFile(join(this.clearlyDir, TASKS_FILE));
386
+ this.watchFile(join(this.clearlyDir, MEMORIES_FILE));
387
+ this.watchDir(join(this.clearlyDir, NOTES_DIR));
388
+ }
389
+ }
390
+ watchFile(filePath) {
391
+ if (!existsSync(filePath))
392
+ return;
393
+ try {
394
+ const watcher = fsWatch(filePath, () => this.schedulePush());
395
+ this.watchers.push(watcher);
396
+ }
397
+ catch { }
398
+ }
399
+ watchDir(dirPath) {
400
+ if (!existsSync(dirPath))
401
+ return;
402
+ try {
403
+ const watcher = fsWatch(dirPath, (_event, filename) => {
404
+ if (!filename || basename(filename).startsWith('.'))
405
+ return;
406
+ this.schedulePush();
407
+ });
408
+ this.watchers.push(watcher);
409
+ }
410
+ catch { }
411
+ }
412
+ schedulePush() {
413
+ if (this.suppressPush)
414
+ return;
415
+ if (this.pushTimer)
416
+ clearTimeout(this.pushTimer);
417
+ this.pushTimer = setTimeout(() => this.push(), PUSH_DEBOUNCE);
418
+ }
419
+ // ── State Persistence ────────────────────────────────────────────────
420
+ ensureDirectories() {
421
+ mkdirSync(this.clearlyDir, { recursive: true });
422
+ mkdirSync(join(this.clearlyDir, NOTES_DIR), { recursive: true });
423
+ }
424
+ loadState() {
425
+ const filePath = join(this.clearlyDir, SYNC_FILE);
426
+ if (!existsSync(filePath)) {
427
+ return {
428
+ lastPullAt: 0,
429
+ lastPushAt: 0,
430
+ fileHashes: {},
431
+ remoteVersions: {},
432
+ projectId: this.projectId,
433
+ };
434
+ }
435
+ try {
436
+ return JSON.parse(readFileSync(filePath, 'utf8'));
437
+ }
438
+ catch {
439
+ return {
440
+ lastPullAt: 0,
441
+ lastPushAt: 0,
442
+ fileHashes: {},
443
+ remoteVersions: {},
444
+ projectId: this.projectId,
445
+ };
446
+ }
447
+ }
448
+ saveState() {
449
+ const filePath = join(this.clearlyDir, SYNC_FILE);
450
+ writeFileSync(filePath, JSON.stringify(this.state, null, 2), 'utf8');
451
+ }
452
+ }
453
+ // ── Helpers ──────────────────────────────────────────────────────────────
454
+ function md5(content) {
455
+ return createHash('md5').update(content).digest('hex');
456
+ }
457
+ function escapeYaml(str) {
458
+ return str.replace(/"/g, '\\"').replace(/\n/g, '\\n');
459
+ }
460
+ function slugify(title) {
461
+ return title
462
+ .toLowerCase()
463
+ .replace(/[^a-z0-9]+/g, '-')
464
+ .replace(/^-|-$/g, '')
465
+ .slice(0, 60);
466
+ }
467
+ /** Parse the simple YAML-ish tasks format */
468
+ export function parseTasksYaml(content) {
469
+ const tasks = [];
470
+ let current = null;
471
+ for (const line of content.split('\n')) {
472
+ const trimmed = line.trim();
473
+ if (!trimmed || trimmed.startsWith('#'))
474
+ continue;
475
+ if (trimmed.startsWith('- id:')) {
476
+ if (current && current.title)
477
+ tasks.push(current);
478
+ current = { id: trimmed.replace('- id:', '').trim() };
479
+ }
480
+ else if (current) {
481
+ const match = trimmed.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
482
+ if (match) {
483
+ const [, key, value] = match;
484
+ if (key === 'title')
485
+ current.title = value;
486
+ else if (key === 'status')
487
+ current.status = value;
488
+ else if (key === 'description')
489
+ current.description = value;
490
+ else if (key === 'tags') {
491
+ current.tags = value
492
+ .replace(/[[\]]/g, '')
493
+ .split(',')
494
+ .map((t) => t.trim())
495
+ .filter(Boolean);
496
+ }
497
+ }
498
+ }
499
+ }
500
+ if (current && current.title)
501
+ tasks.push(current);
502
+ return tasks;
503
+ }
504
+ /** Parse the simple key: "value" memories format */
505
+ export function parseMemoriesYaml(content) {
506
+ const memories = [];
507
+ for (const line of content.split('\n')) {
508
+ const trimmed = line.trim();
509
+ if (!trimmed || trimmed.startsWith('#'))
510
+ continue;
511
+ const match = trimmed.match(/^([^:]+):\s*"?([^"]*)"?\s*$/);
512
+ if (match) {
513
+ const [, key, value] = match;
514
+ memories.push({ key: key.trim(), value: value.trim() });
515
+ }
516
+ }
517
+ return memories;
518
+ }