homaruscc 0.2.0 → 0.3.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.
Binary file
Binary file
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/png" href="/favicon.png" />
7
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
8
+ <link rel="apple-touch-icon" href="/favicon.png" />
9
+ <title>HomarUScc Dashboard</title>
10
+ <script type="module" crossorigin src="/assets/index-Xu4GUpcF.js"></script>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ </body>
15
+ </html>
@@ -1,11 +1,10 @@
1
1
  import type { Event, Logger } from "./types.js";
2
- export type AgentStatus = "running" | "completed" | "failed";
2
+ export type AgentStatus = "running" | "completed" | "failed" | "timeout";
3
3
  export interface AgentEntry {
4
4
  id: string;
5
5
  description: string;
6
6
  status: AgentStatus;
7
7
  startTime: number;
8
- outputFile?: string;
9
8
  result?: string;
10
9
  error?: string;
11
10
  }
@@ -14,11 +13,11 @@ export declare class AgentRegistry {
14
13
  private maxConcurrent;
15
14
  private emitFn;
16
15
  private logger;
17
- private pollIntervalMs;
18
- private pollTimer;
19
- constructor(logger: Logger, maxConcurrent?: number, pollIntervalMs?: number);
16
+ private timeoutMs;
17
+ private timeoutTimer;
18
+ constructor(logger: Logger, maxConcurrent?: number, timeoutMs?: number);
20
19
  setEmitter(fn: (event: Event) => void): void;
21
- register(id: string, description: string, outputFile?: string): boolean;
20
+ register(id: string, description: string): boolean;
22
21
  getAll(): AgentEntry[];
23
22
  get(id: string): AgentEntry | null;
24
23
  complete(id: string, result: string): void;
@@ -26,12 +25,10 @@ export declare class AgentRegistry {
26
25
  cleanup(id: string): void;
27
26
  getAvailableSlots(): number;
28
27
  getActiveCount(): number;
29
- startPolling(): void;
30
- stopPolling(): void;
31
- pollAgents(): void;
32
- private checkAgentFile;
33
- private readTail;
34
- private extractSummary;
28
+ private startTimeoutChecker;
29
+ private stopTimeoutChecker;
30
+ stop(): void;
31
+ private checkTimeouts;
35
32
  private resolve;
36
33
  private emit;
37
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"agent-registry.d.ts","sourceRoot":"","sources":["../src/agent-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE7D,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAWD,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,SAAS,CAA+C;gBAEpD,MAAM,EAAE,MAAM,EAAE,aAAa,SAAI,EAAE,cAAc,SAAO;IAMpE,UAAU,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAI5C,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO;IAmBvE,MAAM,IAAI,UAAU,EAAE;IAItB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAKlC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAY1C,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAYrC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIzB,iBAAiB,IAAI,MAAM;IAI3B,cAAc,IAAI,MAAM;IASxB,YAAY,IAAI,IAAI;IAWpB,WAAW,IAAI,IAAI;IASnB,UAAU,IAAI,IAAI;IAiBlB,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,QAAQ;IAoBhB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,IAAI;CAcb"}
1
+ {"version":3,"file":"agent-registry.d.ts","sourceRoot":"","sources":["../src/agent-registry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAA+C;gBAEvD,MAAM,EAAE,MAAM,EAAE,aAAa,SAAI,EAAE,SAAS,SAAqB;IAM7E,UAAU,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAI5C,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO;IAwBlD,MAAM,IAAI,UAAU,EAAE;IAItB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAKlC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAY1C,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAYrC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAQzB,iBAAiB,IAAI,MAAM;IAI3B,cAAc,IAAI,MAAM;IASxB,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,kBAAkB;IAO1B,IAAI,IAAI,IAAI;IAIZ,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,IAAI;CAcb"}
@@ -1,30 +1,25 @@
1
- // CRC: crc-AgentRegistry.md | Seq: seq-agent-dispatch.md, seq-agent-poll.md
1
+ // CRC: crc-AgentRegistry.md | Seq: seq-agent-dispatch.md
2
2
  import { randomUUID } from "node:crypto";
3
- import { statSync, openSync, readSync, closeSync } from "node:fs";
4
- // R156: Completion markers found in Task agent JSONL output
5
- const COMPLETION_MARKERS = ['"stop_reason":"end_turn"', '"type":"result"'];
6
- // R156: Stable mtime threshold (ms) — file unchanged for this long is considered complete
7
- const STABLE_THRESHOLD_MS = 10_000;
8
- // R156: Number of bytes to read from file tail for marker detection
9
- const TAIL_BYTES = 512;
3
+ // Default timeout: 30 minutes
4
+ const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
5
+ // How often to check for timed-out agents
6
+ const TIMEOUT_CHECK_INTERVAL_MS = 60_000;
10
7
  export class AgentRegistry {
11
8
  agents = new Map();
12
9
  maxConcurrent;
13
10
  emitFn = null;
14
11
  logger;
15
- // R155: Configurable poll interval
16
- pollIntervalMs;
17
- // R159: Global poll timer handle
18
- pollTimer = null;
19
- constructor(logger, maxConcurrent = 3, pollIntervalMs = 5000) {
12
+ timeoutMs;
13
+ timeoutTimer = null;
14
+ constructor(logger, maxConcurrent = 3, timeoutMs = DEFAULT_TIMEOUT_MS) {
20
15
  this.logger = logger;
21
16
  this.maxConcurrent = maxConcurrent;
22
- this.pollIntervalMs = pollIntervalMs;
17
+ this.timeoutMs = timeoutMs;
23
18
  }
24
19
  setEmitter(fn) {
25
20
  this.emitFn = fn;
26
21
  }
27
- register(id, description, outputFile) {
22
+ register(id, description) {
28
23
  const active = this.getActiveCount();
29
24
  if (active >= this.maxConcurrent) {
30
25
  this.logger.warn("Agent registry at capacity", { active, max: this.maxConcurrent });
@@ -35,9 +30,12 @@ export class AgentRegistry {
35
30
  description,
36
31
  status: "running",
37
32
  startTime: Date.now(),
38
- outputFile,
39
33
  });
40
34
  this.logger.info("Agent registered", { id, description });
35
+ // Start timeout checker if not already running
36
+ if (!this.timeoutTimer) {
37
+ this.startTimeoutChecker();
38
+ }
41
39
  return true;
42
40
  }
43
41
  getAll() {
@@ -46,7 +44,7 @@ export class AgentRegistry {
46
44
  get(id) {
47
45
  return this.agents.get(id) ?? null;
48
46
  }
49
- // R157, R162: Complete only if still running (prevents duplicate events)
47
+ // Called via POST /api/agents/:id/complete callback from the agent itself
50
48
  complete(id, result) {
51
49
  const agent = this.resolve(id);
52
50
  if (!agent)
@@ -62,14 +60,19 @@ export class AgentRegistry {
62
60
  const agent = this.resolve(id);
63
61
  if (!agent)
64
62
  return;
63
+ if (agent.status !== "running")
64
+ return;
65
65
  agent.status = "failed";
66
66
  agent.error = error;
67
67
  this.emit("agent_failed", id, agent.description, { error });
68
68
  this.logger.warn("Agent failed", { id, error });
69
69
  }
70
- // R160: Cleanup removes agent and any associated polling state
71
70
  cleanup(id) {
72
71
  this.agents.delete(id);
72
+ // Stop timeout checker if no agents remain
73
+ if (this.getActiveCount() === 0) {
74
+ this.stopTimeoutChecker();
75
+ }
73
76
  }
74
77
  getAvailableSlots() {
75
78
  return Math.max(0, this.maxConcurrent - this.getActiveCount());
@@ -82,110 +85,38 @@ export class AgentRegistry {
82
85
  }
83
86
  return count;
84
87
  }
85
- // R159: Start global polling interval
86
- startPolling() {
87
- if (this.pollTimer)
88
+ // Periodic check for agents that have exceeded the timeout
89
+ startTimeoutChecker() {
90
+ if (this.timeoutTimer)
88
91
  return;
89
- this.pollTimer = setInterval(() => this.pollAgents(), this.pollIntervalMs);
90
- // Unref so the timer does not keep the process alive during shutdown
91
- if (this.pollTimer && typeof this.pollTimer === "object" && "unref" in this.pollTimer) {
92
- this.pollTimer.unref();
93
- }
94
- this.logger.info("Agent completion polling started", { intervalMs: this.pollIntervalMs });
95
- }
96
- // R159: Stop global polling interval
97
- stopPolling() {
98
- if (this.pollTimer) {
99
- clearInterval(this.pollTimer);
100
- this.pollTimer = null;
101
- this.logger.info("Agent completion polling stopped");
92
+ this.timeoutTimer = setInterval(() => this.checkTimeouts(), TIMEOUT_CHECK_INTERVAL_MS);
93
+ if (this.timeoutTimer && typeof this.timeoutTimer === "object" && "unref" in this.timeoutTimer) {
94
+ this.timeoutTimer.unref();
102
95
  }
103
96
  }
104
- // R154, R158: Poll all running agents with outputFiles for completion
105
- pollAgents() {
106
- for (const agent of this.agents.values()) {
107
- // R158: Only poll running agents with an outputFile
108
- if (agent.status !== "running" || !agent.outputFile)
109
- continue;
110
- try {
111
- this.checkAgentFile(agent);
112
- }
113
- catch (err) {
114
- // R161: Log and skip on errors
115
- this.logger.debug("Poll check error for agent", {
116
- id: agent.id,
117
- error: String(err),
118
- });
119
- }
97
+ stopTimeoutChecker() {
98
+ if (this.timeoutTimer) {
99
+ clearInterval(this.timeoutTimer);
100
+ this.timeoutTimer = null;
120
101
  }
121
102
  }
122
- checkAgentFile(agent) {
123
- // R161: statSync may throw ENOENT if file does not exist yet
124
- let stat;
125
- try {
126
- stat = statSync(agent.outputFile);
127
- }
128
- catch {
129
- return; // File does not exist yet — skip
130
- }
131
- // Skip empty files
132
- if (stat.size === 0)
133
- return;
134
- // R156: Read the tail of the file
135
- const tail = this.readTail(agent.outputFile, stat.size);
136
- if (!tail)
137
- return;
138
- // R156: Check for completion markers in the tail
139
- const hasMarker = COMPLETION_MARKERS.some((m) => tail.includes(m));
140
- if (hasMarker) {
141
- this.logger.info("Detected completion marker in agent output", { id: agent.id });
142
- this.complete(agent.id, this.extractSummary(tail));
143
- return;
144
- }
145
- // R156: Check for stable mtime (no writes in STABLE_THRESHOLD_MS)
146
- const age = Date.now() - stat.mtimeMs;
147
- if (age >= STABLE_THRESHOLD_MS) {
148
- this.logger.info("Detected stable output file for agent", {
149
- id: agent.id,
150
- stableForMs: Math.round(age),
151
- });
152
- this.complete(agent.id, this.extractSummary(tail));
153
- }
103
+ stop() {
104
+ this.stopTimeoutChecker();
154
105
  }
155
- // Read the last `count` bytes of a file as a UTF-8 string
156
- readTail(filePath, fileSize) {
157
- const readSize = Math.min(TAIL_BYTES, fileSize);
158
- const offset = fileSize - readSize;
159
- const buf = Buffer.alloc(readSize);
160
- let fd = null;
161
- try {
162
- fd = openSync(filePath, "r");
163
- readSync(fd, buf, 0, readSize, offset);
164
- return buf.toString("utf-8");
165
- }
166
- catch {
167
- return null;
168
- }
169
- finally {
170
- if (fd !== null) {
171
- try {
172
- closeSync(fd);
173
- }
174
- catch { /* ignore */ }
106
+ checkTimeouts() {
107
+ const now = Date.now();
108
+ for (const agent of this.agents.values()) {
109
+ if (agent.status !== "running")
110
+ continue;
111
+ const elapsed = now - agent.startTime;
112
+ if (elapsed >= this.timeoutMs) {
113
+ agent.status = "timeout";
114
+ agent.error = `Agent timed out after ${Math.round(elapsed / 60_000)}m`;
115
+ this.emit("agent_timeout", agent.id, agent.description, { error: agent.error });
116
+ this.logger.warn("Agent timed out", { id: agent.id, elapsedMs: elapsed });
175
117
  }
176
118
  }
177
119
  }
178
- // Extract a brief summary from the file tail for the completion result
179
- extractSummary(tail) {
180
- // Try to find the last JSON line that looks like a result
181
- const lines = tail.split("\n").filter((l) => l.trim().length > 0);
182
- const lastLine = lines[lines.length - 1] ?? "";
183
- // Truncate to a reasonable length for the event payload
184
- if (lastLine.length > 200) {
185
- return lastLine.slice(0, 200) + "...";
186
- }
187
- return lastLine || "(output file completed)";
188
- }
189
120
  resolve(id) {
190
121
  const agent = this.agents.get(id);
191
122
  if (!agent) {
@@ -1 +1 @@
1
- {"version":3,"file":"agent-registry.js","sourceRoot":"","sources":["../src/agent-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-registry.js","sourceRoot":"","sources":["../src/agent-registry.ts"],"names":[],"mappings":"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"}
@@ -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;IAicnB,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";
@@ -189,8 +190,8 @@ export class DashboardServer {
189
190
  });
190
191
  // --- Agent Registry endpoints (R137, R138, R139) ---
191
192
  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);
193
+ const { id, description } = req.body;
194
+ const ok = this.loop.getAgentRegistry().register(id, description);
194
195
  if (ok) {
195
196
  res.json({ ok: true });
196
197
  }
@@ -217,6 +218,19 @@ export class DashboardServer {
217
218
  this.loop.getAgentRegistry().cleanup(req.params.id);
218
219
  res.json({ ok: true });
219
220
  });
221
+ // Agent completion callback — agents POST here when done
222
+ this.app.post("/api/agents/:id/complete", express.json(), (req, res) => {
223
+ const { id } = req.params;
224
+ const { result, error } = req.body;
225
+ const registry = this.loop.getAgentRegistry();
226
+ if (error) {
227
+ registry.fail(id, error);
228
+ }
229
+ else {
230
+ registry.complete(id, result ?? "(completed)");
231
+ }
232
+ res.json({ ok: true });
233
+ });
220
234
  this.app.get("/api/tool-list", (_req, res) => {
221
235
  res.json(this.mcpTools.map((t) => ({
222
236
  name: t.name,
@@ -262,6 +276,282 @@ export class DashboardServer {
262
276
  res.status(500).json({ error: `Resource error: ${String(err)}` });
263
277
  }
264
278
  });
279
+ // --- Apps Platform endpoints ---
280
+ const appsDir = join(homedir(), ".homaruscc", "apps");
281
+ this.app.get("/api/apps", (_req, res) => {
282
+ if (!existsSync(appsDir)) {
283
+ res.json([]);
284
+ return;
285
+ }
286
+ const apps = [];
287
+ for (const slug of readdirSync(appsDir)) {
288
+ const manifestPath = join(appsDir, slug, "manifest.json");
289
+ if (existsSync(manifestPath)) {
290
+ try {
291
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
292
+ apps.push({ ...manifest, slug });
293
+ }
294
+ catch { /* skip invalid manifests */ }
295
+ }
296
+ }
297
+ res.json(apps);
298
+ });
299
+ this.app.get("/api/apps/:slug/data", (req, res) => {
300
+ const dataPath = join(appsDir, req.params.slug, "data.json");
301
+ if (!existsSync(dataPath)) {
302
+ res.json({});
303
+ return;
304
+ }
305
+ try {
306
+ res.json(JSON.parse(readFileSync(dataPath, "utf8")));
307
+ }
308
+ catch {
309
+ res.json({});
310
+ }
311
+ });
312
+ this.app.put("/api/apps/:slug/data", express.json(), (req, res) => {
313
+ const dataPath = join(appsDir, req.params.slug, "data.json");
314
+ writeFileSync(dataPath, JSON.stringify(req.body, null, 2));
315
+ res.json({ ok: true });
316
+ });
317
+ // Serve static files from app directories (icons, etc.)
318
+ this.app.get("/api/apps/:slug/static/:file", (req, res) => {
319
+ const filePath = join(appsDir, req.params.slug, req.params.file);
320
+ if (!existsSync(filePath)) {
321
+ res.status(404).end();
322
+ return;
323
+ }
324
+ res.sendFile(filePath);
325
+ });
326
+ // --- Kanban task CRUD ---
327
+ const kanbanDataPath = join(appsDir, "kanban", "data.json");
328
+ const readKanban = () => {
329
+ if (!existsSync(kanbanDataPath))
330
+ return { tasks: [] };
331
+ try {
332
+ return JSON.parse(readFileSync(kanbanDataPath, "utf8"));
333
+ }
334
+ catch {
335
+ return { tasks: [] };
336
+ }
337
+ };
338
+ const writeKanban = (data) => {
339
+ const dir = join(appsDir, "kanban");
340
+ if (!existsSync(dir))
341
+ mkdirSync(dir, { recursive: true });
342
+ writeFileSync(kanbanDataPath, JSON.stringify(data, null, 2));
343
+ };
344
+ // List all tasks
345
+ this.app.get("/api/kanban/tasks", (_req, res) => {
346
+ res.json(readKanban().tasks);
347
+ });
348
+ // Create a task
349
+ this.app.post("/api/kanban/tasks", express.json(), (req, res) => {
350
+ const data = readKanban();
351
+ const task = {
352
+ id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
353
+ title: req.body.title ?? "Untitled",
354
+ description: req.body.description ?? "",
355
+ assignee: req.body.assignee ?? "max",
356
+ status: req.body.status ?? "todo",
357
+ created: new Date().toISOString(),
358
+ updated: new Date().toISOString(),
359
+ };
360
+ data.tasks.push(task);
361
+ writeKanban(data);
362
+ res.json(task);
363
+ });
364
+ // Update a task
365
+ this.app.patch("/api/kanban/tasks/:id", express.json(), (req, res) => {
366
+ const data = readKanban();
367
+ const task = data.tasks.find((t) => t.id === req.params.id);
368
+ if (!task) {
369
+ res.status(404).json({ error: "Task not found" });
370
+ return;
371
+ }
372
+ const { title, description, assignee, status } = req.body;
373
+ if (title !== undefined)
374
+ task.title = title;
375
+ if (description !== undefined)
376
+ task.description = description;
377
+ if (assignee !== undefined)
378
+ task.assignee = assignee;
379
+ if (status !== undefined)
380
+ task.status = status;
381
+ task.updated = new Date().toISOString();
382
+ writeKanban(data);
383
+ res.json(task);
384
+ });
385
+ // Delete a task
386
+ this.app.delete("/api/kanban/tasks/:id", (req, res) => {
387
+ const data = readKanban();
388
+ data.tasks = data.tasks.filter((t) => t.id !== req.params.id);
389
+ writeKanban(data);
390
+ res.json({ ok: true });
391
+ });
392
+ // --- CRM CRUD (markdown files with YAML frontmatter) ---
393
+ const crmDir = join(homedir(), ".homaruscc", "crm");
394
+ const parseCrmFile = (slug, content) => {
395
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
396
+ if (!fmMatch)
397
+ 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 };
398
+ const fm = {};
399
+ for (const line of fmMatch[1].split("\n")) {
400
+ const colonIdx = line.indexOf(":");
401
+ if (colonIdx === -1)
402
+ continue;
403
+ const key = line.slice(0, colonIdx).trim();
404
+ let val = line.slice(colonIdx + 1).trim();
405
+ if (val.startsWith("[") && val.endsWith("]")) {
406
+ try {
407
+ fm[key] = JSON.parse(val);
408
+ }
409
+ catch {
410
+ fm[key] = val;
411
+ }
412
+ }
413
+ else {
414
+ fm[key] = val;
415
+ }
416
+ }
417
+ // Parse connections from YAML array format
418
+ const connections = [];
419
+ if (Array.isArray(fm.connections)) {
420
+ for (const c of fm.connections) {
421
+ if (typeof c === "object" && c !== null)
422
+ connections.push(c);
423
+ }
424
+ }
425
+ return {
426
+ slug,
427
+ name: fm.name ?? slug,
428
+ aliases: Array.isArray(fm.aliases) ? fm.aliases : [],
429
+ email: fm.email,
430
+ phone: fm.phone,
431
+ social: fm.social,
432
+ tags: Array.isArray(fm.tags) ? fm.tags : [],
433
+ connections,
434
+ context: fm.context ?? "",
435
+ source: fm.source ?? "manual",
436
+ lastMentioned: fm.lastMentioned ?? new Date().toISOString().slice(0, 10),
437
+ created: fm.created ?? new Date().toISOString().slice(0, 10),
438
+ notes: fmMatch[2].trim(),
439
+ };
440
+ };
441
+ const contactToMarkdown = (c) => {
442
+ const lines = [
443
+ "---",
444
+ `name: ${c.name}`,
445
+ `aliases: ${JSON.stringify(c.aliases ?? [])}`,
446
+ ];
447
+ if (c.email)
448
+ lines.push(`email: ${c.email}`);
449
+ if (c.phone)
450
+ lines.push(`phone: ${c.phone}`);
451
+ if (c.social)
452
+ lines.push(`social: ${JSON.stringify(c.social)}`);
453
+ lines.push(`tags: ${JSON.stringify(c.tags ?? [])}`);
454
+ if (c.connections?.length) {
455
+ lines.push(`connections: ${JSON.stringify(c.connections)}`);
456
+ }
457
+ else {
458
+ lines.push("connections: []");
459
+ }
460
+ lines.push(`context: ${c.context ?? ""}`);
461
+ lines.push(`source: ${c.source ?? "manual"}`);
462
+ lines.push(`lastMentioned: ${c.lastMentioned ?? new Date().toISOString().slice(0, 10)}`);
463
+ lines.push(`created: ${c.created ?? new Date().toISOString().slice(0, 10)}`);
464
+ lines.push("---");
465
+ if (c.notes)
466
+ lines.push("", c.notes);
467
+ return lines.join("\n") + "\n";
468
+ };
469
+ const slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
470
+ // List all contacts
471
+ this.app.get("/api/crm/contacts", (_req, res) => {
472
+ if (!existsSync(crmDir)) {
473
+ res.json([]);
474
+ return;
475
+ }
476
+ const contacts = [];
477
+ for (const file of readdirSync(crmDir)) {
478
+ if (!file.endsWith(".md"))
479
+ continue;
480
+ try {
481
+ const content = readFileSync(join(crmDir, file), "utf8");
482
+ contacts.push(parseCrmFile(file.replace(/\.md$/, ""), content));
483
+ }
484
+ catch { /* skip */ }
485
+ }
486
+ contacts.sort((a, b) => b.lastMentioned.localeCompare(a.lastMentioned));
487
+ res.json(contacts);
488
+ });
489
+ // Get single contact
490
+ this.app.get("/api/crm/contacts/:slug", (req, res) => {
491
+ const filePath = join(crmDir, `${req.params.slug}.md`);
492
+ if (!existsSync(filePath)) {
493
+ res.status(404).json({ error: "Contact not found" });
494
+ return;
495
+ }
496
+ const content = readFileSync(filePath, "utf8");
497
+ res.json(parseCrmFile(req.params.slug, content));
498
+ });
499
+ // Create contact
500
+ this.app.post("/api/crm/contacts", express.json(), (req, res) => {
501
+ if (!existsSync(crmDir))
502
+ mkdirSync(crmDir, { recursive: true });
503
+ const body = req.body;
504
+ if (!body.name) {
505
+ res.status(400).json({ error: "Name required" });
506
+ return;
507
+ }
508
+ const slug = slugify(body.name);
509
+ const filePath = join(crmDir, `${slug}.md`);
510
+ const contact = {
511
+ slug,
512
+ name: body.name,
513
+ aliases: body.aliases ?? [],
514
+ email: body.email,
515
+ phone: body.phone,
516
+ social: body.social,
517
+ tags: body.tags ?? [],
518
+ connections: body.connections ?? [],
519
+ context: body.context ?? "",
520
+ source: body.source ?? "manual",
521
+ lastMentioned: new Date().toISOString().slice(0, 10),
522
+ created: new Date().toISOString().slice(0, 10),
523
+ notes: body.notes ?? "",
524
+ };
525
+ writeFileSync(filePath, contactToMarkdown(contact));
526
+ res.json(contact);
527
+ });
528
+ // Update contact
529
+ this.app.patch("/api/crm/contacts/:slug", express.json(), (req, res) => {
530
+ const filePath = join(crmDir, `${req.params.slug}.md`);
531
+ if (!existsSync(filePath)) {
532
+ res.status(404).json({ error: "Contact not found" });
533
+ return;
534
+ }
535
+ const existing = parseCrmFile(req.params.slug, readFileSync(filePath, "utf8"));
536
+ const body = req.body;
537
+ const updated = { ...existing, ...body, slug: existing.slug };
538
+ writeFileSync(filePath, contactToMarkdown(updated));
539
+ // If name changed, rename the file
540
+ if (body.name && slugify(body.name) !== existing.slug) {
541
+ const newSlug = slugify(body.name);
542
+ const newPath = join(crmDir, `${newSlug}.md`);
543
+ renameSync(filePath, newPath);
544
+ updated.slug = newSlug;
545
+ }
546
+ res.json(updated);
547
+ });
548
+ // Delete contact
549
+ this.app.delete("/api/crm/contacts/:slug", (req, res) => {
550
+ const filePath = join(crmDir, `${req.params.slug}.md`);
551
+ if (existsSync(filePath))
552
+ unlinkSync(filePath);
553
+ res.json({ ok: true });
554
+ });
265
555
  // Serve built dashboard in production
266
556
  const distPath = resolve(import.meta.dirname ?? __dirname, "../dashboard/dist");
267
557
  if (existsSync(distPath)) {