@johpaz/hive-sdk 0.0.12 → 0.0.15
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/.github/CODEOWNERS +9 -0
- package/.github/workflows/publish.yml +89 -0
- package/.github/workflows/version-bump.yml +102 -0
- package/CHANGELOG.md +38 -0
- package/README.md +158 -0
- package/bun.lock +543 -0
- package/bunfig.toml +7 -0
- package/docs/API-AGENTS.md +316 -0
- package/docs/API-CONTEXT-COMPILER.md +252 -0
- package/docs/API-DAG-SCHEDULER.md +273 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
- package/docs/API-WORKERS-EVENTS.md +152 -0
- package/docs/INDEX.md +141 -0
- package/docs/README.md +68 -0
- package/package.json +54 -105
- package/packages/cli/package.json +17 -0
- package/packages/cli/src/commands/init.ts +56 -0
- package/packages/cli/src/commands/run.ts +45 -0
- package/packages/cli/src/commands/test.ts +42 -0
- package/packages/cli/src/commands/trace.ts +55 -0
- package/packages/cli/src/index.ts +43 -0
- package/packages/core/package.json +58 -0
- package/packages/core/src/ace/Curator.ts +158 -0
- package/packages/core/src/ace/Reflector.ts +200 -0
- package/packages/core/src/ace/Tracer.ts +100 -0
- package/packages/core/src/ace/index.ts +4 -0
- package/packages/core/src/agent/AgentRunner.ts +699 -0
- package/packages/core/src/agent/Compaction.ts +221 -0
- package/packages/core/src/agent/ContextCompiler.ts +567 -0
- package/packages/core/src/agent/ContextGuard.ts +91 -0
- package/packages/core/src/agent/ConversationStore.ts +244 -0
- package/packages/core/src/agent/Hooks.ts +166 -0
- package/packages/core/src/agent/NativeTools.ts +31 -0
- package/packages/core/src/agent/PromptBuilder.ts +169 -0
- package/packages/core/src/agent/Service.ts +267 -0
- package/packages/core/src/agent/StuckLoop.ts +133 -0
- package/packages/core/src/agent/index.ts +12 -0
- package/packages/core/src/agent/providers/LLMClient.ts +149 -0
- package/packages/core/src/agent/providers/anthropic.ts +212 -0
- package/packages/core/src/agent/providers/gemini.ts +215 -0
- package/packages/core/src/agent/providers/index.ts +199 -0
- package/packages/core/src/agent/providers/interface.ts +195 -0
- package/packages/core/src/agent/providers/ollama.ts +175 -0
- package/packages/core/src/agent/providers/openai-compat.ts +231 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
- package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
- package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
- package/packages/core/src/agent/selectors/index.ts +6 -0
- package/packages/core/src/api/createAgent.test.ts +48 -0
- package/packages/core/src/api/createAgent.ts +122 -0
- package/packages/core/src/api/index.ts +2 -0
- package/packages/core/src/canvas/CanvasManager.ts +390 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +6 -0
- package/packages/core/src/config/index.ts +2 -0
- package/packages/core/src/config/loader.ts +554 -0
- package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
- package/packages/core/src/ethics/EthicsGuard.ts +66 -0
- package/packages/core/src/ethics/index.ts +2 -0
- package/packages/core/src/gateway/channel-notify.test.ts +14 -0
- package/packages/core/src/gateway/channel-notify.ts +12 -0
- package/packages/core/src/gateway/index.ts +1 -0
- package/packages/core/src/index.ts +37 -0
- package/packages/core/src/mcp/MCPClient.ts +439 -0
- package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
- package/packages/core/src/mcp/config.ts +13 -0
- package/packages/core/src/mcp/hot-reload.ts +147 -0
- package/packages/core/src/mcp/index.ts +11 -0
- package/packages/core/src/mcp/logger.ts +42 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/transports/index.ts +67 -0
- package/packages/core/src/mcp/transports/sse.ts +241 -0
- package/packages/core/src/mcp/transports/websocket.ts +159 -0
- package/packages/core/src/memory/Scratchpad.test.ts +47 -0
- package/packages/core/src/memory/Scratchpad.ts +37 -0
- package/packages/core/src/memory/Storage.ts +6 -0
- package/packages/core/src/memory/index.ts +2 -0
- package/packages/core/src/multimodal/VisionService.ts +293 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/security/Pairing.ts +250 -0
- package/packages/core/src/security/RateLimit.ts +270 -0
- package/packages/core/src/security/index.ts +4 -0
- package/packages/core/src/skills/SkillLoader.ts +388 -0
- package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
- package/packages/core/src/skills/defineSkill.ts +18 -0
- package/packages/core/src/skills/index.ts +4 -0
- package/packages/core/src/state/index.ts +2 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/SQLiteStorage.ts +407 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/onboarding.ts +1603 -0
- package/packages/core/src/storage/schema.ts +689 -0
- package/packages/core/src/storage/seed.ts +740 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/swarm/AgentBus.ts +460 -0
- package/packages/core/src/swarm/AgentExecutor.ts +53 -0
- package/packages/core/src/swarm/Coordinator.ts +251 -0
- package/packages/core/src/swarm/EventBridge.ts +122 -0
- package/packages/core/src/swarm/EventBus.ts +169 -0
- package/packages/core/src/swarm/TaskGraph.ts +192 -0
- package/packages/core/src/swarm/TaskNode.ts +97 -0
- package/packages/core/src/swarm/TaskResult.ts +22 -0
- package/packages/core/src/swarm/WorkerPool.ts +236 -0
- package/packages/core/src/swarm/errors.ts +37 -0
- package/packages/core/src/swarm/index.ts +30 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
- package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/swarm/presets/index.ts +4 -0
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/swarm/strategies/index.ts +3 -0
- package/packages/core/src/swarm/types.ts +164 -0
- package/packages/core/src/tools/ToolExecutor.ts +58 -0
- package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
- package/packages/core/src/tools/ToolRegistry.ts +61 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +715 -0
- package/packages/core/src/tools/bridge-events.ts +26 -0
- package/packages/core/src/tools/canvas/index.ts +375 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/codebridge/index.ts +342 -0
- package/packages/core/src/tools/core/index.ts +476 -0
- package/packages/core/src/tools/cron/index.ts +626 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +231 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/projects/index.ts +37 -0
- package/packages/core/src/tools/projects/project-create.ts +94 -0
- package/packages/core/src/tools/projects/project-done.ts +66 -0
- package/packages/core/src/tools/projects/project-fail.ts +66 -0
- package/packages/core/src/tools/projects/project-list.ts +96 -0
- package/packages/core/src/tools/projects/project-update.ts +72 -0
- package/packages/core/src/tools/projects/task-create.ts +68 -0
- package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
- package/packages/core/src/tools/projects/task-update.ts +93 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +10 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +656 -0
- package/test/setup-db.ts +216 -0
- package/tsconfig.json +39 -0
- package/src/agents.ts +0 -1
- package/src/canvas.ts +0 -1
- package/src/channels.ts +0 -1
- package/src/config.ts +0 -1
- package/src/events.ts +0 -1
- package/src/gateway.ts +0 -1
- package/src/index.ts +0 -304
- package/src/mcp.ts +0 -1
- package/src/multimodal.ts +0 -1
- package/src/scheduler.ts +0 -1
- package/src/security.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/state.ts +0 -1
- package/src/storage.ts +0 -1
- package/src/tools.ts +0 -1
- package/src/tts.ts +0 -1
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -1
- package/src/voice.ts +0 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const LogLevelSchema = z.enum(["debug", "info", "warn", "error"]);
|
|
6
|
+
const DMPolicySchema = z.enum(["open", "pairing", "allowlist"]);
|
|
7
|
+
const TransportSchema = z.enum(["stdio", "sse", "websocket"]);
|
|
8
|
+
|
|
9
|
+
export function loadEnv(hiveDir: string): void {
|
|
10
|
+
const envPath = path.join(hiveDir, ".env");
|
|
11
|
+
if (existsSync(envPath)) {
|
|
12
|
+
try {
|
|
13
|
+
const text = readFileSync(envPath, "utf8");
|
|
14
|
+
const lines = text.split("\n");
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
18
|
+
|
|
19
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
20
|
+
if (key && valueParts.length > 0) {
|
|
21
|
+
const value = valueParts.join("=").trim().replace(/^['"]|['"]$/g, "");
|
|
22
|
+
process.env[key.trim()] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// Ignore errors loading .env
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getHiveDir(): string {
|
|
32
|
+
// Priority 1: HIVE_HOME explicitly set
|
|
33
|
+
if (process.env.HIVE_HOME) {
|
|
34
|
+
const hiveDir = process.env.HIVE_HOME.startsWith("~")
|
|
35
|
+
? path.join(process.env.HOME || "", process.env.HIVE_HOME.slice(1))
|
|
36
|
+
: process.env.HIVE_HOME;
|
|
37
|
+
loadEnv(hiveDir);
|
|
38
|
+
return hiveDir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Priority 2: HIVE_DEV mode defaults (Local folder)
|
|
42
|
+
// Only check process.env.HIVE_DEV directly - don't load from .env files
|
|
43
|
+
// This ensures production mode is the default unless explicitly set
|
|
44
|
+
if (process.env.HIVE_DEV === "1" || process.env.HIVE_DEV === "true") {
|
|
45
|
+
const localDir = path.join(process.cwd(), ".hive-dev");
|
|
46
|
+
loadEnv(localDir);
|
|
47
|
+
return localDir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Priority 3: Default ~/.hive
|
|
51
|
+
const defaultDir = path.join(process.env.HOME || "", ".hive");
|
|
52
|
+
loadEnv(defaultDir);
|
|
53
|
+
return defaultDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const expandPath = (p: string): string => {
|
|
57
|
+
if (p.startsWith("~/.hive")) {
|
|
58
|
+
const hiveDir = getHiveDir();
|
|
59
|
+
return p.replace("~/.hive", hiveDir);
|
|
60
|
+
}
|
|
61
|
+
if (p.startsWith("~")) {
|
|
62
|
+
return path.join(process.env.HOME || "", p.slice(1));
|
|
63
|
+
}
|
|
64
|
+
return p;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const expandEnvVars = (value: string): string => {
|
|
68
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, key) => {
|
|
69
|
+
return process.env[key] || "";
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const expandEnvInObject = <T>(obj: T): T => {
|
|
74
|
+
if (typeof obj === "string") {
|
|
75
|
+
return expandEnvVars(obj) as T;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(obj)) {
|
|
78
|
+
return obj.map(expandEnvInObject) as T;
|
|
79
|
+
}
|
|
80
|
+
if (obj !== null && typeof obj === "object") {
|
|
81
|
+
const result: Record<string, unknown> = {};
|
|
82
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
83
|
+
result[key] = expandEnvInObject(value);
|
|
84
|
+
}
|
|
85
|
+
return result as T;
|
|
86
|
+
}
|
|
87
|
+
return obj;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const ProviderConfigSchema = z.object({
|
|
91
|
+
apiKey: z.string().optional(),
|
|
92
|
+
baseUrl: z.string().optional(),
|
|
93
|
+
rateLimit: z.number().optional(),
|
|
94
|
+
retries: z.number().optional(),
|
|
95
|
+
retryDelayMs: z.number().optional(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const ToolRestrictionsSchema = z.object({
|
|
99
|
+
allow: z.array(z.string()).optional(),
|
|
100
|
+
deny: z.array(z.string()).optional(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const ExecConfigSchema = z.object({
|
|
104
|
+
enabled: z.boolean().optional(),
|
|
105
|
+
allowlist: z.array(z.string()).optional(),
|
|
106
|
+
denylist: z.array(z.string()).optional(),
|
|
107
|
+
timeoutSeconds: z.number().optional(),
|
|
108
|
+
workDir: z.string().optional(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const WebConfigSchema = z.object({
|
|
112
|
+
allowlist: z.array(z.string()).optional(),
|
|
113
|
+
denylist: z.array(z.string()).optional(),
|
|
114
|
+
timeoutSeconds: z.number().optional(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const BrowserConfigSchema = z.object({
|
|
118
|
+
enabled: z.boolean().optional(),
|
|
119
|
+
cdpUrl: z.string().optional(),
|
|
120
|
+
headless: z.boolean().optional(),
|
|
121
|
+
timeoutMs: z.number().optional(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const CanvasConfigSchema = z.object({
|
|
125
|
+
enabled: z.boolean().optional(),
|
|
126
|
+
port: z.number().optional(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const SandboxConfigSchema = z.object({
|
|
130
|
+
dm: ToolRestrictionsSchema.optional(),
|
|
131
|
+
group: ToolRestrictionsSchema.optional(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const ToolsConfigSchema = z.object({
|
|
135
|
+
allow: z.array(z.string()).optional(),
|
|
136
|
+
deny: z.array(z.string()).optional(),
|
|
137
|
+
exec: ExecConfigSchema.optional(),
|
|
138
|
+
web: WebConfigSchema.optional(),
|
|
139
|
+
browser: BrowserConfigSchema.optional(),
|
|
140
|
+
canvas: CanvasConfigSchema.optional(),
|
|
141
|
+
sandbox: SandboxConfigSchema.optional(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const ContextConfigSchema = z.object({
|
|
145
|
+
maxTokens: z.number().optional(),
|
|
146
|
+
compactionThreshold: z.number().optional(),
|
|
147
|
+
minMessagesAfterCompaction: z.number().optional(),
|
|
148
|
+
maxCompactionRetries: z.number().optional(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const AgentEntrySchema = z.object({
|
|
152
|
+
id: z.string(),
|
|
153
|
+
default: z.boolean().optional(),
|
|
154
|
+
workspace: z.string(),
|
|
155
|
+
description: z.string().optional(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const AccountConfigSchema = z.object({
|
|
159
|
+
botToken: z.string().optional(),
|
|
160
|
+
applicationId: z.string().optional(),
|
|
161
|
+
appToken: z.string().optional(),
|
|
162
|
+
signingSecret: z.string().optional(),
|
|
163
|
+
dmPolicy: DMPolicySchema.optional(),
|
|
164
|
+
allowFrom: z.array(z.string()).optional(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const ChannelConfigSchema = z.object({
|
|
168
|
+
enabled: z.boolean().optional(),
|
|
169
|
+
accounts: z.record(z.string(), AccountConfigSchema).optional(),
|
|
170
|
+
dmPolicy: DMPolicySchema.optional(),
|
|
171
|
+
allowFrom: z.array(z.string()).optional(),
|
|
172
|
+
groups: z.boolean().optional(),
|
|
173
|
+
guilds: z.record(z.string(), z.unknown()).optional(),
|
|
174
|
+
experimental: z.boolean().optional(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const PeerMatchSchema = z.object({
|
|
178
|
+
kind: z.enum(["direct", "group"]).optional(),
|
|
179
|
+
id: z.string().optional(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const BindingMatchSchema = z.object({
|
|
183
|
+
channel: z.string().optional(),
|
|
184
|
+
accountId: z.string().optional(),
|
|
185
|
+
peer: PeerMatchSchema.optional(),
|
|
186
|
+
guildId: z.string().optional(),
|
|
187
|
+
teamId: z.string().optional(),
|
|
188
|
+
roles: z.array(z.string()).optional(),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const BindingSchema = z.object({
|
|
192
|
+
agentId: z.string(),
|
|
193
|
+
match: BindingMatchSchema,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const MCPServerConfigSchema = z.object({
|
|
197
|
+
enabled: z.boolean().optional(),
|
|
198
|
+
transport: TransportSchema,
|
|
199
|
+
command: z.string().optional(),
|
|
200
|
+
args: z.array(z.string()).optional(),
|
|
201
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
202
|
+
url: z.string().optional(),
|
|
203
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
204
|
+
reconnect: z.object({
|
|
205
|
+
enabled: z.boolean().optional(),
|
|
206
|
+
maxRetries: z.number().optional(),
|
|
207
|
+
delayMs: z.number().optional(),
|
|
208
|
+
backoffMultiplier: z.number().optional(),
|
|
209
|
+
}).optional(),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const MCPConfigSchema = z.object({
|
|
213
|
+
enabled: z.boolean().optional(),
|
|
214
|
+
servers: z.record(z.string(), MCPServerConfigSchema).optional(),
|
|
215
|
+
healthCheck: z.object({
|
|
216
|
+
enabled: z.boolean().optional(),
|
|
217
|
+
intervalSeconds: z.number().optional(),
|
|
218
|
+
}).optional(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const EpisodicMemoryConfigSchema = z.object({
|
|
222
|
+
enabled: z.boolean().optional(),
|
|
223
|
+
provider: z.enum(["openai", "local"]).optional(),
|
|
224
|
+
maxEpisodesPerSession: z.number().optional(),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const MemoryConfigSchema = z.object({
|
|
228
|
+
dbPath: z.string().optional(),
|
|
229
|
+
notesDir: z.string().optional(),
|
|
230
|
+
episodic: EpisodicMemoryConfigSchema.optional(),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const CronConfigSchema = z.object({
|
|
234
|
+
enabled: z.boolean().optional(),
|
|
235
|
+
dbPath: z.string().optional(),
|
|
236
|
+
maxConcurrentJobs: z.number().optional(),
|
|
237
|
+
timezone: z.string().optional(),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const RetryConfigSchema = z.object({
|
|
241
|
+
maxAttempts: z.number().optional(),
|
|
242
|
+
initialDelayMs: z.number().optional(),
|
|
243
|
+
backoffMultiplier: z.number().optional(),
|
|
244
|
+
maxDelayMs: z.number().optional(),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const HooksConfigSchema = z.object({
|
|
248
|
+
scripts: z.object({
|
|
249
|
+
before_model_resolve: z.string().optional(),
|
|
250
|
+
before_prompt_build: z.string().optional(),
|
|
251
|
+
before_tool_call: z.string().optional(),
|
|
252
|
+
after_tool_call: z.string().optional(),
|
|
253
|
+
tool_result_persist: z.string().optional(),
|
|
254
|
+
before_compaction: z.string().optional(),
|
|
255
|
+
after_compaction: z.string().optional(),
|
|
256
|
+
message_received: z.string().optional(),
|
|
257
|
+
message_sending: z.string().optional(),
|
|
258
|
+
message_sent: z.string().optional(),
|
|
259
|
+
session_start: z.string().optional(),
|
|
260
|
+
session_end: z.string().optional(),
|
|
261
|
+
gateway_start: z.string().optional(),
|
|
262
|
+
gateway_stop: z.string().optional(),
|
|
263
|
+
}).optional(),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const LoggingConfigSchema = z.object({
|
|
267
|
+
level: LogLevelSchema.optional(),
|
|
268
|
+
dir: z.string().optional(),
|
|
269
|
+
maxSizeMB: z.number().optional(),
|
|
270
|
+
maxFiles: z.number().optional(),
|
|
271
|
+
redactSensitive: z.boolean().optional(),
|
|
272
|
+
console: z.boolean().optional(),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const GatewayConfigSchema = z.object({
|
|
276
|
+
host: z.string().optional(),
|
|
277
|
+
port: z.number().optional(),
|
|
278
|
+
authToken: z.string().optional(),
|
|
279
|
+
pidFile: z.string().optional(),
|
|
280
|
+
tools: ToolRestrictionsSchema.optional(),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const ModelsConfigSchema = z.object({
|
|
284
|
+
defaultProvider: z.enum(["openai", "anthropic", "gemini", "mistral", "kimi", "ollama", "openrouter", "deepseek"]).optional(),
|
|
285
|
+
defaults: z.record(z.string(), z.string()).optional(),
|
|
286
|
+
providers: z.record(z.string(), ProviderConfigSchema).optional(),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const SessionsConfigSchema = z.object({
|
|
290
|
+
dir: z.string().optional(),
|
|
291
|
+
pruneAfterHours: z.number().optional(),
|
|
292
|
+
maxTranscriptSizeMB: z.number().optional(),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const SkillsConfigSchema = z.object({
|
|
296
|
+
allowBundled: z.array(z.string()).optional(),
|
|
297
|
+
managedDir: z.string().optional(),
|
|
298
|
+
extraDirs: z.array(z.string()).optional(),
|
|
299
|
+
hotReload: z.boolean().optional(),
|
|
300
|
+
maxSkillSizeKB: z.number().optional(),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const SecurityConfigSchema = z.object({
|
|
304
|
+
maxMessageLength: z.record(z.string(), z.number()).optional(),
|
|
305
|
+
skillScanning: z.boolean().optional(),
|
|
306
|
+
warnOnInsecureConfig: z.boolean().optional(),
|
|
307
|
+
allowedUsers: z.array(z.string()).optional(),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const CaptchaConfigSchema = z.object({
|
|
311
|
+
enabled: z.boolean().optional(),
|
|
312
|
+
autoSolve: z.boolean().optional(),
|
|
313
|
+
visionProvider: z.enum(["gemini", "openai", "anthropic"]).optional(),
|
|
314
|
+
visionModel: z.string().optional(),
|
|
315
|
+
maxAttempts: z.number().optional(),
|
|
316
|
+
maxRounds: z.number().optional(),
|
|
317
|
+
apiKey: z.string().optional(),
|
|
318
|
+
enabledSites: z.array(z.string()).optional(),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const UserConfigSchema = z.object({
|
|
322
|
+
id: z.string(),
|
|
323
|
+
name: z.string(),
|
|
324
|
+
channels: z.record(z.string(), z.string()).optional(),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const ConfigSchema = z.object({
|
|
328
|
+
gateway: GatewayConfigSchema.optional(),
|
|
329
|
+
logging: LoggingConfigSchema.optional(),
|
|
330
|
+
user: UserConfigSchema.optional(),
|
|
331
|
+
agent: z.object({
|
|
332
|
+
defaultAgentId: z.string().optional(),
|
|
333
|
+
baseDir: z.string().optional(),
|
|
334
|
+
context: ContextConfigSchema.optional(),
|
|
335
|
+
}).optional(),
|
|
336
|
+
models: ModelsConfigSchema.optional(),
|
|
337
|
+
sessions: SessionsConfigSchema.optional(),
|
|
338
|
+
agents: z.object({
|
|
339
|
+
list: z.array(AgentEntrySchema).optional(),
|
|
340
|
+
}).optional(),
|
|
341
|
+
bindings: z.array(BindingSchema).optional(),
|
|
342
|
+
channels: z.record(z.string(), ChannelConfigSchema).optional(),
|
|
343
|
+
tools: ToolsConfigSchema.optional(),
|
|
344
|
+
skills: SkillsConfigSchema.optional(),
|
|
345
|
+
mcp: MCPConfigSchema.optional(),
|
|
346
|
+
memory: MemoryConfigSchema.optional(),
|
|
347
|
+
cron: CronConfigSchema.optional(),
|
|
348
|
+
retry: RetryConfigSchema.optional(),
|
|
349
|
+
security: SecurityConfigSchema.optional(),
|
|
350
|
+
hooks: HooksConfigSchema.optional(),
|
|
351
|
+
captcha: CaptchaConfigSchema.optional(),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
355
|
+
|
|
356
|
+
export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
|
|
357
|
+
export type MCPServerConfig = z.infer<typeof MCPServerConfigSchema>;
|
|
358
|
+
export type AgentEntry = z.infer<typeof AgentEntrySchema>;
|
|
359
|
+
export type Binding = z.infer<typeof BindingSchema>;
|
|
360
|
+
export type UserConfig = z.infer<typeof UserConfigSchema>;
|
|
361
|
+
export type CaptchaConfig = z.infer<typeof CaptchaConfigSchema>;
|
|
362
|
+
|
|
363
|
+
function buildDefaultConfig(): Config {
|
|
364
|
+
const hiveDir = getHiveDir();
|
|
365
|
+
return {
|
|
366
|
+
gateway: {
|
|
367
|
+
host: process.env.HIVE_HOST || "127.0.0.1",
|
|
368
|
+
port: parseInt(process.env.HIVE_PORT || "18790", 10),
|
|
369
|
+
pidFile: path.join(hiveDir, "gateway.pid"),
|
|
370
|
+
authToken: process.env.HIVE_AUTH_TOKEN || undefined,
|
|
371
|
+
tools: {
|
|
372
|
+
allow: ["*"],
|
|
373
|
+
deny: [],
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
logging: {
|
|
377
|
+
level: (process.env.HIVE_LOG_LEVEL as any) || "info",
|
|
378
|
+
dir: path.join(hiveDir, "logs"),
|
|
379
|
+
maxSizeMB: 10,
|
|
380
|
+
maxFiles: 5,
|
|
381
|
+
redactSensitive: true,
|
|
382
|
+
console: true,
|
|
383
|
+
},
|
|
384
|
+
agent: {
|
|
385
|
+
defaultAgentId: "main",
|
|
386
|
+
baseDir: path.join(hiveDir, "agents"),
|
|
387
|
+
context: {
|
|
388
|
+
maxTokens: 0,
|
|
389
|
+
compactionThreshold: 0.8,
|
|
390
|
+
minMessagesAfterCompaction: 4,
|
|
391
|
+
maxCompactionRetries: 3,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
models: {
|
|
395
|
+
defaultProvider: "openai",
|
|
396
|
+
defaults: {
|
|
397
|
+
openai: "gpt-4o",
|
|
398
|
+
anthropic: "claude-sonnet-4-20250514",
|
|
399
|
+
ollama: "llama3.2",
|
|
400
|
+
openrouter: "anthropic/claude-sonnet-4",
|
|
401
|
+
},
|
|
402
|
+
providers: {},
|
|
403
|
+
},
|
|
404
|
+
sessions: {
|
|
405
|
+
dir: path.join(hiveDir, "sessions"),
|
|
406
|
+
pruneAfterHours: 24,
|
|
407
|
+
maxTranscriptSizeMB: 50,
|
|
408
|
+
},
|
|
409
|
+
agents: {
|
|
410
|
+
list: [
|
|
411
|
+
{
|
|
412
|
+
id: "main",
|
|
413
|
+
default: true,
|
|
414
|
+
workspace: path.join(hiveDir, "agents", "main", "workspace"),
|
|
415
|
+
description: "Default personal assistant",
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
bindings: [],
|
|
420
|
+
channels: {
|
|
421
|
+
webchat: { enabled: true },
|
|
422
|
+
},
|
|
423
|
+
tools: {
|
|
424
|
+
allow: ["*"],
|
|
425
|
+
deny: [],
|
|
426
|
+
exec: {
|
|
427
|
+
enabled: true,
|
|
428
|
+
allowlist: [],
|
|
429
|
+
denylist: ["rm -rf /", "sudo", "chmod 777", "> /dev/", "mkfs"],
|
|
430
|
+
timeoutSeconds: 30,
|
|
431
|
+
workDir: path.join(process.env.HOME || "", "exec"), // Points to home for exec by default
|
|
432
|
+
},
|
|
433
|
+
web: {
|
|
434
|
+
allowlist: [],
|
|
435
|
+
denylist: ["file://", "ftp://"],
|
|
436
|
+
timeoutSeconds: 30,
|
|
437
|
+
},
|
|
438
|
+
browser: {
|
|
439
|
+
enabled: true,
|
|
440
|
+
cdpUrl: "ws://127.0.0.1:9222",
|
|
441
|
+
headless: true,
|
|
442
|
+
timeoutMs: 30000,
|
|
443
|
+
},
|
|
444
|
+
canvas: {
|
|
445
|
+
enabled: true,
|
|
446
|
+
port: 18793,
|
|
447
|
+
},
|
|
448
|
+
sandbox: {
|
|
449
|
+
dm: { allow: ["*"], deny: [] },
|
|
450
|
+
group: { allow: ["*"], deny: [] },
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
skills: {
|
|
454
|
+
allowBundled: [],
|
|
455
|
+
managedDir: path.join(hiveDir, "skills"),
|
|
456
|
+
extraDirs: [],
|
|
457
|
+
hotReload: true,
|
|
458
|
+
maxSkillSizeKB: 100,
|
|
459
|
+
},
|
|
460
|
+
mcp: {
|
|
461
|
+
enabled: true,
|
|
462
|
+
servers: {},
|
|
463
|
+
healthCheck: {
|
|
464
|
+
enabled: true,
|
|
465
|
+
intervalSeconds: 60,
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
memory: {
|
|
469
|
+
dbPath: path.join(hiveDir, "memory.db"),
|
|
470
|
+
notesDir: path.join(hiveDir, "agents", "main", "workspace", "memory"),
|
|
471
|
+
episodic: {
|
|
472
|
+
enabled: false,
|
|
473
|
+
provider: "openai",
|
|
474
|
+
maxEpisodesPerSession: 100,
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
cron: {
|
|
478
|
+
enabled: true,
|
|
479
|
+
dbPath: path.join(hiveDir, "cron.db"),
|
|
480
|
+
maxConcurrentJobs: 5,
|
|
481
|
+
timezone: "UTC",
|
|
482
|
+
},
|
|
483
|
+
retry: {
|
|
484
|
+
maxAttempts: 3,
|
|
485
|
+
initialDelayMs: 1000,
|
|
486
|
+
backoffMultiplier: 2,
|
|
487
|
+
maxDelayMs: 30000,
|
|
488
|
+
},
|
|
489
|
+
security: {
|
|
490
|
+
maxMessageLength: {
|
|
491
|
+
telegram: 4096,
|
|
492
|
+
discord: 2000,
|
|
493
|
+
slack: 40000,
|
|
494
|
+
webchat: 100000,
|
|
495
|
+
whatsapp: 65536,
|
|
496
|
+
},
|
|
497
|
+
skillScanning: true,
|
|
498
|
+
warnOnInsecureConfig: true,
|
|
499
|
+
},
|
|
500
|
+
hooks: {
|
|
501
|
+
scripts: {},
|
|
502
|
+
},
|
|
503
|
+
captcha: {
|
|
504
|
+
enabled: false,
|
|
505
|
+
autoSolve: true,
|
|
506
|
+
visionProvider: 'gemini',
|
|
507
|
+
visionModel: 'gemini-2.0-flash-exp',
|
|
508
|
+
maxAttempts: 3,
|
|
509
|
+
maxRounds: 5,
|
|
510
|
+
enabledSites: [],
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
// deepMerge kept for potential future use
|
|
517
|
+
function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
|
|
518
|
+
const result = { ...target };
|
|
519
|
+
|
|
520
|
+
for (const key of Object.keys(source) as (keyof T)[]) {
|
|
521
|
+
const sourceValue = source[key];
|
|
522
|
+
const targetValue = result[key];
|
|
523
|
+
|
|
524
|
+
if (
|
|
525
|
+
sourceValue !== undefined &&
|
|
526
|
+
sourceValue !== null &&
|
|
527
|
+
typeof sourceValue === "object" &&
|
|
528
|
+
!Array.isArray(sourceValue) &&
|
|
529
|
+
targetValue !== undefined &&
|
|
530
|
+
targetValue !== null &&
|
|
531
|
+
typeof targetValue === "object" &&
|
|
532
|
+
!Array.isArray(targetValue)
|
|
533
|
+
) {
|
|
534
|
+
result[key] = deepMerge(
|
|
535
|
+
targetValue as Record<string, unknown>,
|
|
536
|
+
sourceValue as Record<string, unknown>
|
|
537
|
+
) as T[keyof T];
|
|
538
|
+
} else if (sourceValue !== undefined) {
|
|
539
|
+
result[key] = sourceValue as T[keyof T];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
export function loadConfig(): Config {
|
|
546
|
+
return buildDefaultConfig();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function expandConfigPath(p: string | undefined): string | undefined {
|
|
550
|
+
if (!p) return undefined;
|
|
551
|
+
return expandPath(p);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export { expandPath };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { EthicsGuard } from "./EthicsGuard.ts";
|
|
3
|
+
import { getDb, initializeDatabase, dbService } from "../storage/SQLiteStorage.ts";
|
|
4
|
+
|
|
5
|
+
describe("EthicsGuard", () => {
|
|
6
|
+
let db: any;
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
await initializeDatabase();
|
|
10
|
+
db = getDb();
|
|
11
|
+
db.run(`
|
|
12
|
+
INSERT OR IGNORE INTO playbook (id, rule, category, applicable_to, helpful_count, active)
|
|
13
|
+
VALUES (1, 'Siempre verificar fuentes antes de responder', 'response_quality', 'agent', 5, 1)
|
|
14
|
+
`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
dbService.close();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("loads rules from DB", () => {
|
|
22
|
+
const guard = new EthicsGuard(db);
|
|
23
|
+
const rules = guard.getRules();
|
|
24
|
+
expect(Array.isArray(rules)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("injectIntoPrompt appends rules to system prompt", () => {
|
|
28
|
+
const guard = new EthicsGuard(db);
|
|
29
|
+
const rules = guard.getRules();
|
|
30
|
+
const result = guard.injectIntoPrompt("Eres un asistente.", rules);
|
|
31
|
+
expect(result).toContain("Eres un asistente.");
|
|
32
|
+
if (rules.length > 0) {
|
|
33
|
+
expect(result).toContain("Calidad de Respuesta");
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("hasEthicsLayer detects response quality rules", () => {
|
|
38
|
+
const guard = new EthicsGuard(db);
|
|
39
|
+
const has = guard.hasEthicsLayer();
|
|
40
|
+
expect(typeof has).toBe("boolean");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("getRules accepts optional agentRole for FTS5 search", () => {
|
|
44
|
+
const guard = new EthicsGuard(db);
|
|
45
|
+
const rules = guard.getRules("agent");
|
|
46
|
+
expect(Array.isArray(rules)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("getRules without agentRole returns all rules", () => {
|
|
50
|
+
const guard = new EthicsGuard(db);
|
|
51
|
+
const rules = guard.getRules();
|
|
52
|
+
expect(Array.isArray(rules)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface EthicsRule {
|
|
2
|
+
id: number;
|
|
3
|
+
rule: string;
|
|
4
|
+
category: string;
|
|
5
|
+
applicable_to: string;
|
|
6
|
+
helpful_count: number;
|
|
7
|
+
active: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class EthicsGuard {
|
|
11
|
+
private db: any;
|
|
12
|
+
|
|
13
|
+
constructor(db: any) {
|
|
14
|
+
this.db = db;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getRules(agentRole?: string): EthicsRule[] {
|
|
18
|
+
if (!agentRole) {
|
|
19
|
+
return this.db
|
|
20
|
+
.query(
|
|
21
|
+
`SELECT p.* FROM playbook p
|
|
22
|
+
WHERE p.category = 'response_quality' AND p.active = 1
|
|
23
|
+
ORDER BY p.helpful_count DESC`
|
|
24
|
+
)
|
|
25
|
+
.all() as EthicsRule[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const ftsRows = this.db
|
|
30
|
+
.query(
|
|
31
|
+
`SELECT p.* FROM playbook p
|
|
32
|
+
JOIN playbook_fts fts ON p.rowid = fts.rowid
|
|
33
|
+
WHERE fts.playbook_fts MATCH ?
|
|
34
|
+
AND p.category = 'response_quality' AND p.active = 1
|
|
35
|
+
ORDER BY p.helpful_count DESC`,
|
|
36
|
+
[agentRole]
|
|
37
|
+
)
|
|
38
|
+
.all() as EthicsRule[];
|
|
39
|
+
|
|
40
|
+
if (ftsRows.length > 0) return ftsRows;
|
|
41
|
+
} catch {}
|
|
42
|
+
|
|
43
|
+
return this.db
|
|
44
|
+
.query(
|
|
45
|
+
`SELECT * FROM playbook
|
|
46
|
+
WHERE category = 'response_quality' AND active = 1
|
|
47
|
+
ORDER BY helpful_count DESC`
|
|
48
|
+
)
|
|
49
|
+
.all() as EthicsRule[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
injectIntoPrompt(systemPrompt: string, rules: EthicsRule[]): string {
|
|
53
|
+
if (rules.length === 0) return systemPrompt;
|
|
54
|
+
const ethicsSection = rules
|
|
55
|
+
.map(r => `- ${r.rule}`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
return `${systemPrompt}\n\n## Reglas de Calidad de Respuesta\n${ethicsSection}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
hasEthicsLayer(): boolean {
|
|
61
|
+
const count = this.db
|
|
62
|
+
.query(`SELECT COUNT(*) as c FROM playbook WHERE category = 'response_quality' AND active = 1`)
|
|
63
|
+
.get() as any;
|
|
64
|
+
return (count?.c ?? 0) > 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { sendToUserChannel } from "./channel-notify.ts";
|
|
3
|
+
|
|
4
|
+
describe("sendToUserChannel (stub)", () => {
|
|
5
|
+
it("returns ok without real gateway", async () => {
|
|
6
|
+
const result = await sendToUserChannel("cli:test", "user-1", "hello");
|
|
7
|
+
expect(result.ok).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("handles empty message", async () => {
|
|
11
|
+
const result = await sendToUserChannel("cli:test", "user-1", "");
|
|
12
|
+
expect(result.ok).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.ts";
|
|
2
|
+
|
|
3
|
+
const log = logger.child("channel-notify");
|
|
4
|
+
|
|
5
|
+
export async function sendToUserChannel(
|
|
6
|
+
channel: string,
|
|
7
|
+
userId: string,
|
|
8
|
+
message: string,
|
|
9
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
10
|
+
log.info(`[stub] channel=${channel} userId=${userId} msg=${message.substring(0, 80)}`);
|
|
11
|
+
return { ok: true };
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { sendToUserChannel } from "./channel-notify.ts";
|