pretticlaw 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.
Files changed (158) hide show
  1. package/CONTRIBUTING.md +123 -0
  2. package/README.md +150 -0
  3. package/assets/logo.png +0 -0
  4. package/dist/agent/context.d.ts +22 -0
  5. package/dist/agent/context.js +85 -0
  6. package/dist/agent/loop.d.ts +63 -0
  7. package/dist/agent/loop.js +244 -0
  8. package/dist/agent/memory.d.ts +16 -0
  9. package/dist/agent/memory.js +98 -0
  10. package/dist/agent/skills.d.ts +18 -0
  11. package/dist/agent/skills.js +121 -0
  12. package/dist/agent/subagent.d.ts +30 -0
  13. package/dist/agent/subagent.js +92 -0
  14. package/dist/agent/tools/base.d.ts +10 -0
  15. package/dist/agent/tools/base.js +58 -0
  16. package/dist/agent/tools/cron.d.ts +43 -0
  17. package/dist/agent/tools/cron.js +83 -0
  18. package/dist/agent/tools/filesystem.d.ts +79 -0
  19. package/dist/agent/tools/filesystem.js +125 -0
  20. package/dist/agent/tools/message.d.ts +41 -0
  21. package/dist/agent/tools/message.js +55 -0
  22. package/dist/agent/tools/registry.d.ts +9 -0
  23. package/dist/agent/tools/registry.js +33 -0
  24. package/dist/agent/tools/shell.d.ts +26 -0
  25. package/dist/agent/tools/shell.js +78 -0
  26. package/dist/agent/tools/spawn.d.ts +27 -0
  27. package/dist/agent/tools/spawn.js +35 -0
  28. package/dist/agent/tools/web.d.ts +50 -0
  29. package/dist/agent/tools/web.js +119 -0
  30. package/dist/bus/async-queue.d.ts +7 -0
  31. package/dist/bus/async-queue.js +20 -0
  32. package/dist/bus/events.d.ts +19 -0
  33. package/dist/bus/events.js +3 -0
  34. package/dist/bus/queue.d.ts +12 -0
  35. package/dist/bus/queue.js +23 -0
  36. package/dist/channels/base.d.ts +22 -0
  37. package/dist/channels/base.js +35 -0
  38. package/dist/channels/discord.d.ts +24 -0
  39. package/dist/channels/discord.js +133 -0
  40. package/dist/channels/manager.d.ts +17 -0
  41. package/dist/channels/manager.js +67 -0
  42. package/dist/channels/stub.d.ts +10 -0
  43. package/dist/channels/stub.js +18 -0
  44. package/dist/channels/telegram.d.ts +20 -0
  45. package/dist/channels/telegram.js +93 -0
  46. package/dist/cli/commands.d.ts +2 -0
  47. package/dist/cli/commands.js +552 -0
  48. package/dist/config/loader.d.ts +5 -0
  49. package/dist/config/loader.js +55 -0
  50. package/dist/config/schema.d.ts +246 -0
  51. package/dist/config/schema.js +94 -0
  52. package/dist/cron/service.d.ts +33 -0
  53. package/dist/cron/service.js +195 -0
  54. package/dist/cron/types.d.ts +47 -0
  55. package/dist/cron/types.js +1 -0
  56. package/dist/dashboard/index.html +1567 -0
  57. package/dist/heartbeat/service.d.ts +21 -0
  58. package/dist/heartbeat/service.js +101 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.js +5 -0
  61. package/dist/providers/base.d.ts +23 -0
  62. package/dist/providers/base.js +21 -0
  63. package/dist/providers/custom-provider.d.ts +16 -0
  64. package/dist/providers/custom-provider.js +49 -0
  65. package/dist/providers/litellm-provider.d.ts +19 -0
  66. package/dist/providers/litellm-provider.js +128 -0
  67. package/dist/providers/registry.d.ts +5 -0
  68. package/dist/providers/registry.js +45 -0
  69. package/dist/session/manager.d.ts +31 -0
  70. package/dist/session/manager.js +116 -0
  71. package/dist/skills/README.md +25 -0
  72. package/dist/skills/clawhub/SKILL.md +53 -0
  73. package/dist/skills/cron/SKILL.md +57 -0
  74. package/dist/skills/github/SKILL.md +48 -0
  75. package/dist/skills/memory/SKILL.md +31 -0
  76. package/dist/skills/skill-creator/SKILL.md +371 -0
  77. package/dist/skills/summarize/SKILL.md +67 -0
  78. package/dist/skills/tmux/SKILL.md +121 -0
  79. package/dist/skills/tmux/scripts/find-sessions.sh +112 -0
  80. package/dist/skills/tmux/scripts/wait-for-text.sh +83 -0
  81. package/dist/skills/weather/SKILL.md +49 -0
  82. package/dist/templates/AGENTS.md +23 -0
  83. package/dist/templates/HEARTBEAT.md +16 -0
  84. package/dist/templates/SOUL.md +21 -0
  85. package/dist/templates/TOOLS.md +15 -0
  86. package/dist/templates/USER.md +49 -0
  87. package/dist/templates/memory/MEMORY.md +23 -0
  88. package/dist/types.d.ts +4 -0
  89. package/dist/types.js +3 -0
  90. package/dist/utils/helpers.d.ts +5 -0
  91. package/dist/utils/helpers.js +53 -0
  92. package/dist/web/server.d.ts +15 -0
  93. package/dist/web/server.js +169 -0
  94. package/package.json +37 -0
  95. package/scripts/copy-assets.mjs +21 -0
  96. package/src/agent/context.ts +90 -0
  97. package/src/agent/loop.ts +291 -0
  98. package/src/agent/memory.ts +104 -0
  99. package/src/agent/skills.ts +121 -0
  100. package/src/agent/subagent.ts +96 -0
  101. package/src/agent/tools/base.ts +59 -0
  102. package/src/agent/tools/cron.ts +79 -0
  103. package/src/agent/tools/filesystem.ts +93 -0
  104. package/src/agent/tools/message.ts +57 -0
  105. package/src/agent/tools/registry.ts +36 -0
  106. package/src/agent/tools/shell.ts +69 -0
  107. package/src/agent/tools/spawn.ts +37 -0
  108. package/src/agent/tools/web.ts +108 -0
  109. package/src/bus/async-queue.ts +20 -0
  110. package/src/bus/events.ts +23 -0
  111. package/src/bus/queue.ts +31 -0
  112. package/src/channels/base.ts +36 -0
  113. package/src/channels/discord.ts +156 -0
  114. package/src/channels/manager.ts +70 -0
  115. package/src/channels/stub.ts +20 -0
  116. package/src/channels/telegram.ts +120 -0
  117. package/src/cli/commands.ts +581 -0
  118. package/src/config/loader.ts +58 -0
  119. package/src/config/schema.ts +144 -0
  120. package/src/cron/service.ts +190 -0
  121. package/src/cron/types.ts +36 -0
  122. package/src/dashboard/index.html +1567 -0
  123. package/src/heartbeat/service.ts +95 -0
  124. package/src/index.ts +6 -0
  125. package/src/providers/base.ts +43 -0
  126. package/src/providers/custom-provider.ts +46 -0
  127. package/src/providers/litellm-provider.ts +131 -0
  128. package/src/providers/registry.ts +48 -0
  129. package/src/session/manager.ts +129 -0
  130. package/src/skills/README.md +25 -0
  131. package/src/skills/clawhub/SKILL.md +53 -0
  132. package/src/skills/cron/SKILL.md +57 -0
  133. package/src/skills/github/SKILL.md +48 -0
  134. package/src/skills/memory/SKILL.md +31 -0
  135. package/src/skills/skill-creator/SKILL.md +371 -0
  136. package/src/skills/summarize/SKILL.md +67 -0
  137. package/src/skills/tmux/SKILL.md +121 -0
  138. package/src/skills/tmux/scripts/find-sessions.sh +112 -0
  139. package/src/skills/tmux/scripts/wait-for-text.sh +83 -0
  140. package/src/skills/weather/SKILL.md +49 -0
  141. package/src/templates/AGENTS.md +23 -0
  142. package/src/templates/HEARTBEAT.md +16 -0
  143. package/src/templates/SOUL.md +21 -0
  144. package/src/templates/TOOLS.md +15 -0
  145. package/src/templates/USER.md +49 -0
  146. package/src/templates/memory/MEMORY.md +23 -0
  147. package/src/types/prompts.d.ts +14 -0
  148. package/src/types/ws.d.ts +15 -0
  149. package/src/types.ts +5 -0
  150. package/src/utils/helpers.ts +55 -0
  151. package/src/web/server.ts +198 -0
  152. package/test/context.test.ts +27 -0
  153. package/test/cron-service.test.ts +31 -0
  154. package/test/message-tool.test.ts +10 -0
  155. package/test/providers.test.ts +43 -0
  156. package/test/tool-validation.test.ts +61 -0
  157. package/tsconfig.json +16 -0
  158. package/vitest.config.ts +8 -0
