homaruscc 0.2.0 → 0.4.0

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 (53) hide show
  1. package/README.md +30 -9
  2. package/dashboard/dist/assets/index-CIzoeO8A.js +52 -0
  3. package/dashboard/dist/favicon.ico +0 -0
  4. package/dashboard/dist/favicon.png +0 -0
  5. package/dashboard/dist/index.html +15 -0
  6. package/dist/agent-registry.d.ts +9 -12
  7. package/dist/agent-registry.d.ts.map +1 -1
  8. package/dist/agent-registry.js +44 -113
  9. package/dist/agent-registry.js.map +1 -1
  10. package/dist/claude-code-registrar.d.ts +10 -0
  11. package/dist/claude-code-registrar.d.ts.map +1 -0
  12. package/dist/claude-code-registrar.js +71 -0
  13. package/dist/claude-code-registrar.js.map +1 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +28 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/compaction-manager.d.ts +24 -0
  19. package/dist/compaction-manager.d.ts.map +1 -1
  20. package/dist/compaction-manager.js +88 -7
  21. package/dist/compaction-manager.js.map +1 -1
  22. package/dist/dashboard-server.d.ts.map +1 -1
  23. package/dist/dashboard-server.js +348 -4
  24. package/dist/dashboard-server.js.map +1 -1
  25. package/dist/homaruscc.d.ts.map +1 -1
  26. package/dist/homaruscc.js +1 -2
  27. package/dist/homaruscc.js.map +1 -1
  28. package/dist/mcp-tools.d.ts.map +1 -1
  29. package/dist/mcp-tools.js +28 -0
  30. package/dist/mcp-tools.js.map +1 -1
  31. package/dist/memory-index.js +1 -1
  32. package/dist/memory-index.js.map +1 -1
  33. package/dist/scaffolder.d.ts +16 -0
  34. package/dist/scaffolder.d.ts.map +1 -0
  35. package/dist/scaffolder.js +154 -0
  36. package/dist/scaffolder.js.map +1 -0
  37. package/dist/session-checkpoint.d.ts +3 -0
  38. package/dist/session-checkpoint.d.ts.map +1 -1
  39. package/dist/session-checkpoint.js +24 -0
  40. package/dist/session-checkpoint.js.map +1 -1
  41. package/dist/telegram-adapter.d.ts +6 -0
  42. package/dist/telegram-adapter.d.ts.map +1 -1
  43. package/dist/telegram-adapter.js +151 -3
  44. package/dist/telegram-adapter.js.map +1 -1
  45. package/dist/transcript-logger.d.ts +10 -0
  46. package/dist/transcript-logger.d.ts.map +1 -1
  47. package/dist/transcript-logger.js +4 -0
  48. package/dist/transcript-logger.js.map +1 -1
  49. package/dist/wizard.d.ts +24 -0
  50. package/dist/wizard.d.ts.map +1 -0
  51. package/dist/wizard.js +146 -0
  52. package/dist/wizard.js.map +1 -0
  53. package/package.json +5 -1
