homarus 0.1.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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/agent-manager.d.ts +35 -0
- package/dist/agent-manager.js +127 -0
- package/dist/agent-worker.d.ts +2 -0
- package/dist/agent-worker.js +141 -0
- package/dist/agent.d.ts +33 -0
- package/dist/agent.js +197 -0
- package/dist/browser-manager.d.ts +33 -0
- package/dist/browser-manager.js +170 -0
- package/dist/channel-adapter.d.ts +29 -0
- package/dist/channel-adapter.js +94 -0
- package/dist/channel-manager.d.ts +17 -0
- package/dist/channel-manager.js +84 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +212 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +185 -0
- package/dist/embedding-provider.d.ts +35 -0
- package/dist/embedding-provider.js +103 -0
- package/dist/event-bus.d.ts +18 -0
- package/dist/event-bus.js +46 -0
- package/dist/event-queue.d.ts +18 -0
- package/dist/event-queue.js +77 -0
- package/dist/execution-strategy.d.ts +26 -0
- package/dist/execution-strategy.js +20 -0
- package/dist/homarus.d.ts +36 -0
- package/dist/homarus.js +308 -0
- package/dist/http-api.d.ts +16 -0
- package/dist/http-api.js +82 -0
- package/dist/identity-manager.d.ts +28 -0
- package/dist/identity-manager.js +123 -0
- package/dist/memory-index.d.ts +52 -0
- package/dist/memory-index.js +286 -0
- package/dist/model-provider.d.ts +33 -0
- package/dist/model-provider.js +255 -0
- package/dist/model-router.d.ts +32 -0
- package/dist/model-router.js +148 -0
- package/dist/setup-wizard.d.ts +28 -0
- package/dist/setup-wizard.js +240 -0
- package/dist/skill-manager.d.ts +26 -0
- package/dist/skill-manager.js +171 -0
- package/dist/skill-transport.d.ts +51 -0
- package/dist/skill-transport.js +116 -0
- package/dist/skill.d.ts +22 -0
- package/dist/skill.js +118 -0
- package/dist/subprocess-strategy.d.ts +54 -0
- package/dist/subprocess-strategy.js +106 -0
- package/dist/telegram-adapter.d.ts +34 -0
- package/dist/telegram-adapter.js +165 -0
- package/dist/timer-service.d.ts +30 -0
- package/dist/timer-service.js +142 -0
- package/dist/tool-registry.d.ts +29 -0
- package/dist/tool-registry.js +100 -0
- package/dist/tools/bash.d.ts +3 -0
- package/dist/tools/bash.js +48 -0
- package/dist/tools/browser.d.ts +4 -0
- package/dist/tools/browser.js +47 -0
- package/dist/tools/edit.d.ts +3 -0
- package/dist/tools/edit.js +48 -0
- package/dist/tools/git.d.ts +3 -0
- package/dist/tools/git.js +109 -0
- package/dist/tools/glob.d.ts +3 -0
- package/dist/tools/glob.js +86 -0
- package/dist/tools/grep.d.ts +3 -0
- package/dist/tools/grep.js +169 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +46 -0
- package/dist/tools/lsp.d.ts +3 -0
- package/dist/tools/lsp.js +216 -0
- package/dist/tools/memory.d.ts +4 -0
- package/dist/tools/memory.js +64 -0
- package/dist/tools/read.d.ts +3 -0
- package/dist/tools/read.js +49 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +51 -0
- package/dist/tools/web-search.d.ts +3 -0
- package/dist/tools/web-search.js +73 -0
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.js +31 -0
- package/dist/types.d.ts +240 -0
- package/dist/types.js +14 -0
- package/package.json +69 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BrowserConfig, Logger } from "./types.js";
|
|
2
|
+
export interface BrowserAction {
|
|
3
|
+
action: "navigate" | "click" | "type" | "screenshot" | "content" | "evaluate" | "back" | "forward" | "wait" | "scroll";
|
|
4
|
+
url?: string;
|
|
5
|
+
selector?: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
script?: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
direction?: "up" | "down";
|
|
10
|
+
pixels?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface BrowserResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
data?: string;
|
|
15
|
+
screenshot?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class BrowserManager {
|
|
21
|
+
private browser;
|
|
22
|
+
private context;
|
|
23
|
+
private page;
|
|
24
|
+
private config;
|
|
25
|
+
private logger;
|
|
26
|
+
private launched;
|
|
27
|
+
constructor(logger: Logger, config?: BrowserConfig);
|
|
28
|
+
launch(): Promise<void>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
isLaunched(): boolean;
|
|
31
|
+
execute(action: BrowserAction): Promise<BrowserResult>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=browser-manager.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export class BrowserManager {
|
|
2
|
+
browser = null;
|
|
3
|
+
context = null;
|
|
4
|
+
page = null;
|
|
5
|
+
config;
|
|
6
|
+
logger;
|
|
7
|
+
launched = false;
|
|
8
|
+
constructor(logger, config = {}) {
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
async launch() {
|
|
13
|
+
if (this.launched)
|
|
14
|
+
return;
|
|
15
|
+
const modName = "playwright";
|
|
16
|
+
// Dynamic import — fails at runtime (not compile time) if playwright isn't installed
|
|
17
|
+
const pw = await import(modName).catch(() => {
|
|
18
|
+
throw new Error("playwright is not installed — run: npm install playwright");
|
|
19
|
+
});
|
|
20
|
+
const chromium = pw.chromium;
|
|
21
|
+
const launchOptions = {
|
|
22
|
+
headless: this.config.headless ?? true,
|
|
23
|
+
};
|
|
24
|
+
if (this.config.executablePath) {
|
|
25
|
+
launchOptions.executablePath = this.config.executablePath;
|
|
26
|
+
}
|
|
27
|
+
if (this.config.proxy) {
|
|
28
|
+
launchOptions.proxy = { server: this.config.proxy };
|
|
29
|
+
}
|
|
30
|
+
this.browser = await chromium.launch(launchOptions);
|
|
31
|
+
this.context = await this.browser.newContext({
|
|
32
|
+
viewport: this.config.viewport ?? { width: 1280, height: 720 },
|
|
33
|
+
});
|
|
34
|
+
this.page = await this.context.newPage();
|
|
35
|
+
this.page.setDefaultTimeout(this.config.timeout ?? 30_000);
|
|
36
|
+
this.launched = true;
|
|
37
|
+
this.logger.info("Browser launched", { headless: this.config.headless ?? true });
|
|
38
|
+
}
|
|
39
|
+
async close() {
|
|
40
|
+
if (!this.launched)
|
|
41
|
+
return;
|
|
42
|
+
await this.context?.close();
|
|
43
|
+
await this.browser?.close();
|
|
44
|
+
this.page = null;
|
|
45
|
+
this.context = null;
|
|
46
|
+
this.browser = null;
|
|
47
|
+
this.launched = false;
|
|
48
|
+
this.logger.info("Browser closed");
|
|
49
|
+
}
|
|
50
|
+
isLaunched() {
|
|
51
|
+
return this.launched;
|
|
52
|
+
}
|
|
53
|
+
async execute(action) {
|
|
54
|
+
if (!this.launched || !this.page) {
|
|
55
|
+
// Auto-launch on first use
|
|
56
|
+
await this.launch();
|
|
57
|
+
if (!this.page) {
|
|
58
|
+
return { success: false, error: "Browser failed to launch" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const page = this.page;
|
|
62
|
+
const timeout = action.timeout ?? this.config.timeout ?? 30_000;
|
|
63
|
+
try {
|
|
64
|
+
switch (action.action) {
|
|
65
|
+
case "navigate": {
|
|
66
|
+
if (!action.url)
|
|
67
|
+
return { success: false, error: "url required for navigate" };
|
|
68
|
+
await page.goto(action.url, { timeout, waitUntil: "domcontentloaded" });
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
url: page.url(),
|
|
72
|
+
title: await page.title(),
|
|
73
|
+
data: `Navigated to ${page.url()}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
case "click": {
|
|
77
|
+
if (!action.selector)
|
|
78
|
+
return { success: false, error: "selector required for click" };
|
|
79
|
+
await page.click(action.selector, { timeout });
|
|
80
|
+
await page.waitForLoadState("domcontentloaded").catch(() => { });
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
url: page.url(),
|
|
84
|
+
data: `Clicked ${action.selector}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
case "type": {
|
|
88
|
+
if (!action.selector)
|
|
89
|
+
return { success: false, error: "selector required for type" };
|
|
90
|
+
if (action.text === undefined)
|
|
91
|
+
return { success: false, error: "text required for type" };
|
|
92
|
+
await page.fill(action.selector, action.text, { timeout });
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
data: `Typed into ${action.selector}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
case "screenshot": {
|
|
99
|
+
const buffer = await page.screenshot({ fullPage: false, type: "png" });
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
screenshot: buffer.toString("base64"),
|
|
103
|
+
url: page.url(),
|
|
104
|
+
title: await page.title(),
|
|
105
|
+
data: `Screenshot captured (${buffer.length} bytes)`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
case "content": {
|
|
109
|
+
// Get visible text content, not raw HTML
|
|
110
|
+
const text = await page.evaluate(() => {
|
|
111
|
+
// Get innerText which respects visibility
|
|
112
|
+
return document.body.innerText;
|
|
113
|
+
});
|
|
114
|
+
const truncated = text.length > 50_000
|
|
115
|
+
? text.slice(0, 50_000) + `\n... (truncated, ${text.length} total chars)`
|
|
116
|
+
: text;
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
data: truncated,
|
|
120
|
+
url: page.url(),
|
|
121
|
+
title: await page.title(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
case "evaluate": {
|
|
125
|
+
if (!action.script)
|
|
126
|
+
return { success: false, error: "script required for evaluate" };
|
|
127
|
+
const result = await page.evaluate(action.script);
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
data: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
case "back": {
|
|
134
|
+
await page.goBack({ timeout });
|
|
135
|
+
return { success: true, url: page.url(), data: "Navigated back" };
|
|
136
|
+
}
|
|
137
|
+
case "forward": {
|
|
138
|
+
await page.goForward({ timeout });
|
|
139
|
+
return { success: true, url: page.url(), data: "Navigated forward" };
|
|
140
|
+
}
|
|
141
|
+
case "wait": {
|
|
142
|
+
if (action.selector) {
|
|
143
|
+
await page.waitForSelector(action.selector, { timeout });
|
|
144
|
+
return { success: true, data: `Element found: ${action.selector}` };
|
|
145
|
+
}
|
|
146
|
+
// Wait a fixed duration
|
|
147
|
+
await page.waitForTimeout(action.timeout ?? 1000);
|
|
148
|
+
return { success: true, data: `Waited ${action.timeout ?? 1000}ms` };
|
|
149
|
+
}
|
|
150
|
+
case "scroll": {
|
|
151
|
+
const dir = action.direction ?? "down";
|
|
152
|
+
const px = action.pixels ?? 500;
|
|
153
|
+
const delta = dir === "down" ? px : -px;
|
|
154
|
+
await page.evaluate((d) => window.scrollBy(0, d), delta);
|
|
155
|
+
return { success: true, data: `Scrolled ${dir} ${px}px` };
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
return { success: false, error: `Unknown action: ${action.action}` };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
error: String(err.message ?? err),
|
|
165
|
+
url: page.url(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=browser-manager.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Event, MessagePayload, OutboundMessage, DmPolicy, GroupPolicy, HealthStatus, Logger } from "./types.js";
|
|
2
|
+
export type AdapterState = "disconnected" | "connecting" | "connected" | "error";
|
|
3
|
+
export declare abstract class ChannelAdapter {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
protected state: AdapterState;
|
|
6
|
+
protected dmPolicy: DmPolicy;
|
|
7
|
+
protected groupPolicy: GroupPolicy;
|
|
8
|
+
protected logger: Logger;
|
|
9
|
+
private messageHandler;
|
|
10
|
+
constructor(name: string, logger: Logger, dmPolicy?: DmPolicy, groupPolicy?: GroupPolicy);
|
|
11
|
+
getState(): AdapterState;
|
|
12
|
+
abstract connect(): Promise<void>;
|
|
13
|
+
abstract disconnect(): Promise<void>;
|
|
14
|
+
abstract send(target: string, message: OutboundMessage): Promise<void>;
|
|
15
|
+
abstract health(): HealthStatus;
|
|
16
|
+
onMessage(handler: (event: Event) => void): void;
|
|
17
|
+
protected normalizeInbound(payload: MessagePayload): Event;
|
|
18
|
+
protected checkAccess(payload: MessagePayload): boolean;
|
|
19
|
+
protected deliver(payload: MessagePayload): void;
|
|
20
|
+
}
|
|
21
|
+
export declare class CLIChannelAdapter extends ChannelAdapter {
|
|
22
|
+
private rl;
|
|
23
|
+
constructor(logger: Logger);
|
|
24
|
+
connect(): Promise<void>;
|
|
25
|
+
disconnect(): Promise<void>;
|
|
26
|
+
send(_target: string, message: OutboundMessage): Promise<void>;
|
|
27
|
+
health(): HealthStatus;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=channel-adapter.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// CRC: crc-ChannelAdapter.md | Seq: seq-message-dispatch.md
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
export class ChannelAdapter {
|
|
4
|
+
name;
|
|
5
|
+
state = "disconnected";
|
|
6
|
+
dmPolicy;
|
|
7
|
+
groupPolicy;
|
|
8
|
+
logger;
|
|
9
|
+
messageHandler = null;
|
|
10
|
+
constructor(name, logger, dmPolicy = "open", groupPolicy = "mention_required") {
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
this.dmPolicy = dmPolicy;
|
|
14
|
+
this.groupPolicy = groupPolicy;
|
|
15
|
+
}
|
|
16
|
+
getState() {
|
|
17
|
+
return this.state;
|
|
18
|
+
}
|
|
19
|
+
// CRC: crc-ChannelAdapter.md — onMessage()
|
|
20
|
+
onMessage(handler) {
|
|
21
|
+
this.messageHandler = handler;
|
|
22
|
+
}
|
|
23
|
+
// CRC: crc-ChannelAdapter.md — normalizeInbound()
|
|
24
|
+
normalizeInbound(payload) {
|
|
25
|
+
return {
|
|
26
|
+
id: uuid(),
|
|
27
|
+
type: "message",
|
|
28
|
+
source: `channel:${this.name}`,
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
payload,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// CRC: crc-ChannelAdapter.md — checkAccess()
|
|
34
|
+
checkAccess(payload) {
|
|
35
|
+
if (payload.isGroup) {
|
|
36
|
+
if (this.groupPolicy === "disabled")
|
|
37
|
+
return false;
|
|
38
|
+
if (this.groupPolicy === "mention_required" && !payload.isMention)
|
|
39
|
+
return false;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// DM
|
|
43
|
+
if (this.dmPolicy === "disabled")
|
|
44
|
+
return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Subclasses call this to deliver normalized events to the loop
|
|
48
|
+
deliver(payload) {
|
|
49
|
+
if (!this.checkAccess(payload)) {
|
|
50
|
+
this.logger.debug("Message rejected by policy", { channel: this.name, from: payload.from });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const event = this.normalizeInbound(payload);
|
|
54
|
+
this.messageHandler?.(event);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// CLI channel adapter — reads from stdin, writes to stdout
|
|
58
|
+
export class CLIChannelAdapter extends ChannelAdapter {
|
|
59
|
+
rl = null;
|
|
60
|
+
constructor(logger) {
|
|
61
|
+
super("cli", logger, "open", "always_on");
|
|
62
|
+
}
|
|
63
|
+
async connect() {
|
|
64
|
+
this.state = "connecting";
|
|
65
|
+
const { createInterface } = await import("node:readline");
|
|
66
|
+
this.rl = createInterface({ input: process.stdin });
|
|
67
|
+
this.rl.on("line", (line) => {
|
|
68
|
+
if (!line.trim())
|
|
69
|
+
return;
|
|
70
|
+
this.deliver({
|
|
71
|
+
from: "user",
|
|
72
|
+
channel: "cli",
|
|
73
|
+
text: line,
|
|
74
|
+
isGroup: false,
|
|
75
|
+
isMention: false,
|
|
76
|
+
raw: line,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
this.state = "connected";
|
|
80
|
+
this.logger.info("CLI channel connected");
|
|
81
|
+
}
|
|
82
|
+
async disconnect() {
|
|
83
|
+
this.rl?.close();
|
|
84
|
+
this.rl = null;
|
|
85
|
+
this.state = "disconnected";
|
|
86
|
+
}
|
|
87
|
+
async send(_target, message) {
|
|
88
|
+
process.stdout.write(message.text + "\n");
|
|
89
|
+
}
|
|
90
|
+
health() {
|
|
91
|
+
return { healthy: this.state === "connected", lastCheck: Date.now() };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=channel-adapter.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Event, OutboundMessage, ChannelConfig, HealthStatus, Logger } from "./types.js";
|
|
2
|
+
import { ChannelAdapter } from "./channel-adapter.js";
|
|
3
|
+
export declare class ChannelManager {
|
|
4
|
+
private adapters;
|
|
5
|
+
private logger;
|
|
6
|
+
private eventHandler;
|
|
7
|
+
constructor(logger: Logger);
|
|
8
|
+
setEventHandler(fn: (event: Event) => void): void;
|
|
9
|
+
loadAdapters(channels: Record<string, ChannelConfig>): void;
|
|
10
|
+
connectAll(): Promise<void>;
|
|
11
|
+
disconnectAll(): Promise<void>;
|
|
12
|
+
send(channel: string, target: string, message: OutboundMessage): Promise<void>;
|
|
13
|
+
getAdapter(name: string): ChannelAdapter | undefined;
|
|
14
|
+
getConnected(): ChannelAdapter[];
|
|
15
|
+
healthCheck(): Record<string, HealthStatus>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=channel-manager.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { CLIChannelAdapter } from "./channel-adapter.js";
|
|
2
|
+
import { TelegramChannelAdapter } from "./telegram-adapter.js";
|
|
3
|
+
export class ChannelManager {
|
|
4
|
+
adapters = new Map();
|
|
5
|
+
logger;
|
|
6
|
+
eventHandler = null;
|
|
7
|
+
constructor(logger) {
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
setEventHandler(fn) {
|
|
11
|
+
this.eventHandler = fn;
|
|
12
|
+
}
|
|
13
|
+
// CRC: crc-ChannelManager.md — loadAdapters()
|
|
14
|
+
loadAdapters(channels) {
|
|
15
|
+
for (const [name, config] of Object.entries(channels)) {
|
|
16
|
+
let adapter;
|
|
17
|
+
switch (name) {
|
|
18
|
+
case "cli":
|
|
19
|
+
adapter = new CLIChannelAdapter(this.logger);
|
|
20
|
+
break;
|
|
21
|
+
case "telegram":
|
|
22
|
+
adapter = TelegramChannelAdapter.fromChannelConfig(config, this.logger);
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
this.logger.warn("Unknown channel type, skipping", { name });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
this.adapters.set(name, adapter);
|
|
29
|
+
}
|
|
30
|
+
// Always ensure CLI adapter exists as fallback
|
|
31
|
+
if (!this.adapters.has("cli")) {
|
|
32
|
+
this.adapters.set("cli", new CLIChannelAdapter(this.logger));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// CRC: crc-ChannelManager.md — connectAll()
|
|
36
|
+
async connectAll() {
|
|
37
|
+
for (const [name, adapter] of this.adapters) {
|
|
38
|
+
try {
|
|
39
|
+
adapter.onMessage((event) => this.eventHandler?.(event));
|
|
40
|
+
await adapter.connect();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
this.logger.error("Failed to connect channel", { name, error: String(err) });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
this.logger.info("Channels connected", { count: this.adapters.size });
|
|
47
|
+
}
|
|
48
|
+
// CRC: crc-ChannelManager.md — disconnectAll()
|
|
49
|
+
async disconnectAll() {
|
|
50
|
+
for (const [name, adapter] of this.adapters) {
|
|
51
|
+
try {
|
|
52
|
+
await adapter.disconnect();
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
this.logger.warn("Error disconnecting channel", { name, error: String(err) });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// CRC: crc-ChannelManager.md — send()
|
|
60
|
+
async send(channel, target, message) {
|
|
61
|
+
const adapter = this.adapters.get(channel);
|
|
62
|
+
if (!adapter) {
|
|
63
|
+
throw new Error(`Unknown channel: ${channel}`);
|
|
64
|
+
}
|
|
65
|
+
await adapter.send(target, message);
|
|
66
|
+
}
|
|
67
|
+
// CRC: crc-ChannelManager.md — getAdapter()
|
|
68
|
+
getAdapter(name) {
|
|
69
|
+
return this.adapters.get(name);
|
|
70
|
+
}
|
|
71
|
+
// CRC: crc-ChannelManager.md — getConnected()
|
|
72
|
+
getConnected() {
|
|
73
|
+
return [...this.adapters.values()].filter((a) => a.getState() === "connected");
|
|
74
|
+
}
|
|
75
|
+
// CRC: crc-ChannelManager.md — healthCheck()
|
|
76
|
+
healthCheck() {
|
|
77
|
+
const results = {};
|
|
78
|
+
for (const [name, adapter] of this.adapters) {
|
|
79
|
+
results[name] = adapter.health();
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=channel-manager.js.map
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CRC: crc-CLI.md | Seq: seq-startup.md
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { Homarus } from "./homarus.js";
|
|
6
|
+
import { runSetupWizard, buildConfigFromWizard } from "./setup-wizard.js";
|
|
7
|
+
const logger = {
|
|
8
|
+
debug: (msg, meta) => { if (process.env.DEBUG)
|
|
9
|
+
console.error(`[DEBUG] ${msg}`, meta ?? ""); },
|
|
10
|
+
info: (msg, meta) => console.error(`[INFO] ${msg}`, meta ? JSON.stringify(meta) : ""),
|
|
11
|
+
warn: (msg, meta) => console.error(`[WARN] ${msg}`, meta ? JSON.stringify(meta) : ""),
|
|
12
|
+
error: (msg, meta) => console.error(`[ERROR] ${msg}`, meta ? JSON.stringify(meta) : ""),
|
|
13
|
+
};
|
|
14
|
+
const [, , command, ...args] = process.argv;
|
|
15
|
+
const noWizard = args.includes("--no-wizard");
|
|
16
|
+
async function main() {
|
|
17
|
+
switch (command) {
|
|
18
|
+
case "start":
|
|
19
|
+
return start();
|
|
20
|
+
case "init":
|
|
21
|
+
return init();
|
|
22
|
+
case "status":
|
|
23
|
+
return status();
|
|
24
|
+
case "config":
|
|
25
|
+
return configValidate();
|
|
26
|
+
case "skills":
|
|
27
|
+
return skillList();
|
|
28
|
+
case "install-daemon":
|
|
29
|
+
return installDaemon();
|
|
30
|
+
default:
|
|
31
|
+
printUsage();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// CRC: crc-CLI.md — start()
|
|
35
|
+
async function start() {
|
|
36
|
+
const loop = new Homarus(logger, args[0]);
|
|
37
|
+
// Graceful shutdown on signals
|
|
38
|
+
const shutdown = async () => {
|
|
39
|
+
logger.info("Shutdown signal received");
|
|
40
|
+
await loop.stop();
|
|
41
|
+
process.exit(0);
|
|
42
|
+
};
|
|
43
|
+
process.on("SIGINT", shutdown);
|
|
44
|
+
process.on("SIGTERM", shutdown);
|
|
45
|
+
await loop.start();
|
|
46
|
+
}
|
|
47
|
+
// CRC: crc-CLI.md — init()
|
|
48
|
+
async function init() {
|
|
49
|
+
const home = process.env.HOME ?? ".";
|
|
50
|
+
const configDir = resolve(home, ".homarus");
|
|
51
|
+
const configPath = resolve(configDir, "config.json");
|
|
52
|
+
const envPath = resolve(configDir, ".env");
|
|
53
|
+
if (existsSync(configPath)) {
|
|
54
|
+
logger.info("Config already exists", { path: configPath });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
mkdirSync(configDir, { recursive: true });
|
|
58
|
+
mkdirSync(resolve(configDir, "identity"), { recursive: true });
|
|
59
|
+
mkdirSync(resolve(configDir, "identity/overlays"), { recursive: true });
|
|
60
|
+
mkdirSync(resolve(configDir, "workspace"), { recursive: true });
|
|
61
|
+
mkdirSync(resolve(configDir, "memory"), { recursive: true });
|
|
62
|
+
mkdirSync(resolve(configDir, "skills"), { recursive: true });
|
|
63
|
+
let configObj;
|
|
64
|
+
let envLines = [];
|
|
65
|
+
if (noWizard || !process.stdin.isTTY) {
|
|
66
|
+
// Non-interactive: use defaults
|
|
67
|
+
configObj = {
|
|
68
|
+
models: {
|
|
69
|
+
default: "anthropic/claude-sonnet-4-5",
|
|
70
|
+
aliases: { smart: "anthropic/claude-opus-4-5", fast: "anthropic/claude-haiku-4-5" },
|
|
71
|
+
providers: {
|
|
72
|
+
anthropic: { apiKey: "${ANTHROPIC_API_KEY}" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
channels: { cli: {} },
|
|
76
|
+
agents: { maxConcurrent: 5, defaultTimeout: 300000, defaultMaxTurns: 20 },
|
|
77
|
+
memory: { embedding: { provider: "ollama", model: "nomic-embed-text", baseUrl: "http://127.0.0.1:11434/v1" } },
|
|
78
|
+
skills: { paths: [resolve(configDir, "skills")] },
|
|
79
|
+
server: { port: 18800 },
|
|
80
|
+
timers: { enabled: true },
|
|
81
|
+
identity: { dir: resolve(configDir, "identity"), workspaceDir: resolve(configDir, "workspace") },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Interactive wizard
|
|
86
|
+
const result = await runSetupWizard();
|
|
87
|
+
const built = buildConfigFromWizard(result, configDir);
|
|
88
|
+
configObj = built.config;
|
|
89
|
+
envLines = built.envLines;
|
|
90
|
+
}
|
|
91
|
+
writeFileSync(configPath, JSON.stringify(configObj, null, 2));
|
|
92
|
+
writeFileSync(resolve(configDir, "identity/soul.md"), "# Soul\n\nYou are a helpful AI assistant.");
|
|
93
|
+
writeFileSync(resolve(configDir, "identity/user.md"), "# User\n\nNo user profile configured.");
|
|
94
|
+
if (envLines.length > 0) {
|
|
95
|
+
writeFileSync(envPath, envLines.join("\n") + "\n", { mode: 0o600 });
|
|
96
|
+
console.log(`Secrets written to ${envPath}`);
|
|
97
|
+
}
|
|
98
|
+
logger.info("Initialized homarus", { configDir });
|
|
99
|
+
console.log(`Config created at ${configPath}`);
|
|
100
|
+
if (noWizard || !process.stdin.isTTY) {
|
|
101
|
+
console.log("Edit the config to add your API keys and customize settings.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// CRC: crc-CLI.md — status()
|
|
105
|
+
async function status() {
|
|
106
|
+
const port = args[0] ?? "18800";
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(`http://localhost:${port}/status`);
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
console.log(`Status check failed: ${response.status}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
console.log(JSON.stringify(data, null, 2));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
console.log("Event loop is not running or not reachable.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// CRC: crc-CLI.md — configValidate()
|
|
121
|
+
async function configValidate() {
|
|
122
|
+
const { Config } = await import("./config.js");
|
|
123
|
+
const config = new Config(logger, args[0]);
|
|
124
|
+
try {
|
|
125
|
+
config.load();
|
|
126
|
+
console.log("Config is valid.");
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error("Config validation failed:", String(err));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// CRC: crc-CLI.md — skillList()
|
|
134
|
+
function skillList() {
|
|
135
|
+
console.log("Use `homarus status` to see loaded skills from a running instance.");
|
|
136
|
+
}
|
|
137
|
+
// CRC: crc-CLI.md — installDaemon()
|
|
138
|
+
function installDaemon() {
|
|
139
|
+
const home = process.env.HOME ?? ".";
|
|
140
|
+
const platform = process.platform;
|
|
141
|
+
if (platform === "darwin") {
|
|
142
|
+
const plistPath = resolve(home, "Library/LaunchAgents/com.homarus.plist");
|
|
143
|
+
const nodePath = process.execPath;
|
|
144
|
+
const cliPath = resolve(import.meta.dirname ?? ".", "cli.js");
|
|
145
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
146
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
147
|
+
<plist version="1.0">
|
|
148
|
+
<dict>
|
|
149
|
+
<key>Label</key>
|
|
150
|
+
<string>com.homarus</string>
|
|
151
|
+
<key>ProgramArguments</key>
|
|
152
|
+
<array>
|
|
153
|
+
<string>${nodePath}</string>
|
|
154
|
+
<string>${cliPath}</string>
|
|
155
|
+
<string>start</string>
|
|
156
|
+
</array>
|
|
157
|
+
<key>RunAtLoad</key>
|
|
158
|
+
<true/>
|
|
159
|
+
<key>KeepAlive</key>
|
|
160
|
+
<true/>
|
|
161
|
+
<key>StandardOutPath</key>
|
|
162
|
+
<string>${home}/.homarus/stdout.log</string>
|
|
163
|
+
<key>StandardErrorPath</key>
|
|
164
|
+
<string>${home}/.homarus/stderr.log</string>
|
|
165
|
+
</dict>
|
|
166
|
+
</plist>`;
|
|
167
|
+
writeFileSync(plistPath, plist);
|
|
168
|
+
console.log(`LaunchAgent plist written to ${plistPath}`);
|
|
169
|
+
console.log("Run: launchctl load " + plistPath);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// systemd
|
|
173
|
+
const servicePath = resolve(home, ".config/systemd/user/homarus.service");
|
|
174
|
+
const serviceDir = resolve(home, ".config/systemd/user");
|
|
175
|
+
mkdirSync(serviceDir, { recursive: true });
|
|
176
|
+
const nodePath = process.execPath;
|
|
177
|
+
const cliPath = resolve(import.meta.dirname ?? ".", "cli.js");
|
|
178
|
+
const unit = `[Unit]
|
|
179
|
+
Description=Homarus Agent
|
|
180
|
+
After=network.target
|
|
181
|
+
|
|
182
|
+
[Service]
|
|
183
|
+
ExecStart=${nodePath} ${cliPath} start
|
|
184
|
+
Restart=on-failure
|
|
185
|
+
RestartSec=5
|
|
186
|
+
KillMode=control-group
|
|
187
|
+
|
|
188
|
+
[Install]
|
|
189
|
+
WantedBy=default.target
|
|
190
|
+
`;
|
|
191
|
+
writeFileSync(servicePath, unit);
|
|
192
|
+
console.log(`Systemd service written to ${servicePath}`);
|
|
193
|
+
console.log("Run: systemctl --user daemon-reload && systemctl --user enable --now homarus");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function printUsage() {
|
|
197
|
+
console.log(`homarus - Event-driven AI agent coordinator
|
|
198
|
+
|
|
199
|
+
Usage:
|
|
200
|
+
homarus start [config-path] Start the event loop (foreground)
|
|
201
|
+
homarus init [--no-wizard] Create config via interactive wizard (or defaults)
|
|
202
|
+
homarus status [port] Show status of running instance
|
|
203
|
+
homarus config [config-path] Validate config file
|
|
204
|
+
homarus skills List loaded skills
|
|
205
|
+
homarus install-daemon Install systemd/launchd service
|
|
206
|
+
`);
|
|
207
|
+
}
|
|
208
|
+
main().catch((err) => {
|
|
209
|
+
logger.error("Fatal error", { error: String(err) });
|
|
210
|
+
process.exit(1);
|
|
211
|
+
});
|
|
212
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ConfigData, Logger } from "./types.js";
|
|
2
|
+
export declare class Config {
|
|
3
|
+
private data;
|
|
4
|
+
private configPath;
|
|
5
|
+
private watching;
|
|
6
|
+
private logger;
|
|
7
|
+
constructor(logger: Logger, configPath?: string);
|
|
8
|
+
load(path?: string): ConfigData;
|
|
9
|
+
get<T = unknown>(key: string): T | undefined;
|
|
10
|
+
getSection<T = unknown>(section: string): T;
|
|
11
|
+
getAll(): ConfigData;
|
|
12
|
+
private resolveEnvVars;
|
|
13
|
+
private loadEnvFile;
|
|
14
|
+
startWatching(onChange: (safeChange: boolean) => void): void;
|
|
15
|
+
stopWatching(): void;
|
|
16
|
+
private isSafeChange;
|
|
17
|
+
private resolveConfigPath;
|
|
18
|
+
private merge;
|
|
19
|
+
private flatten;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=config.d.ts.map
|