@johpaz/hive-core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +43 -0
  2. package/src/agent/compaction.ts +161 -0
  3. package/src/agent/context-guard.ts +91 -0
  4. package/src/agent/context.ts +148 -0
  5. package/src/agent/ethics.ts +102 -0
  6. package/src/agent/hooks.ts +166 -0
  7. package/src/agent/index.ts +67 -0
  8. package/src/agent/providers/index.ts +278 -0
  9. package/src/agent/providers.ts +1 -0
  10. package/src/agent/soul.ts +89 -0
  11. package/src/agent/stuck-loop.ts +133 -0
  12. package/src/agent/user.ts +86 -0
  13. package/src/channels/base.ts +91 -0
  14. package/src/channels/discord.ts +185 -0
  15. package/src/channels/index.ts +7 -0
  16. package/src/channels/manager.ts +204 -0
  17. package/src/channels/slack.ts +209 -0
  18. package/src/channels/telegram.ts +177 -0
  19. package/src/channels/webchat.ts +83 -0
  20. package/src/channels/whatsapp.ts +305 -0
  21. package/src/config/index.ts +1 -0
  22. package/src/config/loader.ts +508 -0
  23. package/src/gateway/index.ts +5 -0
  24. package/src/gateway/lane-queue.ts +169 -0
  25. package/src/gateway/router.ts +124 -0
  26. package/src/gateway/server.ts +347 -0
  27. package/src/gateway/session.ts +131 -0
  28. package/src/gateway/slash-commands.ts +176 -0
  29. package/src/heartbeat/index.ts +157 -0
  30. package/src/index.ts +21 -0
  31. package/src/memory/index.ts +1 -0
  32. package/src/memory/notes.ts +170 -0
  33. package/src/multi-agent/bindings.ts +171 -0
  34. package/src/multi-agent/index.ts +4 -0
  35. package/src/multi-agent/manager.ts +182 -0
  36. package/src/multi-agent/sandbox.ts +130 -0
  37. package/src/multi-agent/subagents.ts +302 -0
  38. package/src/security/index.ts +187 -0
  39. package/src/tools/cron.ts +156 -0
  40. package/src/tools/exec.ts +105 -0
  41. package/src/tools/index.ts +6 -0
  42. package/src/tools/memory.ts +176 -0
  43. package/src/tools/notify.ts +53 -0
  44. package/src/tools/read.ts +154 -0
  45. package/src/tools/registry.ts +115 -0
  46. package/src/tools/web.ts +186 -0
  47. package/src/utils/crypto.ts +73 -0
  48. package/src/utils/index.ts +3 -0
  49. package/src/utils/logger.ts +254 -0
  50. package/src/utils/retry.ts +70 -0