@@ -0,0 +1,35 @@
1
+ import { Tool } from "./base.js";
2
+ export class SpawnTool extends Tool {
3
+ manager;
4
+ name = "spawn";
5
+ description = "Spawn a subagent to handle a task in the background.";
6
+ parameters = {
7
+ type: "object",
8
+ properties: {
9
+ task: { type: "string", description: "The task for the subagent to complete" },
10
+ label: { type: "string", description: "Optional short label" },
11
+ },
12
+ required: ["task"],
13
+ };
14
+ originChannel = "cli";
15
+ originChatId = "direct";
16
+ sessionKey = "cli:direct";
17
+ constructor(manager) {
18
+ super();
19
+ this.manager = manager;
20
+ }
21
+ setContext(channel, chatId) {
22
+ this.originChannel = channel;
23
+ this.originChatId = chatId;
24
+ this.sessionKey = `${channel}:${chatId}`;
25
+ }
26
+ async execute(args) {
27
+ return this.manager.spawn({
28
+ task: String(args.task ?? ""),
29
+ label: args.label != null ? String(args.label) : null,
30
+ originChannel: this.originChannel,
31
+ originChatId: this.originChatId,
32
+ sessionKey: this.sessionKey,
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,50 @@
1
+ import { Tool } from "./base.js";
2
+ export declare class WebSearchTool extends Tool {
3
+ private readonly apiKey;
4
+ private readonly maxResults;
5
+ readonly name = "web_search";
6
+ readonly description = "Search the web. Returns titles, URLs, and snippets.";
7
+ readonly parameters: {
8
+ type: string;
9
+ properties: {
10
+ query: {
11
+ type: string;
12
+ description: string;
13
+ };
14
+ count: {
15
+ type: string;
16
+ minimum: number;
17
+ maximum: number;
18
+ description: string;
19
+ };
20
+ };
21
+ required: string[];
22
+ };
23
+ constructor(apiKey: string | null, maxResults?: number);
24
+ execute(args: Record<string, unknown>): Promise<string>;
25
+ }
26
+ export declare class WebFetchTool extends Tool {
27
+ private readonly maxChars;
28
+ readonly name = "web_fetch";
29
+ readonly description = "Fetch URL and extract readable content (HTML to markdown/text).";
30
+ readonly parameters: {
31
+ type: string;
32
+ properties: {
33
+ url: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ extractMode: {
38
+ type: string;
39
+ enum: string[];
40
+ };
41
+ maxChars: {
42
+ type: string;
43
+ minimum: number;
44
+ };
45
+ };
46
+ required: string[];
47
+ };
48
+ constructor(maxChars?: number);
49
+ execute(args: Record<string, unknown>): Promise<string>;
50
+ }
@@ -0,0 +1,119 @@
1
+ import { Tool } from "./base.js";
2
+ function stripTags(text) {
3
+ return text.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, "").trim();
4
+ }
5
+ function normalize(text) {
6
+ return text.replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
7
+ }
8
+ function validateUrl(url) {
9
+ try {
10
+ const u = new URL(url);
11
+ if (!["http:", "https:"].includes(u.protocol))
12
+ return [false, `Only http/https allowed, got '${u.protocol.replace(":", "")}'`];
13
+ if (!u.hostname)
14
+ return [false, "Missing domain"];
15
+ return [true, ""];
16
+ }
17
+ catch (err) {
18
+ return [false, String(err)];
19
+ }
20
+ }
21
+ export class WebSearchTool extends Tool {
22
+ apiKey;
23
+ maxResults;
24
+ name = "web_search";
25
+ description = "Search the web. Returns titles, URLs, and snippets.";
26
+ parameters = {
27
+ type: "object",
28
+ properties: { query: { type: "string", description: "Search query" }, count: { type: "integer", minimum: 1, maximum: 10, description: "Results (1-10)" } },
29
+ required: ["query"],
30
+ };
31
+ constructor(apiKey, maxResults = 5) {
32
+ super();
33
+ this.apiKey = apiKey;
34
+ this.maxResults = maxResults;
35
+ }
36
+ async execute(args) {
37
+ const query = String(args.query ?? "");
38
+ const count = Math.min(Math.max(Number(args.count ?? this.maxResults), 1), 10);
39
+ const key = this.apiKey || process.env.BRAVE_API_KEY || "";
40
+ if (!key)
41
+ return "Error: Brave Search API key not configured. Set tools.web.search.apiKey or BRAVE_API_KEY.";
42
+ try {
43
+ const url = new URL("https://api.search.brave.com/res/v1/web/search");
44
+ url.searchParams.set("q", query);
45
+ url.searchParams.set("count", String(count));
46
+ const res = await fetch(url, { headers: { Accept: "application/json", "X-Subscription-Token": key } });
47
+ if (!res.ok)
48
+ return `Error: ${res.status} ${res.statusText}`;
49
+ const json = await res.json();
50
+ const results = json.web?.results ?? [];
51
+ if (!results.length)
52
+ return `No results for: ${query}`;
53
+ const lines = [`Results for: ${query}`, ""];
54
+ results.slice(0, count).forEach((item, i) => {
55
+ lines.push(`${i + 1}. ${item.title ?? ""}`);
56
+ lines.push(` ${item.url ?? ""}`);
57
+ if (item.description)
58
+ lines.push(` ${item.description}`);
59
+ });
60
+ return lines.join("\n");
61
+ }
62
+ catch (err) {
63
+ return `Error: ${String(err)}`;
64
+ }
65
+ }
66
+ }
67
+ export class WebFetchTool extends Tool {
68
+ maxChars;
69
+ name = "web_fetch";
70
+ description = "Fetch URL and extract readable content (HTML to markdown/text).";
71
+ parameters = {
72
+ type: "object",
73
+ properties: {
74
+ url: { type: "string", description: "URL to fetch" },
75
+ extractMode: { type: "string", enum: ["markdown", "text"] },
76
+ maxChars: { type: "integer", minimum: 100 },
77
+ },
78
+ required: ["url"],
79
+ };
80
+ constructor(maxChars = 50000) {
81
+ super();
82
+ this.maxChars = maxChars;
83
+ }
84
+ async execute(args) {
85
+ const url = String(args.url ?? "");
86
+ const extractMode = String(args.extractMode ?? "markdown");
87
+ const maxChars = Number(args.maxChars ?? this.maxChars);
88
+ const [ok, err] = validateUrl(url);
89
+ if (!ok)
90
+ return JSON.stringify({ error: `URL validation failed: ${err}`, url });
91
+ try {
92
+ const res = await fetch(url, { redirect: "follow", headers: { "User-Agent": "Mozilla/5.0" } });
93
+ const ctype = res.headers.get("content-type") || "";
94
+ let text = "";
95
+ let extractor = "raw";
96
+ if (ctype.includes("application/json")) {
97
+ text = JSON.stringify(await res.json(), null, 2);
98
+ extractor = "json";
99
+ }
100
+ else {
101
+ const raw = await res.text();
102
+ if (ctype.includes("text/html") || /^\s*<!doctype|^\s*<html/i.test(raw.slice(0, 256))) {
103
+ const content = extractMode === "text" ? stripTags(raw) : normalize(stripTags(raw));
104
+ text = content;
105
+ extractor = "html";
106
+ }
107
+ else {
108
+ text = raw;
109
+ }
110
+ }
111
+ const truncated = text.length > maxChars;
112
+ const sliced = truncated ? text.slice(0, maxChars) : text;
113
+ return JSON.stringify({ url, finalUrl: res.url, status: res.status, extractor, truncated, length: sliced.length, text: sliced });
114
+ }
115
+ catch (e) {
116
+ return JSON.stringify({ error: String(e), url });
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,7 @@
1
+ export declare class AsyncQueue<T> {
2
+ private items;
3
+ private waiters;
4
+ push(v: T): void;
5
+ pop(): Promise<T>;
6
+ size(): number;
7
+ }
@@ -0,0 +1,20 @@
1
+ export class AsyncQueue {
2
+ items = [];
3
+ waiters = [];
4
+ push(v) {
5
+ const waiter = this.waiters.shift();
6
+ if (waiter)
7
+ waiter(v);
8
+ else
9
+ this.items.push(v);
10
+ }
11
+ async pop() {
12
+ const item = this.items.shift();
13
+ if (item !== undefined)
14
+ return item;
15
+ return new Promise((resolve) => this.waiters.push(resolve));
16
+ }
17
+ size() {
18
+ return this.items.length;
19
+ }
20
+ }
@@ -0,0 +1,19 @@
1
+ export interface InboundMessage {
2
+ channel: string;
3
+ senderId: string;
4
+ chatId: string;
5
+ content: string;
6
+ timestamp?: Date;
7
+ media?: string[];
8
+ metadata?: Record<string, unknown>;
9
+ sessionKeyOverride?: string;
10
+ }
11
+ export interface OutboundMessage {
12
+ channel: string;
13
+ chatId: string;
14
+ content: string;
15
+ replyTo?: string;
16
+ media?: string[];
17
+ metadata?: Record<string, unknown>;
18
+ }
19
+ export declare function sessionKey(msg: InboundMessage): string;
@@ -0,0 +1,3 @@
1
+ export function sessionKey(msg) {
2
+ return msg.sessionKeyOverride ?? `${msg.channel}:${msg.chatId}`;
3
+ }
@@ -0,0 +1,12 @@
1
+ import { AsyncQueue } from "./async-queue.js";
2
+ import type { InboundMessage, OutboundMessage } from "./events.js";
3
+ export declare class MessageBus {
4
+ readonly inbound: AsyncQueue<InboundMessage>;
5
+ readonly outbound: AsyncQueue<OutboundMessage>;
6
+ publishInbound(msg: InboundMessage): Promise<void>;
7
+ consumeInbound(): Promise<InboundMessage>;
8
+ publishOutbound(msg: OutboundMessage): Promise<void>;
9
+ consumeOutbound(): Promise<OutboundMessage>;
10
+ get inboundSize(): number;
11
+ get outboundSize(): number;
12
+ }
@@ -0,0 +1,23 @@
1
+ import { AsyncQueue } from "./async-queue.js";
2
+ export class MessageBus {
3
+ inbound = new AsyncQueue();
4
+ outbound = new AsyncQueue();
5
+ async publishInbound(msg) {
6
+ this.inbound.push(msg);
7
+ }
8
+ async consumeInbound() {
9
+ return this.inbound.pop();
10
+ }
11
+ async publishOutbound(msg) {
12
+ this.outbound.push(msg);
13
+ }
14
+ async consumeOutbound() {
15
+ return this.outbound.pop();
16
+ }
17
+ get inboundSize() {
18
+ return this.inbound.size();
19
+ }
20
+ get outboundSize() {
21
+ return this.outbound.size();
22
+ }
23
+ }
@@ -0,0 +1,22 @@
1
+ import type { MessageBus } from "../bus/queue.js";
2
+ import type { OutboundMessage } from "../bus/events.js";
3
+ export declare abstract class BaseChannel<TConfig = unknown> {
4
+ protected readonly config: TConfig;
5
+ protected readonly bus: MessageBus;
6
+ protected running: boolean;
7
+ constructor(config: TConfig, bus: MessageBus);
8
+ abstract readonly name: string;
9
+ abstract start(): Promise<void>;
10
+ abstract stop(): Promise<void>;
11
+ abstract send(msg: OutboundMessage): Promise<void>;
12
+ protected isAllowed(senderId: string): boolean;
13
+ protected handleMessage(input: {
14
+ senderId: string;
15
+ chatId: string;
16
+ content: string;
17
+ media?: string[];
18
+ metadata?: Record<string, unknown>;
19
+ sessionKey?: string;
20
+ }): Promise<void>;
21
+ get isRunning(): boolean;
22
+ }
@@ -0,0 +1,35 @@
1
+ export class BaseChannel {
2
+ config;
3
+ bus;
4
+ running = false;
5
+ constructor(config, bus) {
6
+ this.config = config;
7
+ this.bus = bus;
8
+ }
9
+ isAllowed(senderId) {
10
+ const allowFrom = (this.config?.allowFrom ?? []);
11
+ if (!allowFrom.length)
12
+ return true;
13
+ if (allowFrom.includes(String(senderId)))
14
+ return true;
15
+ if (String(senderId).includes("|"))
16
+ return String(senderId).split("|").some((p) => allowFrom.includes(p));
17
+ return false;
18
+ }
19
+ async handleMessage(input) {
20
+ if (!this.isAllowed(input.senderId))
21
+ return;
22
+ await this.bus.publishInbound({
23
+ channel: this.name,
24
+ senderId: String(input.senderId),
25
+ chatId: String(input.chatId),
26
+ content: input.content,
27
+ media: input.media ?? [],
28
+ metadata: input.metadata ?? {},
29
+ sessionKeyOverride: input.sessionKey,
30
+ });
31
+ }
32
+ get isRunning() {
33
+ return this.running;
34
+ }
35
+ }
@@ -0,0 +1,24 @@
1
+ import type { OutboundMessage } from "../bus/events.js";
2
+ import type { MessageBus } from "../bus/queue.js";
3
+ import { BaseChannel } from "./base.js";
4
+ type DiscordConfig = {
5
+ enabled: boolean;
6
+ token: string;
7
+ allowFrom: string[];
8
+ gatewayUrl: string;
9
+ intents: number;
10
+ };
11
+ export declare class DiscordChannel extends BaseChannel<DiscordConfig> {
12
+ readonly name = "discord";
13
+ private ws;
14
+ private hb;
15
+ private seq;
16
+ constructor(config: DiscordConfig, bus: MessageBus);
17
+ start(): Promise<void>;
18
+ stop(): Promise<void>;
19
+ send(msg: OutboundMessage): Promise<void>;
20
+ private connectOnce;
21
+ private startHeartbeat;
22
+ private sendPayload;
23
+ }
24
+ export {};
@@ -0,0 +1,133 @@
1
+ import WebSocket from "ws";
2
+ import { BaseChannel } from "./base.js";
3
+ export class DiscordChannel extends BaseChannel {
4
+ name = "discord";
5
+ ws = null;
6
+ hb = null;
7
+ seq = null;
8
+ constructor(config, bus) {
9
+ super(config, bus);
10
+ }
11
+ async start() {
12
+ if (!this.config.token) {
13
+ console.error("Discord token not configured");
14
+ return;
15
+ }
16
+ this.running = true;
17
+ while (this.running) {
18
+ try {
19
+ await this.connectOnce();
20
+ }
21
+ catch {
22
+ // reconnect loop
23
+ }
24
+ if (this.running)
25
+ await new Promise((r) => setTimeout(r, 2000));
26
+ }
27
+ }
28
+ async stop() {
29
+ this.running = false;
30
+ if (this.hb) {
31
+ clearInterval(this.hb);
32
+ this.hb = null;
33
+ }
34
+ if (this.ws) {
35
+ try {
36
+ this.ws.close();
37
+ }
38
+ catch {
39
+ // ignore
40
+ }
41
+ this.ws = null;
42
+ }
43
+ }
44
+ async send(msg) {
45
+ if (!this.config.token)
46
+ return;
47
+ await fetch(`https://discord.com/api/v10/channels/${msg.chatId}/messages`, {
48
+ method: "POST",
49
+ headers: {
50
+ Authorization: `Bot ${this.config.token}`,
51
+ "Content-Type": "application/json",
52
+ },
53
+ body: JSON.stringify({ content: msg.content || "" }),
54
+ }).catch(() => undefined);
55
+ }
56
+ async connectOnce() {
57
+ await new Promise((resolve) => {
58
+ const ws = new WebSocket(this.config.gatewayUrl);
59
+ this.ws = ws;
60
+ ws.on("message", async (raw) => {
61
+ let payload;
62
+ try {
63
+ payload = JSON.parse(raw.toString());
64
+ }
65
+ catch {
66
+ return;
67
+ }
68
+ if (payload.s != null)
69
+ this.seq = payload.s;
70
+ if (payload.op === 10) {
71
+ const interval = Number(payload.d?.heartbeat_interval ?? 30000);
72
+ this.startHeartbeat(interval);
73
+ this.sendPayload({
74
+ op: 2,
75
+ d: {
76
+ token: this.config.token,
77
+ intents: this.config.intents,
78
+ properties: { os: process.platform, browser: "pretticlaw", device: "pretticlaw" },
79
+ },
80
+ });
81
+ return;
82
+ }
83
+ if (payload.op === 7) {
84
+ ws.close();
85
+ return;
86
+ }
87
+ if (payload.op === 11)
88
+ return;
89
+ if (payload.t === "MESSAGE_CREATE") {
90
+ const m = payload.d;
91
+ if (!m || m.author?.bot)
92
+ return;
93
+ const content = String(m.content ?? "").trim();
94
+ if (!content)
95
+ return;
96
+ const sender = `${m.author?.id}${m.author?.username ? `|${m.author.username}` : ""}`;
97
+ await this.handleMessage({
98
+ senderId: sender,
99
+ chatId: String(m.channel_id),
100
+ content,
101
+ metadata: { message_id: m.id, guild_id: m.guild_id ?? null },
102
+ });
103
+ }
104
+ });
105
+ ws.on("close", () => {
106
+ if (this.hb) {
107
+ clearInterval(this.hb);
108
+ this.hb = null;
109
+ }
110
+ resolve();
111
+ });
112
+ ws.on("error", () => {
113
+ if (this.hb) {
114
+ clearInterval(this.hb);
115
+ this.hb = null;
116
+ }
117
+ resolve();
118
+ });
119
+ });
120
+ }
121
+ startHeartbeat(intervalMs) {
122
+ if (this.hb)
123
+ clearInterval(this.hb);
124
+ this.hb = setInterval(() => {
125
+ this.sendPayload({ op: 1, d: this.seq });
126
+ }, intervalMs);
127
+ }
128
+ sendPayload(data) {
129
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
130
+ return;
131
+ this.ws.send(JSON.stringify(data));
132
+ }
133
+ }
@@ -0,0 +1,17 @@
1
+ import type { Config } from "../config/schema.js";
2
+ import type { MessageBus } from "../bus/queue.js";
3
+ import type { BaseChannel } from "./base.js";
4
+ export declare class ChannelManager {
5
+ private readonly config;
6
+ private readonly bus;
7
+ readonly channels: Map<string, BaseChannel<unknown>>;
8
+ private dispatchLoop;
9
+ private stopping;
10
+ constructor(config: Config, bus: MessageBus);
11
+ private initChannels;
12
+ startAll(): Promise<void>;
13
+ stopAll(): Promise<void>;
14
+ private dispatchOutbound;
15
+ get enabledChannels(): string[];
16
+ getStatus(): Record<string, unknown>;
17
+ }
@@ -0,0 +1,67 @@
1
+ import { StubChannel } from "./stub.js";
2
+ import { TelegramChannel } from "./telegram.js";
3
+ import { DiscordChannel } from "./discord.js";
4
+ export class ChannelManager {
5
+ config;
6
+ bus;
7
+ channels = new Map();
8
+ dispatchLoop = null;
9
+ stopping = false;
10
+ constructor(config, bus) {
11
+ this.config = config;
12
+ this.bus = bus;
13
+ this.initChannels();
14
+ }
15
+ initChannels() {
16
+ const add = (name, enabled, channelConfig) => {
17
+ if (enabled)
18
+ this.channels.set(name, new StubChannel(name, channelConfig, this.bus));
19
+ };
20
+ if (this.config.channels.telegram.enabled) {
21
+ this.channels.set("telegram", new TelegramChannel(this.config.channels.telegram, this.bus));
22
+ }
23
+ add("whatsapp", this.config.channels.whatsapp.enabled, this.config.channels.whatsapp);
24
+ if (this.config.channels.discord.enabled) {
25
+ this.channels.set("discord", new DiscordChannel(this.config.channels.discord, this.bus));
26
+ }
27
+ add("feishu", this.config.channels.feishu.enabled, this.config.channels.feishu);
28
+ add("mochat", this.config.channels.mochat.enabled, this.config.channels.mochat);
29
+ add("dingtalk", this.config.channels.dingtalk.enabled, this.config.channels.dingtalk);
30
+ add("email", this.config.channels.email.enabled, this.config.channels.email);
31
+ add("slack", this.config.channels.slack.enabled, this.config.channels.slack);
32
+ add("qq", this.config.channels.qq.enabled, this.config.channels.qq);
33
+ add("matrix", this.config.channels.matrix.enabled, this.config.channels.matrix);
34
+ }
35
+ async startAll() {
36
+ if (!this.channels.size)
37
+ return;
38
+ this.stopping = false;
39
+ this.dispatchLoop = this.dispatchOutbound();
40
+ await Promise.all([...this.channels.values()].map((c) => c.start().catch(() => undefined)));
41
+ }
42
+ async stopAll() {
43
+ this.stopping = true;
44
+ await Promise.all([...this.channels.values()].map((c) => c.stop().catch(() => undefined)));
45
+ }
46
+ async dispatchOutbound() {
47
+ while (!this.stopping) {
48
+ const msg = await this.bus.consumeOutbound();
49
+ if (msg.metadata?._progress) {
50
+ const isTool = !!msg.metadata._tool_hint;
51
+ if (isTool && !this.config.channels.sendToolHints)
52
+ continue;
53
+ if (!isTool && !this.config.channels.sendProgress)
54
+ continue;
55
+ }
56
+ const ch = this.channels.get(msg.channel);
57
+ if (ch)
58
+ await ch.send(msg).catch(() => undefined);
59
+ }
60
+ }
61
+ get enabledChannels() {
62
+ return [...this.channels.keys()];
63
+ }
64
+ getStatus() {
65
+ return Object.fromEntries([...this.channels].map(([name, c]) => [name, { enabled: true, running: c.isRunning }]));
66
+ }
67
+ }
@@ -0,0 +1,10 @@
1
+ import type { OutboundMessage } from "../bus/events.js";
2
+ import type { MessageBus } from "../bus/queue.js";
3
+ import { BaseChannel } from "./base.js";
4
+ export declare class StubChannel extends BaseChannel<any> {
5
+ readonly name: string;
6
+ constructor(name: string, config: any, bus: MessageBus);
7
+ start(): Promise<void>;
8
+ stop(): Promise<void>;
9
+ send(_msg: OutboundMessage): Promise<void>;
10
+ }
@@ -0,0 +1,18 @@
1
+ import { BaseChannel } from "./base.js";
2
+ export class StubChannel extends BaseChannel {
3
+ name;
4
+ constructor(name, config, bus) {
5
+ super(config, bus);
6
+ this.name = name;
7
+ }
8
+ async start() {
9
+ this.running = true;
10
+ await new Promise(() => { });
11
+ }
12
+ async stop() {
13
+ this.running = false;
14
+ }
15
+ async send(_msg) {
16
+ // no-op
17
+ }
18
+ }
@@ -0,0 +1,20 @@
1
+ import type { OutboundMessage } from "../bus/events.js";
2
+ import type { MessageBus } from "../bus/queue.js";
3
+ import { BaseChannel } from "./base.js";
4
+ type TelegramConfig = {
5
+ enabled: boolean;
6
+ token: string;
7
+ allowFrom: string[];
8
+ proxy: string | null;
9
+ replyToMessage: boolean;
10
+ };
11
+ export declare class TelegramChannel extends BaseChannel<TelegramConfig> {
12
+ readonly name = "telegram";
13
+ private offset;
14
+ constructor(config: TelegramConfig, bus: MessageBus);
15
+ private api;
16
+ start(): Promise<void>;
17
+ stop(): Promise<void>;
18
+ send(msg: OutboundMessage): Promise<void>;
19
+ }
20
+ export {};