@ebowwa/daemons 0.5.0 → 0.7.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.
- package/dist/core.d.ts +89 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +346 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -125340
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +19 -63
- package/src/core.ts +476 -0
- package/src/index.ts +23 -101
- package/src/types.ts +24 -301
- package/dist/bin/discord-cli.js +0 -124118
- package/dist/bin/manager.js +0 -143
- package/dist/bin/telegram-cli.js +0 -124114
- package/src/agent.ts +0 -111
- package/src/channels/base.ts +0 -573
- package/src/channels/discord.ts +0 -306
- package/src/channels/index.ts +0 -169
- package/src/channels/telegram.ts +0 -315
- package/src/daemon.ts +0 -534
- package/src/hooks.ts +0 -97
- package/src/memory.ts +0 -369
- package/src/skills/coding/commit.ts +0 -202
- package/src/skills/coding/execute-subtask.ts +0 -136
- package/src/skills/coding/fix-issues.ts +0 -126
- package/src/skills/coding/index.ts +0 -26
- package/src/skills/coding/plan-task.ts +0 -158
- package/src/skills/coding/quality-check.ts +0 -155
- package/src/skills/index.ts +0 -65
- package/src/skills/registry.ts +0 -380
- package/src/skills/shared/index.ts +0 -21
- package/src/skills/shared/reflect.ts +0 -156
- package/src/skills/shared/review.ts +0 -201
- package/src/skills/shared/trajectory.ts +0 -319
- package/src/skills/trading/analyze-market.ts +0 -144
- package/src/skills/trading/check-risk.ts +0 -176
- package/src/skills/trading/execute-trade.ts +0 -185
- package/src/skills/trading/generate-signal.ts +0 -160
- package/src/skills/trading/index.ts +0 -26
- package/src/skills/trading/monitor-position.ts +0 -179
- package/src/skills/types.ts +0 -235
- package/src/skills/workflows.ts +0 -340
- package/src/state.ts +0 -77
- package/src/tools.ts +0 -134
- package/src/workflow.ts +0 -341
- package/src/workflows/coding.ts +0 -580
- package/src/workflows/index.ts +0 -61
- package/src/workflows/trading.ts +0 -608
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,sBAAsB;AACtB,OAAO,EACL,UAAU,GAMX,MAAM,WAAW,CAAC;AAEnB,QAAQ;AACR,cAAc,YAAY,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemons - Core Types
|
|
3
|
+
*
|
|
4
|
+
* Minimal types for DaemonCore and VPSDaemon.
|
|
5
|
+
*/
|
|
6
|
+
export type DaemonStatus = "starting" | "running" | "stopping" | "stopped" | "error";
|
|
7
|
+
export interface Channel {
|
|
8
|
+
id: string;
|
|
9
|
+
type: string;
|
|
10
|
+
start(): Promise<void>;
|
|
11
|
+
stop(): Promise<void>;
|
|
12
|
+
isRunning(): boolean;
|
|
13
|
+
send?(message: unknown): Promise<unknown>;
|
|
14
|
+
onMessage(handler: (msg: unknown) => Promise<void>): void;
|
|
15
|
+
}
|
|
16
|
+
export interface ChannelMessage {
|
|
17
|
+
id: string;
|
|
18
|
+
channelId: string;
|
|
19
|
+
type: "text" | "command" | "event";
|
|
20
|
+
content: string;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
}
|
|
24
|
+
export interface HealthStatus {
|
|
25
|
+
status: "healthy" | "degraded" | "unhealthy";
|
|
26
|
+
uptime: number;
|
|
27
|
+
channels: Record<string, {
|
|
28
|
+
running: boolean;
|
|
29
|
+
error?: string;
|
|
30
|
+
}>;
|
|
31
|
+
checks: Record<string, {
|
|
32
|
+
passed: boolean;
|
|
33
|
+
message?: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAErF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,SAAS,IAAI,OAAO,CAAC;IACrB,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,SAAS,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3D;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/D"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
package/package.json
CHANGED
|
@@ -1,67 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ebowwa/daemons",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Framework for building daemons - DaemonCore primitive with lifecycle, signals, channels, HTTP API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"daemons-telegram": "./dist/bin/telegram-cli.js",
|
|
10
|
-
"daemons-discord": "./dist/bin/discord-cli.js",
|
|
11
|
-
"daemons-manager": "./dist/bin/manager.js"
|
|
12
|
-
},
|
|
13
8
|
"exports": {
|
|
14
9
|
".": {
|
|
15
10
|
"types": "./dist/index.d.ts",
|
|
16
11
|
"import": "./dist/index.js"
|
|
17
12
|
},
|
|
18
|
-
"./
|
|
19
|
-
"types": "./dist/
|
|
20
|
-
"import": "./dist/
|
|
21
|
-
},
|
|
22
|
-
"./agent": {
|
|
23
|
-
"types": "./dist/agent.d.ts",
|
|
24
|
-
"import": "./dist/agent.js"
|
|
25
|
-
},
|
|
26
|
-
"./hooks": {
|
|
27
|
-
"types": "./dist/hooks.d.ts",
|
|
28
|
-
"import": "./dist/hooks.js"
|
|
29
|
-
},
|
|
30
|
-
"./tools": {
|
|
31
|
-
"types": "./dist/tools.d.ts",
|
|
32
|
-
"import": "./dist/tools.js"
|
|
33
|
-
},
|
|
34
|
-
"./state": {
|
|
35
|
-
"types": "./dist/state.d.ts",
|
|
36
|
-
"import": "./dist/state.js"
|
|
37
|
-
},
|
|
38
|
-
"./channels": {
|
|
39
|
-
"types": "./dist/channels/index.d.ts",
|
|
40
|
-
"import": "./dist/channels/index.js"
|
|
13
|
+
"./core": {
|
|
14
|
+
"types": "./dist/core.d.ts",
|
|
15
|
+
"import": "./dist/core.js"
|
|
41
16
|
}
|
|
42
17
|
},
|
|
43
18
|
"scripts": {
|
|
44
|
-
"build": "
|
|
45
|
-
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
19
|
+
"build": "tsc",
|
|
46
20
|
"dev": "bun --watch src/index.ts",
|
|
47
21
|
"test": "bun test",
|
|
48
|
-
"
|
|
49
|
-
"clean": "rm -rf dist",
|
|
50
|
-
"telegram": "bun run dist/bin/telegram-cli.js",
|
|
51
|
-
"discord": "bun run dist/bin/discord-cli.js",
|
|
52
|
-
"manager": "bun run dist/bin/manager.js",
|
|
53
|
-
"status": "bun run dist/bin/manager.js status"
|
|
54
|
-
},
|
|
55
|
-
"dependencies": {
|
|
56
|
-
"@ebowwa/ai": "^0.3.0",
|
|
57
|
-
"@ebowwa/channel-telegram": "^1.13.0",
|
|
58
|
-
"@ebowwa/channel-types": "^0.1.1",
|
|
59
|
-
"@ebowwa/codespaces-types": "^1.4.5",
|
|
60
|
-
"@ebowwa/structured-prompts": "^0.3.2",
|
|
61
|
-
"@ebowwa/teammates": "^0.1.2",
|
|
62
|
-
"discord.js": "^14.16.3",
|
|
63
|
-
"zod": "^4.3.5"
|
|
22
|
+
"clean": "rm -rf dist"
|
|
64
23
|
},
|
|
24
|
+
"dependencies": {},
|
|
65
25
|
"devDependencies": {
|
|
66
26
|
"@types/bun": "catalog:dev",
|
|
67
27
|
"@types/node": "catalog:dev",
|
|
@@ -74,21 +34,17 @@
|
|
|
74
34
|
"README.md"
|
|
75
35
|
],
|
|
76
36
|
"keywords": [
|
|
77
|
-
"glm",
|
|
78
37
|
"daemon",
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"telegram",
|
|
83
|
-
"discord",
|
|
84
|
-
"bot",
|
|
85
|
-
"chatbot",
|
|
86
|
-
"channels"
|
|
38
|
+
"framework",
|
|
39
|
+
"lifecycle",
|
|
40
|
+
"signals"
|
|
87
41
|
],
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
42
|
+
"author": "ebowwa",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"ownership": {
|
|
45
|
+
"domain": "daemons",
|
|
46
|
+
"responsibilities": [
|
|
47
|
+
"daemon-framework"
|
|
48
|
+
]
|
|
93
49
|
}
|
|
94
|
-
}
|
|
50
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Core - Generic Daemon Primitive
|
|
3
|
+
*
|
|
4
|
+
* Base class for all daemons. Provides:
|
|
5
|
+
* - Lifecycle management (start/stop)
|
|
6
|
+
* - Signal handling (SIGINT, SIGTERM, SIGHUP)
|
|
7
|
+
* - Channel registration and routing
|
|
8
|
+
* - HTTP health/status API
|
|
9
|
+
* - State persistence
|
|
10
|
+
* - Hook system
|
|
11
|
+
*
|
|
12
|
+
* @module @ebowwa/daemons/core
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { promises as fs } from "fs";
|
|
16
|
+
import type {
|
|
17
|
+
Channel,
|
|
18
|
+
ChannelMessage,
|
|
19
|
+
HealthStatus,
|
|
20
|
+
DaemonStatus,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
// ============================================================
|
|
24
|
+
// Core Types
|
|
25
|
+
// ============================================================
|
|
26
|
+
|
|
27
|
+
export interface DaemonCoreConfig {
|
|
28
|
+
/** Unique daemon identifier */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Human-readable name */
|
|
31
|
+
name: string;
|
|
32
|
+
/** HTTP API config */
|
|
33
|
+
api?: {
|
|
34
|
+
port?: number;
|
|
35
|
+
host?: string;
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** State persistence */
|
|
39
|
+
state?: {
|
|
40
|
+
path?: string;
|
|
41
|
+
autoSave?: boolean;
|
|
42
|
+
saveInterval?: number;
|
|
43
|
+
};
|
|
44
|
+
/** Graceful shutdown timeout (ms) */
|
|
45
|
+
shutdownTimeout?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DaemonCoreState {
|
|
49
|
+
status: DaemonStatus;
|
|
50
|
+
startedAt: string | null;
|
|
51
|
+
stoppedAt: string | null;
|
|
52
|
+
error: string | null;
|
|
53
|
+
metadata: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type CoreHookName =
|
|
57
|
+
| "onStart"
|
|
58
|
+
| "onStop"
|
|
59
|
+
| "onError"
|
|
60
|
+
| "onChannelRegister"
|
|
61
|
+
| "onChannelStart"
|
|
62
|
+
| "onChannelStop"
|
|
63
|
+
| "onMessage"
|
|
64
|
+
| "onHealthCheck";
|
|
65
|
+
|
|
66
|
+
export type CoreHookCallback<P = unknown, R = void> = (payload: P) => Promise<R> | R;
|
|
67
|
+
|
|
68
|
+
export interface DaemonPlugin {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
init(daemon: DaemonCore): Promise<void>;
|
|
72
|
+
destroy?(): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================
|
|
76
|
+
// Daemon Core Class
|
|
77
|
+
// ============================================================
|
|
78
|
+
|
|
79
|
+
const DEFAULT_STATE_PATH = ".daemon-state.json";
|
|
80
|
+
const DEFAULT_SHUTDOWN_TIMEOUT = 10000;
|
|
81
|
+
|
|
82
|
+
export class DaemonCore {
|
|
83
|
+
protected config: DaemonCoreConfig;
|
|
84
|
+
protected state: DaemonCoreState;
|
|
85
|
+
protected channels: Map<string, Channel> = new Map();
|
|
86
|
+
private hooks: Map<CoreHookName, Set<CoreHookCallback>> = new Map();
|
|
87
|
+
private plugins: Map<string, DaemonPlugin> = new Map();
|
|
88
|
+
private messageHandler: ((msg: ChannelMessage) => Promise<void>) | null = null;
|
|
89
|
+
private httpServer: ReturnType<typeof Bun.serve> | null = null;
|
|
90
|
+
private stateSaveInterval: Timer | null = null;
|
|
91
|
+
protected shuttingDown = false;
|
|
92
|
+
|
|
93
|
+
constructor(config: DaemonCoreConfig) {
|
|
94
|
+
this.config = {
|
|
95
|
+
...config,
|
|
96
|
+
api: config.api ?? { enabled: false },
|
|
97
|
+
state: config.state ?? { autoSave: false },
|
|
98
|
+
shutdownTimeout: config.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.state = {
|
|
102
|
+
status: "stopped",
|
|
103
|
+
startedAt: null,
|
|
104
|
+
stoppedAt: null,
|
|
105
|
+
error: null,
|
|
106
|
+
metadata: {},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this.setupSignalHandlers();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================
|
|
113
|
+
// Lifecycle
|
|
114
|
+
// ============================================================
|
|
115
|
+
|
|
116
|
+
async start(): Promise<void> {
|
|
117
|
+
if (this.state.status === "running") {
|
|
118
|
+
throw new Error("Daemon already running");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.state.status = "starting";
|
|
122
|
+
this.log("Starting...");
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await this.loadState();
|
|
126
|
+
await this.executeHooks("onStart", this.state);
|
|
127
|
+
|
|
128
|
+
for (const [id, channel] of this.channels) {
|
|
129
|
+
await this.startChannel(id, channel);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (this.config.api?.enabled !== false && this.config.api?.port) {
|
|
133
|
+
this.startHTTP();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.config.state?.autoSave && this.config.state.saveInterval) {
|
|
137
|
+
this.stateSaveInterval = setInterval(
|
|
138
|
+
() => this.saveState(),
|
|
139
|
+
this.config.state.saveInterval
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.state.status = "running";
|
|
144
|
+
this.state.startedAt = new Date().toISOString();
|
|
145
|
+
this.state.error = null;
|
|
146
|
+
|
|
147
|
+
this.log("Running");
|
|
148
|
+
} catch (error) {
|
|
149
|
+
this.state.status = "error";
|
|
150
|
+
this.state.error = error instanceof Error ? error.message : String(error);
|
|
151
|
+
await this.executeHooks("onError", error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async stop(): Promise<void> {
|
|
157
|
+
if (this.state.status !== "running") return;
|
|
158
|
+
if (this.shuttingDown) return;
|
|
159
|
+
|
|
160
|
+
this.shuttingDown = true;
|
|
161
|
+
this.state.status = "stopping";
|
|
162
|
+
this.log("Stopping...");
|
|
163
|
+
|
|
164
|
+
const timeout = this.config.shutdownTimeout!;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
if (this.stateSaveInterval) {
|
|
168
|
+
clearInterval(this.stateSaveInterval);
|
|
169
|
+
this.stateSaveInterval = null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await Promise.race([
|
|
173
|
+
this.stopAllChannels(),
|
|
174
|
+
new Promise((_, reject) =>
|
|
175
|
+
setTimeout(() => reject(new Error("Channel stop timeout")), timeout / 2)
|
|
176
|
+
),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
if (this.httpServer) {
|
|
180
|
+
this.httpServer.stop();
|
|
181
|
+
this.httpServer = null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await this.executeHooks("onStop", this.state);
|
|
185
|
+
|
|
186
|
+
for (const plugin of this.plugins.values()) {
|
|
187
|
+
await plugin.destroy?.();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await this.saveState();
|
|
191
|
+
|
|
192
|
+
this.state.status = "stopped";
|
|
193
|
+
this.state.stoppedAt = new Date().toISOString();
|
|
194
|
+
this.log("Stopped");
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.state.status = "error";
|
|
197
|
+
this.state.error = error instanceof Error ? error.message : String(error);
|
|
198
|
+
this.log(`Stop error: ${error}`);
|
|
199
|
+
} finally {
|
|
200
|
+
this.shuttingDown = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async restart(): Promise<void> {
|
|
205
|
+
await this.stop();
|
|
206
|
+
await this.start();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================================
|
|
210
|
+
// Status & Health
|
|
211
|
+
// ============================================================
|
|
212
|
+
|
|
213
|
+
getStatus(): DaemonStatus {
|
|
214
|
+
return this.state.status;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getState(): Readonly<DaemonCoreState> {
|
|
218
|
+
return { ...this.state };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getHealth(): Promise<HealthStatus> {
|
|
222
|
+
const channelStatus: Record<string, { running: boolean; error?: string }> = {};
|
|
223
|
+
|
|
224
|
+
for (const [id, channel] of this.channels) {
|
|
225
|
+
channelStatus[id] = { running: channel.isRunning() };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const hookResults = await this.executeHooks("onHealthCheck", null) as unknown[];
|
|
229
|
+
const checks: Record<string, { passed: boolean; message?: string }> = {};
|
|
230
|
+
|
|
231
|
+
hookResults.forEach((result, i) => {
|
|
232
|
+
if (result && typeof result === "object" && "passed" in result) {
|
|
233
|
+
checks[`check-${i}`] = result as { passed: boolean; message?: string };
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const allChannelsRunning = Object.values(channelStatus).every(c => c.running);
|
|
238
|
+
const allChecksPassed = Object.values(checks).every(c => c.passed);
|
|
239
|
+
|
|
240
|
+
let status: "healthy" | "degraded" | "unhealthy";
|
|
241
|
+
if (!allChannelsRunning) status = "unhealthy";
|
|
242
|
+
else if (!allChecksPassed) status = "degraded";
|
|
243
|
+
else status = "healthy";
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
status,
|
|
247
|
+
uptime: this.state.startedAt ? Date.now() - new Date(this.state.startedAt).getTime() : 0,
|
|
248
|
+
channels: channelStatus,
|
|
249
|
+
checks,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================
|
|
254
|
+
// Channels
|
|
255
|
+
// ============================================================
|
|
256
|
+
|
|
257
|
+
registerChannel(channel: Channel): void {
|
|
258
|
+
if (this.channels.has(channel.id)) {
|
|
259
|
+
throw new Error(`Channel already registered: ${channel.id}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.channels.set(channel.id, channel);
|
|
263
|
+
channel.onMessage((msg) => this.handleChannelMessage(channel.id, msg));
|
|
264
|
+
|
|
265
|
+
this.executeHooks("onChannelRegister", channel);
|
|
266
|
+
this.log(`Channel registered: ${channel.id}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getChannel(id: string): Channel | undefined {
|
|
270
|
+
return this.channels.get(id);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
getChannels(): Channel[] {
|
|
274
|
+
return Array.from(this.channels.values());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private async startChannel(id: string, channel: Channel): Promise<void> {
|
|
278
|
+
await channel.start();
|
|
279
|
+
await this.executeHooks("onChannelStart", { id, channel });
|
|
280
|
+
this.log(`Channel started: ${id}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async stopAllChannels(): Promise<void> {
|
|
284
|
+
const stops = Array.from(this.channels.entries()).map(async ([id, channel]) => {
|
|
285
|
+
try {
|
|
286
|
+
await channel.stop();
|
|
287
|
+
await this.executeHooks("onChannelStop", { id, channel });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
this.log(`Channel stop failed: ${id}`);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await Promise.allSettled(stops);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private async handleChannelMessage(channelId: string, msg: unknown): Promise<void> {
|
|
297
|
+
const message = msg as ChannelMessage;
|
|
298
|
+
await this.executeHooks("onMessage", { channelId, message });
|
|
299
|
+
|
|
300
|
+
if (this.messageHandler) {
|
|
301
|
+
try {
|
|
302
|
+
await this.messageHandler(message);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
this.log(`Message handler error: ${error}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================
|
|
310
|
+
// Message Handler
|
|
311
|
+
// ============================================================
|
|
312
|
+
|
|
313
|
+
onMessage(handler: (msg: ChannelMessage) => Promise<void>): void {
|
|
314
|
+
this.messageHandler = handler;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ============================================================
|
|
318
|
+
// Hooks
|
|
319
|
+
// ============================================================
|
|
320
|
+
|
|
321
|
+
registerHook(name: CoreHookName, callback: CoreHookCallback): void {
|
|
322
|
+
if (!this.hooks.has(name)) this.hooks.set(name, new Set());
|
|
323
|
+
this.hooks.get(name)!.add(callback);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
onStart(cb: CoreHookCallback): void { this.registerHook("onStart", cb); }
|
|
327
|
+
onStop(cb: CoreHookCallback): void { this.registerHook("onStop", cb); }
|
|
328
|
+
onError(cb: CoreHookCallback): void { this.registerHook("onError", cb); }
|
|
329
|
+
onHealthCheck(cb: CoreHookCallback): void { this.registerHook("onHealthCheck", cb); }
|
|
330
|
+
|
|
331
|
+
protected async executeHooks(name: CoreHookName, payload: unknown): Promise<unknown[]> {
|
|
332
|
+
const callbacks = this.hooks.get(name);
|
|
333
|
+
if (!callbacks) return [];
|
|
334
|
+
|
|
335
|
+
const results = [];
|
|
336
|
+
for (const cb of callbacks) {
|
|
337
|
+
try {
|
|
338
|
+
results.push(await cb(payload));
|
|
339
|
+
} catch (error) {
|
|
340
|
+
this.log(`Hook ${name} error: ${error}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ============================================================
|
|
347
|
+
// Plugins
|
|
348
|
+
// ============================================================
|
|
349
|
+
|
|
350
|
+
async use(plugin: DaemonPlugin): Promise<void> {
|
|
351
|
+
if (this.plugins.has(plugin.id)) {
|
|
352
|
+
throw new Error(`Plugin already registered: ${plugin.id}`);
|
|
353
|
+
}
|
|
354
|
+
await plugin.init(this);
|
|
355
|
+
this.plugins.set(plugin.id, plugin);
|
|
356
|
+
this.log(`Plugin loaded: ${plugin.name}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ============================================================
|
|
360
|
+
// State Persistence
|
|
361
|
+
// ============================================================
|
|
362
|
+
|
|
363
|
+
updateState(updates: Partial<DaemonCoreState["metadata"]>): void {
|
|
364
|
+
this.state.metadata = { ...this.state.metadata, ...updates };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private async loadState(): Promise<void> {
|
|
368
|
+
const path = this.config.state?.path ?? DEFAULT_STATE_PATH;
|
|
369
|
+
try {
|
|
370
|
+
const data = await fs.readFile(path, "utf-8");
|
|
371
|
+
const saved = JSON.parse(data);
|
|
372
|
+
this.state.metadata = saved.metadata ?? {};
|
|
373
|
+
} catch {
|
|
374
|
+
// No saved state
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async saveState(): Promise<void> {
|
|
379
|
+
const path = this.config.state?.path ?? DEFAULT_STATE_PATH;
|
|
380
|
+
try {
|
|
381
|
+
await fs.writeFile(path, JSON.stringify({
|
|
382
|
+
status: this.state.status,
|
|
383
|
+
metadata: this.state.metadata,
|
|
384
|
+
savedAt: new Date().toISOString(),
|
|
385
|
+
}, null, 2));
|
|
386
|
+
} catch (error) {
|
|
387
|
+
this.log(`State save failed: ${error}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============================================================
|
|
392
|
+
// HTTP API
|
|
393
|
+
// ============================================================
|
|
394
|
+
|
|
395
|
+
private startHTTP(): void {
|
|
396
|
+
const port = this.config.api?.port ?? 8911;
|
|
397
|
+
const hostname = this.config.api?.host ?? "0.0.0.0";
|
|
398
|
+
|
|
399
|
+
this.httpServer = Bun.serve({
|
|
400
|
+
port,
|
|
401
|
+
hostname,
|
|
402
|
+
fetch: async (req) => {
|
|
403
|
+
const url = new URL(req.url);
|
|
404
|
+
const path = url.pathname;
|
|
405
|
+
|
|
406
|
+
const corsHeaders = {
|
|
407
|
+
"Access-Control-Allow-Origin": "*",
|
|
408
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
409
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
if (req.method === "OPTIONS") {
|
|
413
|
+
return new Response(null, { headers: corsHeaders });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (path === "/health") {
|
|
417
|
+
const health = await this.getHealth();
|
|
418
|
+
return Response.json(health, { headers: corsHeaders });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (path === "/api/status" || path === "/status") {
|
|
422
|
+
return Response.json({
|
|
423
|
+
id: this.config.id,
|
|
424
|
+
name: this.config.name,
|
|
425
|
+
status: this.state.status,
|
|
426
|
+
startedAt: this.state.startedAt,
|
|
427
|
+
channels: Object.fromEntries(
|
|
428
|
+
Array.from(this.channels.entries()).map(([id, ch]) => [id, ch.isRunning()])
|
|
429
|
+
),
|
|
430
|
+
}, { headers: corsHeaders });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (path === "/api/channels") {
|
|
434
|
+
return Response.json(
|
|
435
|
+
this.getChannels().map(ch => ({ id: ch.id, type: ch.type, running: ch.isRunning() })),
|
|
436
|
+
{ headers: corsHeaders }
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
this.log(`API: http://${hostname}:${port}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================================
|
|
448
|
+
// Signal Handlers
|
|
449
|
+
// ============================================================
|
|
450
|
+
|
|
451
|
+
private setupSignalHandlers(): void {
|
|
452
|
+
const handleSignal = async (signal: string) => {
|
|
453
|
+
this.log(`Received ${signal}`);
|
|
454
|
+
await this.stop();
|
|
455
|
+
process.exit(0);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
459
|
+
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
460
|
+
|
|
461
|
+
process.on("SIGHUP", async () => {
|
|
462
|
+
this.log("Received SIGHUP - saving state");
|
|
463
|
+
await this.saveState();
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ============================================================
|
|
468
|
+
// Logging
|
|
469
|
+
// ============================================================
|
|
470
|
+
|
|
471
|
+
protected log(message: string): void {
|
|
472
|
+
console.log(`[${this.config.name}] ${message}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export default DaemonCore;
|