@@ -0,0 +1,187 @@
1
+ import type { Config } from "../config/loader.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+
4
+ export interface RateLimitConfig {
5
+ windowMs: number;
6
+ maxRequests: number;
7
+ }
8
+
9
+ export interface RateLimitEntry {
10
+ count: number;
11
+ resetAt: number;
12
+ }
13
+
14
+ export class RateLimiter {
15
+ private limits: Map<string, RateLimitEntry> = new Map();
16
+ private config: RateLimitConfig;
17
+ private log = logger.child("rate-limiter");
18
+
19
+ constructor(config: RateLimitConfig) {
20
+ this.config = config;
21
+ this.startCleanup();
22
+ }
23
+
24
+ check(key: string): { allowed: boolean; remaining: number; resetAt: number } {
25
+ const now = Date.now();
26
+ const entry = this.limits.get(key);
27
+
28
+ if (!entry || now > entry.resetAt) {
29
+ const resetAt = now + this.config.windowMs;
30
+ this.limits.set(key, { count: 1, resetAt });
31
+ return { allowed: true, remaining: this.config.maxRequests - 1, resetAt };
32
+ }
33
+
34
+ if (entry.count >= this.config.maxRequests) {
35
+ this.log.warn(`Rate limit exceeded for ${key}`);
36
+ return { allowed: false, remaining: 0, resetAt: entry.resetAt };
37
+ }
38
+
39
+ entry.count++;
40
+ return {
41
+ allowed: true,
42
+ remaining: this.config.maxRequests - entry.count,
43
+ resetAt: entry.resetAt
44
+ };
45
+ }
46
+
47
+ reset(key: string): void {
48
+ this.limits.delete(key);
49
+ }
50
+
51
+ private startCleanup(): void {
52
+ setInterval(() => {
53
+ const now = Date.now();
54
+ for (const [key, entry] of this.limits) {
55
+ if (now > entry.resetAt) {
56
+ this.limits.delete(key);
57
+ }
58
+ }
59
+ }, this.config.windowMs);
60
+ }
61
+ }
62
+
63
+ export class InputValidator {
64
+ private maxMessageLength: number;
65
+ private maxCommandArgs: number;
66
+ private log = logger.child("validator");
67
+
68
+ constructor(options: { maxMessageLength?: number; maxCommandArgs?: number } = {}) {
69
+ this.maxMessageLength = options.maxMessageLength ?? 100000;
70
+ this.maxCommandArgs = options.maxCommandArgs ?? 50;
71
+ }
72
+
73
+ validateMessage(content: string): { valid: boolean; error?: string } {
74
+ if (typeof content !== "string") {
75
+ return { valid: false, error: "Message must be a string" };
76
+ }
77
+
78
+ if (content.length === 0) {
79
+ return { valid: false, error: "Message cannot be empty" };
80
+ }
81
+
82
+ if (content.length > this.maxMessageLength) {
83
+ this.log.warn(`Message too long: ${content.length} > ${this.maxMessageLength}`);
84
+ return {
85
+ valid: false,
86
+ error: `Message too long (max ${this.maxMessageLength} characters)`
87
+ };
88
+ }
89
+
90
+ return { valid: true };
91
+ }
92
+
93
+ validateCommand(name: string, args: string[]): { valid: boolean; error?: string } {
94
+ if (!name || typeof name !== "string") {
95
+ return { valid: false, error: "Command name is required" };
96
+ }
97
+
98
+ if (!/^[a-z0-9_-]+$/.test(name)) {
99
+ return { valid: false, error: "Invalid command name format" };
100
+ }
101
+
102
+ if (args.length > this.maxCommandArgs) {
103
+ return { valid: false, error: `Too many arguments (max ${this.maxCommandArgs})` };
104
+ }
105
+
106
+ return { valid: true };
107
+ }
108
+
109
+ validateSessionId(sessionId: string): { valid: boolean; error?: string } {
110
+ if (!sessionId || typeof sessionId !== "string") {
111
+ return { valid: false, error: "Session ID is required" };
112
+ }
113
+
114
+ const pattern = /^agent:[a-z0-9_-]+:[a-z0-9_-]+:(main|dm|group)(?::[a-z0-9_-]+)?$/;
115
+ if (!pattern.test(sessionId)) {
116
+ return { valid: false, error: "Invalid session ID format" };
117
+ }
118
+
119
+ return { valid: true };
120
+ }
121
+
122
+ sanitizeInput(input: string): string {
123
+ return input
124
+ .replace(/\x00/g, "")
125
+ .replace(/[\x1F\x7F]/g, "")
126
+ .trim();
127
+ }
128
+ }
129
+
130
+ export class AuthManager {
131
+ private config: Config;
132
+ private allowedUsers: Set<string>;
133
+ private log = logger.child("auth");
134
+
135
+ constructor(config: Config) {
136
+ this.config = config;
137
+ this.allowedUsers = new Set(config.security?.allowedUsers ?? []);
138
+ }
139
+
140
+ isAllowed(peerId: string, channel: string): boolean {
141
+ if (this.allowedUsers.size === 0) {
142
+ return true;
143
+ }
144
+
145
+ const channelConfig = this.config.channels?.[channel as keyof typeof this.config.channels];
146
+ if (channelConfig && typeof channelConfig === "object" && "allowFrom" in channelConfig) {
147
+ const allowFrom = (channelConfig as { allowFrom?: string[] }).allowFrom;
148
+ if (allowFrom && allowFrom.length > 0) {
149
+ return allowFrom.includes(peerId);
150
+ }
151
+ }
152
+
153
+ return this.allowedUsers.has(peerId);
154
+ }
155
+
156
+ addAllowedUser(peerId: string): void {
157
+ this.allowedUsers.add(peerId);
158
+ this.log.info(`Added allowed user: ${peerId}`);
159
+ }
160
+
161
+ removeAllowedUser(peerId: string): boolean {
162
+ const removed = this.allowedUsers.delete(peerId);
163
+ if (removed) {
164
+ this.log.info(`Removed allowed user: ${peerId}`);
165
+ }
166
+ return removed;
167
+ }
168
+
169
+ listAllowedUsers(): string[] {
170
+ return Array.from(this.allowedUsers);
171
+ }
172
+ }
173
+
174
+ export function createRateLimiter(config: RateLimitConfig): RateLimiter {
175
+ return new RateLimiter(config);
176
+ }
177
+
178
+ export function createInputValidator(options?: {
179
+ maxMessageLength?: number;
180
+ maxCommandArgs?: number;
181
+ }): InputValidator {
182
+ return new InputValidator(options);
183
+ }
184
+
185
+ export function createAuthManager(config: Config): AuthManager {
186
+ return new AuthManager(config);
187
+ }
@@ -0,0 +1,156 @@
1
+ import type { Tool } from "./registry.ts";
2
+ import type { Config } from "../config/loader.ts";
3
+ import { logger } from "../utils/logger.ts";
4
+
5
+ export interface CronJob {
6
+ id: string;
7
+ sessionId: string;
8
+ expression: string;
9
+ task: string;
10
+ enabled: boolean;
11
+ lastRun?: Date;
12
+ nextRun?: Date;
13
+ createdAt: Date;
14
+ }
15
+
16
+ export function createCronTools(_config: Config): Tool[] {
17
+ const log = logger.child("cron");
18
+ const jobs: Map<string, CronJob> = new Map();
19
+ let jobIdCounter = 0;
20
+
21
+ const cronAdd: Tool = {
22
+ name: "cron_add",
23
+ description: "Add a scheduled task",
24
+ parameters: {
25
+ type: "object",
26
+ properties: {
27
+ expression: {
28
+ type: "string",
29
+ description: "Cron expression (e.g., '0 9 * * *' for daily at 9am)",
30
+ },
31
+ task: {
32
+ type: "string",
33
+ description: "Task description or message to send",
34
+ },
35
+ sessionId: {
36
+ type: "string",
37
+ description: "Session to send the task to",
38
+ },
39
+ },
40
+ required: ["expression", "task"],
41
+ },
42
+ execute: async (params: Record<string, unknown>) => {
43
+ const expression = params.expression as string;
44
+ const task = params.task as string;
45
+ const sessionId = (params.sessionId as string) ?? "agent:main:main";
46
+
47
+ const id = `cron-${++jobIdCounter}`;
48
+
49
+ const job: CronJob = {
50
+ id,
51
+ sessionId,
52
+ expression,
53
+ task,
54
+ enabled: true,
55
+ createdAt: new Date(),
56
+ };
57
+
58
+ jobs.set(id, job);
59
+ log.info(`Added cron job: ${id}`, { expression, task });
60
+
61
+ return { success: true, jobId: id, job };
62
+ },
63
+ };
64
+
65
+ const cronList: Tool = {
66
+ name: "cron_list",
67
+ description: "List all scheduled tasks",
68
+ parameters: {
69
+ type: "object",
70
+ properties: {},
71
+ },
72
+ execute: async () => {
73
+ const jobList = Array.from(jobs.values()).map((j) => ({
74
+ id: j.id,
75
+ expression: j.expression,
76
+ task: j.task.slice(0, 100),
77
+ enabled: j.enabled,
78
+ lastRun: j.lastRun,
79
+ }));
80
+
81
+ return { jobs: jobList, count: jobList.length };
82
+ },
83
+ };
84
+
85
+ const cronRemove: Tool = {
86
+ name: "cron_remove",
87
+ description: "Remove a scheduled task",
88
+ parameters: {
89
+ type: "object",
90
+ properties: {
91
+ jobId: {
92
+ type: "string",
93
+ description: "The ID of the job to remove",
94
+ },
95
+ },
96
+ required: ["jobId"],
97
+ },
98
+ execute: async (params: Record<string, unknown>) => {
99
+ const jobId = params.jobId as string;
100
+
101
+ if (!jobs.has(jobId)) {
102
+ throw new Error(`Job not found: ${jobId}`);
103
+ }
104
+
105
+ jobs.delete(jobId);
106
+ log.info(`Removed cron job: ${jobId}`);
107
+
108
+ return { success: true, removedJob: jobId };
109
+ },
110
+ };
111
+
112
+ const cronEdit: Tool = {
113
+ name: "cron_edit",
114
+ description: "Edit a scheduled task",
115
+ parameters: {
116
+ type: "object",
117
+ properties: {
118
+ jobId: {
119
+ type: "string",
120
+ description: "The ID of the job to edit",
121
+ },
122
+ expression: {
123
+ type: "string",
124
+ description: "New cron expression",
125
+ },
126
+ task: {
127
+ type: "string",
128
+ description: "New task description",
129
+ },
130
+ enabled: {
131
+ type: "boolean",
132
+ description: "Enable or disable the job",
133
+ },
134
+ },
135
+ required: ["jobId"],
136
+ },
137
+ execute: async (params: Record<string, unknown>) => {
138
+ const jobId = params.jobId as string;
139
+ const job = jobs.get(jobId);
140
+
141
+ if (!job) {
142
+ throw new Error(`Job not found: ${jobId}`);
143
+ }
144
+
145
+ if (params.expression) job.expression = params.expression as string;
146
+ if (params.task) job.task = params.task as string;
147
+ if (params.enabled !== undefined) job.enabled = params.enabled as boolean;
148
+
149
+ log.info(`Edited cron job: ${jobId}`);
150
+
151
+ return { success: true, job };
152
+ },
153
+ };
154
+
155
+ return [cronAdd, cronList, cronRemove, cronEdit];
156
+ }
@@ -0,0 +1,105 @@
1
+ import type { Tool } from "./registry.ts";
2
+ import type { Config } from "../config/loader.ts";
3
+ import { logger } from "../utils/logger.ts";
4
+
5
+ export function createExecTool(config: Config): Tool | null {
6
+ const execConfig = config.tools?.exec;
7
+
8
+ if (!execConfig?.enabled) {
9
+ return null;
10
+ }
11
+
12
+ const allowlist = execConfig.allowlist ?? [];
13
+ const denylist = execConfig.denylist ?? [
14
+ "rm -rf /",
15
+ "sudo",
16
+ "chmod 777",
17
+ "> /dev/",
18
+ "mkfs",
19
+ ];
20
+ const timeout = (execConfig.timeoutSeconds ?? 30) * 1000;
21
+ const workDir = execConfig.workDir?.replace(/^~/, process.env.HOME ?? "") ?? process.cwd();
22
+
23
+ const log = logger.child("exec");
24
+
25
+ const isAllowed = (command: string): { allowed: boolean; reason?: string } => {
26
+ if (allowlist.length > 0) {
27
+ const baseCmd = command.split(" ")[0] ?? "";
28
+ if (!allowlist.some((allowed) => baseCmd === allowed || command.startsWith(allowed))) {
29
+ return { allowed: false, reason: `Command not in allowlist: ${baseCmd}` };
30
+ }
31
+ }
32
+
33
+ for (const denied of denylist) {
34
+ if (command.includes(denied)) {
35
+ return { allowed: false, reason: `Command matches denylist pattern: ${denied}` };
36
+ }
37
+ }
38
+
39
+ return { allowed: true };
40
+ };
41
+
42
+ return {
43
+ name: "exec",
44
+ description: "Execute shell commands with allowlist/denylist restrictions",
45
+ parameters: {
46
+ type: "object",
47
+ properties: {
48
+ command: {
49
+ type: "string",
50
+ description: "The shell command to execute",
51
+ },
52
+ timeout: {
53
+ type: "number",
54
+ description: "Timeout in seconds (optional, default from config)",
55
+ },
56
+ cwd: {
57
+ type: "string",
58
+ description: "Working directory (optional)",
59
+ },
60
+ },
61
+ required: ["command"],
62
+ },
63
+ execute: async (params: Record<string, unknown>) => {
64
+ const command = params.command as string;
65
+ const cmdTimeout = ((params.timeout as number) ?? timeout / 1000) * 1000;
66
+ const cwd = (params.cwd as string)?.replace(/^~/, process.env.HOME ?? "") ?? workDir;
67
+
68
+ const check = isAllowed(command);
69
+ if (!check.allowed) {
70
+ throw new Error(check.reason ?? "Command not allowed");
71
+ }
72
+
73
+ log.debug(`Executing: ${command}`);
74
+
75
+ try {
76
+ const proc = Bun.spawn(["sh", "-c", command], {
77
+ cwd,
78
+ timeout: cmdTimeout,
79
+ maxBuffer: 10 * 1024 * 1024,
80
+ });
81
+
82
+ const stdout = await new Response(proc.stdout).text();
83
+ const stderr = await new Response(proc.stderr).text();
84
+ const exitCode = await proc.exited;
85
+
86
+ if (exitCode === 0) {
87
+ return {
88
+ stdout: stdout.slice(-10000),
89
+ stderr: stderr.slice(-5000),
90
+ exitCode,
91
+ };
92
+ } else {
93
+ throw new Error(`Command exited with code ${exitCode}: ${stderr.slice(0, 500)}`);
94
+ }
95
+ } catch (error) {
96
+ const err = error as Error;
97
+ if (err.message.includes("exit")) {
98
+ throw error;
99
+ }
100
+ log.error(`Exec failed: ${err.message}`);
101
+ throw new Error(`Failed to execute command: ${err.message}`);
102
+ }
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./registry.ts";
2
+ export * from "./read.ts";
3
+ export * from "./exec.ts";
4
+ export * from "./web.ts";
5
+ export * from "./notify.ts";
6
+ export * from "./cron.ts";
@@ -0,0 +1,176 @@
1
+ import type { Tool, ToolResult } from "./registry.ts";
2
+ import type { MemoryStore, Note } from "../memory/notes.ts";
3
+
4
+ export function memoryWriteTool(memory: MemoryStore): Tool {
5
+ return {
6
+ name: "memory_write",
7
+ description: "Store information in long-term memory. Use this to remember important facts, user preferences, or context for future conversations.",
8
+ parameters: {
9
+ type: "object",
10
+ properties: {
11
+ title: {
12
+ type: "string",
13
+ description: "A descriptive title for this memory entry",
14
+ },
15
+ content: {
16
+ type: "string",
17
+ description: "The content to store in memory",
18
+ },
19
+ },
20
+ required: ["title", "content"],
21
+ },
22
+ execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
23
+ const title = params.title as string;
24
+ const content = params.content as string;
25
+
26
+ if (!title || !content) {
27
+ return { success: false, error: "Title and content are required" };
28
+ }
29
+
30
+ const note = memory.write(title, content);
31
+ return {
32
+ success: true,
33
+ result: {
34
+ title: note.title,
35
+ updatedAt: note.updatedAt.toISOString(),
36
+ },
37
+ };
38
+ },
39
+ };
40
+ }
41
+
42
+ export function memoryReadTool(memory: MemoryStore): Tool {
43
+ return {
44
+ name: "memory_read",
45
+ description: "Retrieve information from long-term memory by title.",
46
+ parameters: {
47
+ type: "object",
48
+ properties: {
49
+ title: {
50
+ type: "string",
51
+ description: "The title of the memory entry to retrieve",
52
+ },
53
+ },
54
+ required: ["title"],
55
+ },
56
+ execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
57
+ const title = params.title as string;
58
+
59
+ if (!title) {
60
+ return { success: false, error: "Title is required" };
61
+ }
62
+
63
+ const note = memory.read(title);
64
+ if (!note) {
65
+ return { success: false, error: `Memory not found: ${title}` };
66
+ }
67
+
68
+ return {
69
+ success: true,
70
+ result: {
71
+ title: note.title,
72
+ content: note.content,
73
+ createdAt: note.createdAt.toISOString(),
74
+ updatedAt: note.updatedAt.toISOString(),
75
+ },
76
+ };
77
+ },
78
+ };
79
+ }
80
+
81
+ export function memoryListTool(memory: MemoryStore): Tool {
82
+ return {
83
+ name: "memory_list",
84
+ description: "List all memory entries.",
85
+ parameters: {
86
+ type: "object",
87
+ properties: {},
88
+ },
89
+ execute: async (_params: Record<string, unknown>): Promise<ToolResult> => {
90
+ const notes = memory.list();
91
+ return {
92
+ success: true,
93
+ result: {
94
+ count: notes.length,
95
+ entries: notes.map((n) => ({ title: n.title })),
96
+ },
97
+ };
98
+ },
99
+ };
100
+ }
101
+
102
+ export function memorySearchTool(memory: MemoryStore): Tool {
103
+ return {
104
+ name: "memory_search",
105
+ description: "Search memory entries by content.",
106
+ parameters: {
107
+ type: "object",
108
+ properties: {
109
+ query: {
110
+ type: "string",
111
+ description: "The search query",
112
+ },
113
+ },
114
+ required: ["query"],
115
+ },
116
+ execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
117
+ const query = params.query as string;
118
+
119
+ if (!query) {
120
+ return { success: false, error: "Query is required" };
121
+ }
122
+
123
+ const results = memory.search(query);
124
+ return {
125
+ success: true,
126
+ result: {
127
+ query,
128
+ count: results.length,
129
+ results: results.map((n: Note) => ({
130
+ title: n.title,
131
+ snippet: n.content.slice(0, 200) + (n.content.length > 200 ? "..." : ""),
132
+ })),
133
+ },
134
+ };
135
+ },
136
+ };
137
+ }
138
+
139
+ export function memoryDeleteTool(memory: MemoryStore): Tool {
140
+ return {
141
+ name: "memory_delete",
142
+ description: "Delete a memory entry by title.",
143
+ parameters: {
144
+ type: "object",
145
+ properties: {
146
+ title: {
147
+ type: "string",
148
+ description: "The title of the memory entry to delete",
149
+ },
150
+ },
151
+ required: ["title"],
152
+ },
153
+ execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
154
+ const title = params.title as string;
155
+
156
+ if (!title) {
157
+ return { success: false, error: "Title is required" };
158
+ }
159
+
160
+ const deleted = memory.delete(title);
161
+ return {
162
+ success: deleted,
163
+ result: deleted ? { message: `Deleted: ${title}` } : { message: `Not found: ${title}` },
164
+ error: deleted ? undefined : `Memory not found: ${title}`,
165
+ };
166
+ },
167
+ };
168
+ }
169
+
170
+ export function registerMemoryTools(registry: { register: (tool: Tool) => void }, memory: MemoryStore): void {
171
+ registry.register(memoryWriteTool(memory));
172
+ registry.register(memoryReadTool(memory));
173
+ registry.register(memoryListTool(memory));
174
+ registry.register(memorySearchTool(memory));
175
+ registry.register(memoryDeleteTool(memory));
176
+ }
@@ -0,0 +1,53 @@
1
+ import type { Tool } from "./registry.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+
4
+ export function createNotifyTool(): Tool {
5
+ const log = logger.child("notify");
6
+
7
+ return {
8
+ name: "notify",
9
+ description: "Send a system notification",
10
+ parameters: {
11
+ type: "object",
12
+ properties: {
13
+ title: {
14
+ type: "string",
15
+ description: "Notification title",
16
+ },
17
+ message: {
18
+ type: "string",
19
+ description: "Notification message",
20
+ },
21
+ urgency: {
22
+ type: "string",
23
+ enum: ["low", "normal", "critical"],
24
+ description: "Notification urgency level",
25
+ },
26
+ },
27
+ required: ["title", "message"],
28
+ },
29
+ execute: async (params: Record<string, unknown>) => {
30
+ const title = params.title as string;
31
+ const message = params.message as string;
32
+ const urgency = (params.urgency as string) ?? "normal";
33
+
34
+ log.debug(`Sending notification: ${title}`);
35
+
36
+ if (process.platform === "darwin") {
37
+ const cmd = `osascript -e 'display notification "${message}" with title "${title}"'`;
38
+ await Bun.$`${{ raw: cmd }}`.quiet();
39
+ } else if (process.platform === "linux") {
40
+ const urgencyFlag = urgency === "critical" ? "-u critical" : urgency === "low" ? "-u low" : "";
41
+ const cmd = `notify-send ${urgencyFlag} "${title}" "${message}"`;
42
+ await Bun.$`${{ raw: cmd }}`.quiet().catch(() => {
43
+ log.warn("notify-send not available");
44
+ });
45
+ } else {
46
+ log.warn(`Notifications not supported on ${process.platform}`);
47
+ return { success: false, reason: "Platform not supported" };
48
+ }
49
+
50
+ return { success: true, title, message };
51
+ },
52
+ };
53
+ }