@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.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- 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
|
+
}
|