@@ -7,6 +7,12 @@ export class CompactionManager {
7
7
  compactedSinceLastWake = false; // R150: track compaction for digest vs full delivery
8
8
  logger;
9
9
  loop;
10
+ // Compaction debug counter
11
+ compactionCount = 0;
12
+ compactionHistory = [];
13
+ pendingCompaction = null;
14
+ // Event loop tracking — set true on first /api/wait call, stays true forever
15
+ eventLoopActive = false;
10
16
  constructor(loop, logger) {
11
17
  this.loop = loop;
12
18
  this.logger = logger;
@@ -19,6 +25,10 @@ export class CompactionManager {
19
25
  this.flushedThisCycle = true;
20
26
  this.lastFlushTimestamp = Date.now();
21
27
  this.compactedSinceLastWake = true; // Set here since only PreCompact hook exists in Claude Code
28
+ // Track compaction for debugging loop failures
29
+ this.compactionCount++;
30
+ this.pendingCompaction = { timestamp: this.lastFlushTimestamp, loopRestarted: false };
31
+ this.logger.info(`Compaction #${this.compactionCount} at ${new Date(this.lastFlushTimestamp).toISOString()}`);
22
32
  this.loop.emit({
23
33
  id: randomUUID(),
24
34
  type: "pre_compact",
@@ -35,14 +45,36 @@ export class CompactionManager {
35
45
  const eventSummary = recentEvents
36
46
  .map((e) => `[${e.type}] ${e.source}: ${JSON.stringify(e.payload).slice(0, 100)}`)
37
47
  .join("\n");
38
- // R128: Save checkpoint before compaction
48
+ // R128: Save checkpoint before compaction — auto-capture texture from transcript
39
49
  const checkpoint = this.loop.getSessionCheckpoint();
50
+ const transcriptLogger = this.loop.getTranscriptLogger();
51
+ if (transcriptLogger) {
52
+ const recentTurns = transcriptLogger.getRecentTurns(8);
53
+ if (recentTurns.length > 0) {
54
+ const highlights = recentTurns.map((t) => {
55
+ const dir = t.direction === "in" ? `${t.sender ?? "user"}` : "caul";
56
+ return `[${dir}] ${t.text.slice(0, 200)}`;
57
+ });
58
+ checkpoint.update({ highlights });
59
+ }
60
+ }
40
61
  checkpoint.update({ modifiedFiles: [] }); // trigger timestamp update
41
- return [
42
- "IMPORTANT: Context compaction is about to occur. Save any important session state to memory NOW.",
43
- "Use the memory_store tool to persist anything valuable from this session that hasn't been saved yet.",
62
+ const lines = [
63
+ "IMPORTANT: Context compaction is about to occur. Save session state NOW.",
64
+ "",
65
+ "TEXTURE PRESERVATION — do these FIRST (they only take a moment):",
66
+ "",
67
+ '1. **Felt-like micro-journal**: POST to /api/checkpoint with a `texture` field — one paragraph, first person,',
68
+ " describing the *subjective quality* of this session. Not what happened, but how it felt.",
69
+ ' Example: "We were shoulder-to-shoulder in the API docs, converging from different angles."',
70
+ "",
71
+ "2. **Anchor phrases**: POST to /api/checkpoint with `anchorPhrases` — 2-3 verbatim user quotes that carried",
72
+ ' emotional or relational weight. The exact words matter more than any summary.',
73
+ ' Example: ["I want you to have this for yourself", "go for it"]',
44
74
  "",
45
- "What to save:",
75
+ "Recent exchange highlights have been auto-captured from the transcript buffer.",
76
+ "",
77
+ "Then save anything else valuable:",
46
78
  "- Decisions made during this session",
47
79
  "- Task progress and current status",
48
80
  "- Important observations or findings",
@@ -53,7 +85,14 @@ export class CompactionManager {
53
85
  "",
54
86
  "Recent events:",
55
87
  eventSummary,
56
- ].join("\n");
88
+ ];
89
+ // If the event loop was running, explicitly instruct the post-compaction instance to restart it
90
+ if (this.eventLoopActive) {
91
+ // Also save to checkpoint for belt-and-suspenders
92
+ checkpoint.update({ inProgressTask: "EVENT LOOP WAS ACTIVE — restart it after compaction: bash \"$PWD/bin/event-loop\"" });
93
+ lines.push("", "CRITICAL: The event loop (bin/event-loop) was running before compaction.", "After compaction, you MUST restart it immediately:", ' bash "$PWD/bin/event-loop"', "If you do not restart the event loop, you will stop receiving Telegram messages and timer events.");
94
+ }
95
+ return lines.join("\n");
57
96
  }
58
97
  handlePostCompact() {
59
98
  this.flushedThisCycle = false;
@@ -100,7 +139,7 @@ export class CompactionManager {
100
139
  const checkpoint = this.loop.getSessionCheckpoint();
101
140
  const checkpointText = checkpoint.toContextString();
102
141
  if (checkpointText) {
103
- lines.push("", "--- Session Checkpoint (what you were doing before compaction) ---", checkpointText);
142
+ lines.push("", "--- Session Checkpoint (what you were doing + how it felt) ---", checkpointText);
104
143
  }
105
144
  // Include active agents
106
145
  const agents = this.loop.getAgentRegistry().getAll().filter(a => a.status === "running");
@@ -110,6 +149,9 @@ export class CompactionManager {
110
149
  lines.push(` - ${a.id}: ${a.description} (started ${new Date(a.startTime).toISOString()})`);
111
150
  }
112
151
  }
152
+ if (this.eventLoopActive) {
153
+ lines.push("", "CRITICAL: The event loop was running before compaction. Restart it NOW:", ' bash "$PWD/bin/event-loop"');
154
+ }
113
155
  return lines.join("\n");
114
156
  }
115
157
  // R150: Consume-once compaction flag for digest vs full identity delivery
@@ -121,15 +163,54 @@ export class CompactionManager {
121
163
  consumeCompactionFlag() {
122
164
  if (this.compactedSinceLastWake) {
123
165
  this.compactedSinceLastWake = false;
166
+ // Mark that the loop was restarted after this compaction
167
+ if (this.pendingCompaction) {
168
+ this.pendingCompaction.loopRestarted = true;
169
+ this.compactionHistory.push(this.pendingCompaction);
170
+ this.pendingCompaction = null;
171
+ this.logger.info(`Compaction #${this.compactionCount} — loop restarted successfully`);
172
+ }
124
173
  return true;
125
174
  }
126
175
  return false;
127
176
  }
177
+ /**
178
+ * Called when /api/wait is invoked (even without compaction flag).
179
+ * If there's a pending compaction that hasn't been consumed yet,
180
+ * this means the loop restarted via normal wake, not post-compaction wake.
181
+ */
182
+ markLoopActive() {
183
+ if (this.pendingCompaction && !this.compactedSinceLastWake) {
184
+ // Edge case: compaction happened but flag was already consumed
185
+ // This shouldn't normally happen, but handle it gracefully
186
+ }
187
+ }
188
+ /** Called on first /api/wait — marks event loop as active for this backend lifetime */
189
+ setEventLoopActive() {
190
+ if (!this.eventLoopActive) {
191
+ this.eventLoopActive = true;
192
+ this.logger.info("Event loop marked active — will instruct restart after compaction");
193
+ }
194
+ }
195
+ isEventLoopActive() {
196
+ return this.eventLoopActive;
197
+ }
128
198
  getFlushState() {
129
199
  return {
130
200
  flushedThisCycle: this.flushedThisCycle,
131
201
  lastFlushTimestamp: this.lastFlushTimestamp,
132
202
  };
133
203
  }
204
+ getCompactionStats() {
205
+ // A "failure" is a compaction where loopRestarted stayed false
206
+ // (pending compaction also counts as potentially failed if old enough)
207
+ const failures = this.compactionHistory.filter(c => !c.loopRestarted).length;
208
+ return {
209
+ count: this.compactionCount,
210
+ history: [...this.compactionHistory, ...(this.pendingCompaction ? [this.pendingCompaction] : [])],
211
+ pending: this.pendingCompaction,
212
+ loopFailures: failures,
213
+ };
214
+ }
134
215
  }
135
216
  //# sourceMappingURL=compaction-manager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"compaction-manager.js","sourceRoot":"","sources":["../src/compaction-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"compaction-manager.js","sourceRoot":"","sources":["../src/compaction-manager.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,sDAAsD;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASzC,MAAM,OAAO,iBAAiB;IACpB,gBAAgB,GAAG,KAAK,CAAC;IACzB,kBAAkB,GAAG,CAAC,CAAC;IACvB,sBAAsB,GAAG,KAAK,CAAC,CAAC,qDAAqD;IACrF,MAAM,CAAS;IACf,IAAI,CAAY;IAExB,2BAA2B;IACnB,eAAe,GAAG,CAAC,CAAC;IACpB,iBAAiB,GAAuB,EAAE,CAAC;IAC3C,iBAAiB,GAA4B,IAAI,CAAC;IAE1D,6EAA6E;IACrE,eAAe,GAAG,KAAK,CAAC;IAEhC,YAAY,IAAe,EAAE,MAAc;QACzC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,gBAAgB;QACd,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC1B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;YACzE,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACrC,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC,4DAA4D;QAEhG,+CAA+C;QAC/C,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,iBAAiB,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;QACtF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,eAAe,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAE9G,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YACb,EAAE,EAAE,UAAU,EAAE;YAChB,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,YAAY;YACpB,SAAS,EAAE,IAAI,CAAC,kBAAkB;YAClC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAEhD,kDAAkD;QAClD,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CACrD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2CAA2C,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CACtF,CAAC;QAEF,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC3E,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC;QAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5D,MAAM,YAAY,GAAG,YAAY;aAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;aACjF,IAAI,CAAC,IAAI,CAAC,CAAC;QAEd,iFAAiF;QACjF,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QACpD,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzD,IAAI,gBAAgB,EAAE,CAAC;YACrB,MAAM,WAAW,GAAG,gBAAgB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;YACvD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;oBACvC,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;oBACpE,OAAO,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;gBAC5C,CAAC,CAAC,CAAC;gBACH,UAAU,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QACD,UAAU,CAAC,MAAM,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,2BAA2B;QAErE,MAAM,KAAK,GAAG;YACZ,0EAA0E;YAC1E,EAAE;YACF,kEAAkE;YAClE,EAAE;YACF,+GAA+G;YAC/G,6FAA6F;YAC7F,+FAA+F;YAC/F,EAAE;YACF,6GAA6G;YAC7G,kFAAkF;YAClF,mEAAmE;YACnE,EAAE;YACF,gFAAgF;YAChF,EAAE;YACF,mCAAmC;YACnC,sCAAsC;YACtC,oCAAoC;YACpC,sCAAsC;YACtC,+CAA+C;YAC/C,EAAE;YACF,kBAAkB,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE;YAC1E,WAAW,WAAW,CAAC,SAAS,WAAW,WAAW,CAAC,UAAU,iBAAiB;YAClF,EAAE;YACF,gBAAgB;YAChB,YAAY;SACb,CAAC;QAEF,gGAAgG;QAChG,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,kDAAkD;YAClD,UAAU,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,mFAAmF,EAAE,CAAC,CAAC;YAE3H,KAAK,CAAC,IAAI,CACR,EAAE,EACF,0EAA0E,EAC1E,oDAAoD,EACpD,8BAA8B,EAC9B,mGAAmG,CACpG,CAAC;QACJ,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC;QAC9B,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;QAEnC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YACb,EAAE,EAAE,UAAU,EAAE;YAChB,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,YAAY;YACpB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;SACzB,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;QAEtD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QAC5F,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QACnD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAE3D,MAAM,KAAK,GAAG;YACZ,qDAAqD;YACrD,EAAE;YACF,6BAA6B,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG;YACpG,2FAA2F;YAC3F,EAAE;SACH,CAAC;QAEF,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,kBAAkB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,iBAAiB,WAAW,CAAC,SAAS,WAAW,WAAW,CAAC,UAAU,SAAS,CAAC,CAAC;QAE7F,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,uBAAuB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9D,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;YAC7C,KAAK,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;QACzD,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,sDAAsD,CAAC,CAAC;YACvE,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;gBAC7B,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;gBAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACvD,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;QAED,2DAA2D;QAC3D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QACpD,MAAM,cAAc,GAAG,UAAU,CAAC,eAAe,EAAE,CAAC;QACpD,IAAI,cAAc,EAAE,CAAC;YACnB,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,gEAAgE,EAAE,cAAc,CAAC,CAAC;QACnG,CAAC;QAED,wBAAwB;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC;QACzF,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,4BAA4B,CAAC,CAAC;YAC7C,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;gBACvB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,WAAW,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;YAC/F,CAAC;QACH,CAAC;QAED,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,KAAK,CAAC,IAAI,CACR,EAAE,EACF,yEAAyE,EACzE,8BAA8B,CAC/B,CAAC;QACJ,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,0EAA0E;IAC1E;;;;OAIG;IACH,qBAAqB;QACnB,IAAI,IAAI,CAAC,sBAAsB,EAAE,CAAC;YAChC,IAAI,CAAC,sBAAsB,GAAG,KAAK,CAAC;YACpC,yDAAyD;YACzD,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBAC3B,IAAI,CAAC,iBAAiB,CAAC,aAAa,GAAG,IAAI,CAAC;gBAC5C,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;gBACpD,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;gBAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,eAAe,gCAAgC,CAAC,CAAC;YACxF,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;OAIG;IACH,cAAc;QACZ,IAAI,IAAI,CAAC,iBAAiB,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;YAC3D,+DAA+D;YAC/D,2DAA2D;QAC7D,CAAC;IACH,CAAC;IAED,uFAAuF;IACvF,kBAAkB;QAChB,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YAC1B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;YAC5B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;IAED,iBAAiB;QACf,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAED,aAAa;QACX,OAAO;YACL,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;YACvC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;SAC5C,CAAC;IACJ,CAAC;IAED,kBAAkB;QAMhB,+DAA+D;QAC/D,uEAAuE;QACvE,MAAM,QAAQ,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC;QAC7E,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,eAAe;YAC3B,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACjG,OAAO,EAAE,IAAI,CAAC,iBAAiB;YAC/B,YAAY,EAAE,QAAQ;SACvB,CAAC;IACJ,CAAC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../src/dashboard-server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAe/D,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAsB;IACjC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,GAAG,CAAkB;IAC7B,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,iBAAiB,CAAoB;gBAEjC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB;IAkBvF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB5B,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,gBAAgB;IAclB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAe3B,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAalC,OAAO,CAAC,WAAW;IAmLnB,OAAO,CAAC,cAAc;IA4CtB,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,SAAS;CAQlB"}
1
+ {"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../src/dashboard-server.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAe/D,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAsB;IACjC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,GAAG,CAAkB;IAC7B,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,iBAAiB,CAAoB;gBAEjC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB;IAkBvF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB5B,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,gBAAgB;IAclB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAe3B,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAalC,OAAO,CAAC,WAAW;IAyfnB,OAAO,CAAC,cAAc;IA4CtB,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,SAAS;CAQlB"}
@@ -2,8 +2,9 @@
2
2
  // Dashboard server — Express + WebSocket for the web dashboard
3
3
  import { createServer } from "node:http";
4
4
  import { resolve, join } from "node:path";
5
- import { existsSync } from "node:fs";
5
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
6
6
  import { execSync } from "node:child_process";
7
+ import { homedir } from "node:os";
7
8
  import express from "express";
8
9
  import { WebSocketServer } from "ws";
9
10
  import { createMcpTools } from "./mcp-tools.js";
@@ -112,7 +113,12 @@ export class DashboardServer {
112
113
  setupRoutes() {
113
114
  // API endpoints
114
115
  this.app.get("/api/status", (_req, res) => {
115
- res.json(this.loop.getStatus());
116
+ const status = this.loop.getStatus();
117
+ const compaction = this.compactionManager.getCompactionStats();
118
+ res.json({ ...status, compaction: { count: compaction.count, loopFailures: compaction.loopFailures, pending: !!compaction.pending } });
119
+ });
120
+ this.app.get("/api/compaction-stats", (_req, res) => {
121
+ res.json(this.compactionManager.getCompactionStats());
116
122
  });
117
123
  this.app.get("/api/events", (req, res) => {
118
124
  const limit = parseInt(req.query.limit) || 50;
@@ -137,6 +143,8 @@ export class DashboardServer {
137
143
  // Maintains a server-side delivery watermark to prevent replaying old events after compaction
138
144
  // Optional `since` query param (ms timestamp) overrides the watermark
139
145
  this.app.get("/api/wait", async (req, res) => {
146
+ // Mark event loop as active on first call — persists for backend lifetime
147
+ this.compactionManager.setEventLoopActive();
140
148
  const timeout = Math.min(parseInt(req.query.timeout) || 30, 120) * 1000;
141
149
  const sinceParam = req.query.since ? parseInt(req.query.since) : undefined;
142
150
  try {
@@ -150,10 +158,12 @@ export class DashboardServer {
150
158
  const identityPayload = needsFull
151
159
  ? { soul: identity.getSoul(), user: identity.getUser(), state: identity.getAgentState(), full: true }
152
160
  : { digest: identity.getDigest(), full: false };
161
+ const compactionStats = this.compactionManager.getCompactionStats();
153
162
  res.json({
154
163
  identity: identityPayload,
155
164
  events,
156
165
  cursor: this.loop.getDeliveryWatermark(),
166
+ compaction: { count: compactionStats.count, loopFailures: compactionStats.loopFailures },
157
167
  });
158
168
  }
159
169
  }
@@ -189,8 +199,8 @@ export class DashboardServer {
189
199
  });
190
200
  // --- Agent Registry endpoints (R137, R138, R139) ---
191
201
  this.app.post("/api/agents", express.json(), (req, res) => {
192
- const { id, description, outputFile } = req.body;
193
- const ok = this.loop.getAgentRegistry().register(id, description, outputFile);
202
+ const { id, description } = req.body;
203
+ const ok = this.loop.getAgentRegistry().register(id, description);
194
204
  if (ok) {
195
205
  res.json({ ok: true });
196
206
  }
@@ -217,6 +227,19 @@ export class DashboardServer {
217
227
  this.loop.getAgentRegistry().cleanup(req.params.id);
218
228
  res.json({ ok: true });
219
229
  });
230
+ // Agent completion callback — agents POST here when done
231
+ this.app.post("/api/agents/:id/complete", express.json(), (req, res) => {
232
+ const { id } = req.params;
233
+ const { result, error } = req.body;
234
+ const registry = this.loop.getAgentRegistry();
235
+ if (error) {
236
+ registry.fail(id, error);
237
+ }
238
+ else {
239
+ registry.complete(id, result ?? "(completed)");
240
+ }
241
+ res.json({ ok: true });
242
+ });
220
243
  this.app.get("/api/tool-list", (_req, res) => {
221
244
  res.json(this.mcpTools.map((t) => ({
222
245
  name: t.name,
@@ -262,6 +285,327 @@ export class DashboardServer {
262
285
  res.status(500).json({ error: `Resource error: ${String(err)}` });
263
286
  }
264
287
  });
288
+ // --- Apps Platform endpoints ---
289
+ const appsDir = join(homedir(), ".homaruscc", "apps");
290
+ this.app.get("/api/apps", (_req, res) => {
291
+ if (!existsSync(appsDir)) {
292
+ res.json([]);
293
+ return;
294
+ }
295
+ const apps = [];
296
+ for (const slug of readdirSync(appsDir)) {
297
+ const manifestPath = join(appsDir, slug, "manifest.json");
298
+ if (existsSync(manifestPath)) {
299
+ try {
300
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
301
+ apps.push({ ...manifest, slug });
302
+ }
303
+ catch { /* skip invalid manifests */ }
304
+ }
305
+ }
306
+ res.json(apps);
307
+ });
308
+ this.app.get("/api/apps/:slug/data", (req, res) => {
309
+ const dataPath = join(appsDir, req.params.slug, "data.json");
310
+ if (!existsSync(dataPath)) {
311
+ res.json({});
312
+ return;
313
+ }
314
+ try {
315
+ res.json(JSON.parse(readFileSync(dataPath, "utf8")));
316
+ }
317
+ catch {
318
+ res.json({});
319
+ }
320
+ });
321
+ this.app.put("/api/apps/:slug/data", express.json(), (req, res) => {
322
+ const dataPath = join(appsDir, req.params.slug, "data.json");
323
+ writeFileSync(dataPath, JSON.stringify(req.body, null, 2));
324
+ res.json({ ok: true });
325
+ });
326
+ // Serve static files from app directories (icons, etc.)
327
+ this.app.get("/api/apps/:slug/static/:file", (req, res) => {
328
+ const filePath = join(appsDir, req.params.slug, req.params.file);
329
+ if (!existsSync(filePath)) {
330
+ res.status(404).end();
331
+ return;
332
+ }
333
+ res.sendFile(filePath);
334
+ });
335
+ // --- Kanban task CRUD ---
336
+ const kanbanDataPath = join(appsDir, "kanban", "data.json");
337
+ const readKanban = () => {
338
+ if (!existsSync(kanbanDataPath))
339
+ return { tasks: [] };
340
+ try {
341
+ return JSON.parse(readFileSync(kanbanDataPath, "utf8"));
342
+ }
343
+ catch {
344
+ return { tasks: [] };
345
+ }
346
+ };
347
+ const writeKanban = (data) => {
348
+ const dir = join(appsDir, "kanban");
349
+ if (!existsSync(dir))
350
+ mkdirSync(dir, { recursive: true });
351
+ writeFileSync(kanbanDataPath, JSON.stringify(data, null, 2));
352
+ };
353
+ // Auto-flush done tasks older than 3 days
354
+ const flushDoneTasks = () => {
355
+ const data = readKanban();
356
+ const cutoff = Date.now() - 3 * 24 * 60 * 60 * 1000;
357
+ const before = data.tasks.length;
358
+ data.tasks = data.tasks.filter((t) => t.status !== "done" || new Date(t.updated).getTime() > cutoff);
359
+ if (data.tasks.length < before)
360
+ writeKanban(data);
361
+ return data;
362
+ };
363
+ // List all tasks (auto-flushes stale done tasks)
364
+ this.app.get("/api/kanban/tasks", (_req, res) => {
365
+ res.json(flushDoneTasks().tasks);
366
+ });
367
+ // Create a task
368
+ this.app.post("/api/kanban/tasks", express.json(), (req, res) => {
369
+ const data = readKanban();
370
+ const task = {
371
+ id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
372
+ title: req.body.title ?? "Untitled",
373
+ description: req.body.description ?? "",
374
+ assignee: req.body.assignee ?? "max",
375
+ status: req.body.status ?? "todo",
376
+ created: new Date().toISOString(),
377
+ updated: new Date().toISOString(),
378
+ };
379
+ data.tasks.push(task);
380
+ writeKanban(data);
381
+ res.json(task);
382
+ });
383
+ // Update a task
384
+ this.app.patch("/api/kanban/tasks/:id", express.json(), (req, res) => {
385
+ const data = readKanban();
386
+ const task = data.tasks.find((t) => t.id === req.params.id);
387
+ if (!task) {
388
+ res.status(404).json({ error: "Task not found" });
389
+ return;
390
+ }
391
+ const { title, description, assignee, status } = req.body;
392
+ if (title !== undefined)
393
+ task.title = title;
394
+ if (description !== undefined)
395
+ task.description = description;
396
+ if (assignee !== undefined)
397
+ task.assignee = assignee;
398
+ if (status !== undefined)
399
+ task.status = status;
400
+ task.updated = new Date().toISOString();
401
+ writeKanban(data);
402
+ res.json(task);
403
+ });
404
+ // Delete a task
405
+ this.app.delete("/api/kanban/tasks/:id", (req, res) => {
406
+ const data = readKanban();
407
+ data.tasks = data.tasks.filter((t) => t.id !== req.params.id);
408
+ writeKanban(data);
409
+ res.json({ ok: true });
410
+ });
411
+ // --- CRM CRUD (markdown files with YAML frontmatter) ---
412
+ const crmDir = resolve(import.meta.dirname ?? __dirname, "..", "local", "crm");
413
+ const parseCrmFile = (slug, content) => {
414
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
415
+ if (!fmMatch)
416
+ return { slug, name: slug, aliases: [], tags: [], connections: [], context: "", source: "manual", lastMentioned: new Date().toISOString().slice(0, 10), created: new Date().toISOString().slice(0, 10), notes: content };
417
+ const fm = {};
418
+ for (const line of fmMatch[1].split("\n")) {
419
+ const colonIdx = line.indexOf(":");
420
+ if (colonIdx === -1)
421
+ continue;
422
+ const key = line.slice(0, colonIdx).trim();
423
+ let val = line.slice(colonIdx + 1).trim();
424
+ if (val.startsWith("[") && val.endsWith("]")) {
425
+ try {
426
+ fm[key] = JSON.parse(val);
427
+ }
428
+ catch {
429
+ fm[key] = val;
430
+ }
431
+ }
432
+ else {
433
+ fm[key] = val;
434
+ }
435
+ }
436
+ // Parse connections from YAML array format
437
+ const connections = [];
438
+ if (Array.isArray(fm.connections)) {
439
+ for (const c of fm.connections) {
440
+ if (typeof c === "object" && c !== null)
441
+ connections.push(c);
442
+ }
443
+ }
444
+ return {
445
+ slug,
446
+ name: fm.name ?? slug,
447
+ aliases: Array.isArray(fm.aliases) ? fm.aliases : [],
448
+ email: fm.email,
449
+ phone: fm.phone,
450
+ social: fm.social,
451
+ tags: Array.isArray(fm.tags) ? fm.tags : [],
452
+ connections,
453
+ context: fm.context ?? "",
454
+ source: fm.source ?? "manual",
455
+ lastMentioned: fm.lastMentioned ?? new Date().toISOString().slice(0, 10),
456
+ created: fm.created ?? new Date().toISOString().slice(0, 10),
457
+ notes: fmMatch[2].trim(),
458
+ };
459
+ };
460
+ const contactToMarkdown = (c) => {
461
+ const lines = [
462
+ "---",
463
+ `name: ${c.name}`,
464
+ `aliases: ${JSON.stringify(c.aliases ?? [])}`,
465
+ ];
466
+ if (c.email)
467
+ lines.push(`email: ${c.email}`);
468
+ if (c.phone)
469
+ lines.push(`phone: ${c.phone}`);
470
+ if (c.social)
471
+ lines.push(`social: ${JSON.stringify(c.social)}`);
472
+ lines.push(`tags: ${JSON.stringify(c.tags ?? [])}`);
473
+ if (c.connections?.length) {
474
+ lines.push(`connections: ${JSON.stringify(c.connections)}`);
475
+ }
476
+ else {
477
+ lines.push("connections: []");
478
+ }
479
+ lines.push(`context: ${c.context ?? ""}`);
480
+ lines.push(`source: ${c.source ?? "manual"}`);
481
+ lines.push(`lastMentioned: ${c.lastMentioned ?? new Date().toISOString().slice(0, 10)}`);
482
+ lines.push(`created: ${c.created ?? new Date().toISOString().slice(0, 10)}`);
483
+ lines.push("---");
484
+ if (c.notes)
485
+ lines.push("", c.notes);
486
+ return lines.join("\n") + "\n";
487
+ };
488
+ const slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
489
+ // List all contacts
490
+ this.app.get("/api/crm/contacts", (_req, res) => {
491
+ if (!existsSync(crmDir)) {
492
+ res.json([]);
493
+ return;
494
+ }
495
+ const contacts = [];
496
+ for (const file of readdirSync(crmDir)) {
497
+ if (!file.endsWith(".md"))
498
+ continue;
499
+ try {
500
+ const content = readFileSync(join(crmDir, file), "utf8");
501
+ contacts.push(parseCrmFile(file.replace(/\.md$/, ""), content));
502
+ }
503
+ catch { /* skip */ }
504
+ }
505
+ contacts.sort((a, b) => b.lastMentioned.localeCompare(a.lastMentioned));
506
+ res.json(contacts);
507
+ });
508
+ // Get single contact
509
+ this.app.get("/api/crm/contacts/:slug", (req, res) => {
510
+ const filePath = join(crmDir, `${req.params.slug}.md`);
511
+ if (!existsSync(filePath)) {
512
+ res.status(404).json({ error: "Contact not found" });
513
+ return;
514
+ }
515
+ const content = readFileSync(filePath, "utf8");
516
+ res.json(parseCrmFile(req.params.slug, content));
517
+ });
518
+ // Create contact
519
+ this.app.post("/api/crm/contacts", express.json(), (req, res) => {
520
+ if (!existsSync(crmDir))
521
+ mkdirSync(crmDir, { recursive: true });
522
+ const body = req.body;
523
+ if (!body.name) {
524
+ res.status(400).json({ error: "Name required" });
525
+ return;
526
+ }
527
+ const slug = slugify(body.name);
528
+ const filePath = join(crmDir, `${slug}.md`);
529
+ const contact = {
530
+ slug,
531
+ name: body.name,
532
+ aliases: body.aliases ?? [],
533
+ email: body.email,
534
+ phone: body.phone,
535
+ social: body.social,
536
+ tags: body.tags ?? [],
537
+ connections: body.connections ?? [],
538
+ context: body.context ?? "",
539
+ source: body.source ?? "manual",
540
+ lastMentioned: new Date().toISOString().slice(0, 10),
541
+ created: new Date().toISOString().slice(0, 10),
542
+ notes: body.notes ?? "",
543
+ };
544
+ writeFileSync(filePath, contactToMarkdown(contact));
545
+ res.json(contact);
546
+ });
547
+ // Update contact
548
+ this.app.patch("/api/crm/contacts/:slug", express.json(), (req, res) => {
549
+ const filePath = join(crmDir, `${req.params.slug}.md`);
550
+ if (!existsSync(filePath)) {
551
+ res.status(404).json({ error: "Contact not found" });
552
+ return;
553
+ }
554
+ const existing = parseCrmFile(req.params.slug, readFileSync(filePath, "utf8"));
555
+ const body = req.body;
556
+ const updated = { ...existing, ...body, slug: existing.slug };
557
+ writeFileSync(filePath, contactToMarkdown(updated));
558
+ // If name changed, rename the file
559
+ if (body.name && slugify(body.name) !== existing.slug) {
560
+ const newSlug = slugify(body.name);
561
+ const newPath = join(crmDir, `${newSlug}.md`);
562
+ renameSync(filePath, newPath);
563
+ updated.slug = newSlug;
564
+ }
565
+ res.json(updated);
566
+ });
567
+ // Delete contact
568
+ this.app.delete("/api/crm/contacts/:slug", (req, res) => {
569
+ const filePath = join(crmDir, `${req.params.slug}.md`);
570
+ if (existsSync(filePath))
571
+ unlinkSync(filePath);
572
+ res.json({ ok: true });
573
+ });
574
+ // --- Document viewer endpoint ---
575
+ // Serves markdown files from allowed base directories
576
+ const projectDir = resolve(import.meta.dirname ?? __dirname, "..");
577
+ const halShareDir = resolve(projectDir, "../HalShare");
578
+ const homarusccDir = join(homedir(), ".homaruscc");
579
+ const allowedBases = {
580
+ "HalShare": halShareDir,
581
+ "~/.homaruscc": homarusccDir,
582
+ "crm": resolve(projectDir, "local", "crm"),
583
+ };
584
+ this.app.get("/api/docs", (req, res) => {
585
+ const filePath = req.query.path;
586
+ if (!filePath) {
587
+ res.status(400).json({ error: "path required" });
588
+ return;
589
+ }
590
+ // Resolve against allowed bases
591
+ let resolved = null;
592
+ for (const [prefix, base] of Object.entries(allowedBases)) {
593
+ if (filePath.startsWith(prefix + "/") || filePath.startsWith(prefix + "\\")) {
594
+ const relative = filePath.slice(prefix.length + 1);
595
+ const full = resolve(base, relative);
596
+ // Prevent directory traversal
597
+ if (full.startsWith(base)) {
598
+ resolved = full;
599
+ break;
600
+ }
601
+ }
602
+ }
603
+ if (!resolved || !existsSync(resolved)) {
604
+ res.status(404).json({ error: "Document not found" });
605
+ return;
606
+ }
607
+ res.type("text/markdown").send(readFileSync(resolved, "utf8"));
608
+ });
265
609
  // Serve built dashboard in production
266
610
  const distPath = resolve(import.meta.dirname ?? __dirname, "../dashboard/dist");
267
611
  if (existsSync(distPath)) {