@fickydev/pigent 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/.env.example +22 -0
- package/AGENTS.md +242 -0
- package/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/PLAN.md +369 -0
- package/README.md +369 -0
- package/TODO.md +183 -0
- package/agents/coder/SYSTEM.md +3 -0
- package/agents/coder/agent.yaml +20 -0
- package/drizzle/migrations/0000_great_daredevil.sql +78 -0
- package/drizzle/migrations/meta/0000_snapshot.json +505 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/drizzle.config.ts +13 -0
- package/package.json +66 -0
- package/pigent.yaml +12 -0
- package/profiles/software-engineer.yaml +11 -0
- package/src/agents/AgentRunner.ts +112 -0
- package/src/agents/BotCommandHandler.ts +65 -0
- package/src/agents/MessageRouter.ts +106 -0
- package/src/channels/telegram/TelegramApi.ts +67 -0
- package/src/channels/telegram/TelegramPollingAdapter.ts +123 -0
- package/src/channels/telegram/types.ts +50 -0
- package/src/channels/types.ts +29 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/setup.ts +261 -0
- package/src/config/loadConfig.ts +115 -0
- package/src/config/schemas.ts +92 -0
- package/src/daemon/AgentDaemon.ts +161 -0
- package/src/db/client.ts +23 -0
- package/src/db/repositories/AgentRepository.ts +45 -0
- package/src/db/repositories/MessageRepository.ts +45 -0
- package/src/db/repositories/RuntimeKvRepository.ts +27 -0
- package/src/db/repositories/SessionRepository.ts +65 -0
- package/src/db/repositories/TelegramRepository.ts +98 -0
- package/src/db/repositories/index.ts +18 -0
- package/src/db/schema.ts +106 -0
- package/src/logging/logger.ts +45 -0
- package/src/main.ts +37 -0
- package/src/pi/PiAgentRunner.ts +73 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { InboundMessage } from "../channels/types";
|
|
2
|
+
import type { LoadedAgentConfig, LoadedConfig, ProfileConfig } from "../config/schemas";
|
|
3
|
+
import type { Repositories } from "../db/repositories";
|
|
4
|
+
import { logger } from "../logging/logger";
|
|
5
|
+
import { PiAgentRunner } from "../pi/PiAgentRunner";
|
|
6
|
+
|
|
7
|
+
export type AgentRunInput = {
|
|
8
|
+
agentId: string;
|
|
9
|
+
text: string;
|
|
10
|
+
message: InboundMessage;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class AgentRunner {
|
|
14
|
+
private readonly piRunner = new PiAgentRunner();
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly config: LoadedConfig,
|
|
18
|
+
private readonly repositories: Repositories,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
async run(input: AgentRunInput): Promise<string> {
|
|
22
|
+
const session = await this.repositories.sessions.getOrCreate({
|
|
23
|
+
agentId: input.agentId,
|
|
24
|
+
channel: input.message.channel,
|
|
25
|
+
chatId: input.message.chatId,
|
|
26
|
+
threadId: input.message.threadId,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await this.repositories.messages.create({
|
|
30
|
+
agentId: input.agentId,
|
|
31
|
+
sessionId: session.id,
|
|
32
|
+
channel: input.message.channel,
|
|
33
|
+
direction: "inbound",
|
|
34
|
+
senderId: input.message.senderId,
|
|
35
|
+
chatId: input.message.chatId,
|
|
36
|
+
threadId: input.message.threadId,
|
|
37
|
+
content: input.text,
|
|
38
|
+
rawJson: input.message.raw,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const response = await this.createResponse(input);
|
|
42
|
+
|
|
43
|
+
await this.repositories.messages.create({
|
|
44
|
+
agentId: input.agentId,
|
|
45
|
+
sessionId: session.id,
|
|
46
|
+
channel: input.message.channel,
|
|
47
|
+
direction: "outbound",
|
|
48
|
+
chatId: input.message.chatId,
|
|
49
|
+
threadId: input.message.threadId,
|
|
50
|
+
content: response,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async createResponse(input: AgentRunInput): Promise<string> {
|
|
57
|
+
if (process.env.PIGENT_FAKE_AGENT === "1") {
|
|
58
|
+
return this.fakeResponse(input.agentId, input.text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const agent = this.findAgent(input.agentId);
|
|
62
|
+
if (!agent) return `Unknown agent: ${input.agentId}`;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
return await this.piRunner.run({
|
|
66
|
+
agent,
|
|
67
|
+
profile: this.findProfile(agent.profile),
|
|
68
|
+
prompt: this.composePrompt(input),
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error("pi runner failed", {
|
|
72
|
+
agentId: input.agentId,
|
|
73
|
+
error: error instanceof Error ? error.message : String(error),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (process.env.PIGENT_FALLBACK_FAKE_AGENT === "1") {
|
|
77
|
+
return this.fakeResponse(input.agentId, input.text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return "Agent execution failed. Check daemon logs.";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private composePrompt(input: AgentRunInput): string {
|
|
85
|
+
return [
|
|
86
|
+
`[Channel]\n${input.message.channel}`,
|
|
87
|
+
`[Chat]\n${input.message.chatId}`,
|
|
88
|
+
input.message.threadId ? `[Thread]\n${input.message.threadId}` : null,
|
|
89
|
+
input.message.senderName || input.message.senderId
|
|
90
|
+
? `[User]\n${input.message.senderName ?? input.message.senderId}`
|
|
91
|
+
: null,
|
|
92
|
+
`[Message]\n${input.text}`,
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join("\n\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private fakeResponse(agentId: string, text: string): string {
|
|
99
|
+
const agent = this.findAgent(agentId);
|
|
100
|
+
const name = agent?.name ?? agentId;
|
|
101
|
+
|
|
102
|
+
return `[${name}] fake runner received: ${text || "(empty message)"}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private findAgent(agentId: string): LoadedAgentConfig | null {
|
|
106
|
+
return this.config.agents.find((candidate) => candidate.id === agentId) ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private findProfile(profileId: string): ProfileConfig | null {
|
|
110
|
+
return this.config.profiles.find((candidate) => candidate.id === profileId) ?? null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { InboundMessage } from "../channels/types";
|
|
2
|
+
import type { LoadedConfig } from "../config/schemas";
|
|
3
|
+
import type { Repositories } from "../db/repositories";
|
|
4
|
+
|
|
5
|
+
export type BotCommandResult =
|
|
6
|
+
| {
|
|
7
|
+
handled: true;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
handled: false;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class BotCommandHandler {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly config: LoadedConfig,
|
|
17
|
+
private readonly repositories: Repositories,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async handle(message: InboundMessage): Promise<BotCommandResult> {
|
|
21
|
+
const [command] = message.text.trim().split(/\s+/, 1);
|
|
22
|
+
|
|
23
|
+
switch (command) {
|
|
24
|
+
case "/help":
|
|
25
|
+
case "/start":
|
|
26
|
+
return { handled: true, text: this.helpText() };
|
|
27
|
+
case "/agents":
|
|
28
|
+
return { handled: true, text: await this.agentsText(message) };
|
|
29
|
+
default:
|
|
30
|
+
return { handled: false };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private helpText(): string {
|
|
35
|
+
return [
|
|
36
|
+
"Pigent commands:",
|
|
37
|
+
"/help - show this help",
|
|
38
|
+
"/agents - list agents for this chat",
|
|
39
|
+
"/agent <agentId> <message> - send message to specific agent",
|
|
40
|
+
"@agentId <message> - send message to specific agent",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async agentsText(message: InboundMessage): Promise<string> {
|
|
45
|
+
const chat = await this.repositories.telegram.findChat(message.chatId);
|
|
46
|
+
|
|
47
|
+
if (!chat) {
|
|
48
|
+
return "No agents configured for this chat.";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const allAgentIds = this.config.agents.map((agent) => agent.id);
|
|
52
|
+
const allowedAgentIds = [];
|
|
53
|
+
|
|
54
|
+
for (const agentId of allAgentIds) {
|
|
55
|
+
if (await this.repositories.telegram.isAgentAllowed(message.chatId, agentId)) {
|
|
56
|
+
allowedAgentIds.push(agentId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
`Default agent: ${chat.defaultAgentId ?? "none"}`,
|
|
62
|
+
`Allowed agents: ${allowedAgentIds.length > 0 ? allowedAgentIds.join(", ") : "none"}`,
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { InboundMessage } from "../channels/types";
|
|
2
|
+
import type { Repositories } from "../db/repositories";
|
|
3
|
+
|
|
4
|
+
export type RouteResult =
|
|
5
|
+
| {
|
|
6
|
+
ok: true;
|
|
7
|
+
agentId: string;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: "unknown_agent" | "disallowed_agent" | "no_default_agent" | "chat_disabled";
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class MessageRouter {
|
|
17
|
+
constructor(private readonly repositories: Repositories) {}
|
|
18
|
+
|
|
19
|
+
async route(message: InboundMessage): Promise<RouteResult> {
|
|
20
|
+
if (message.channel !== "telegram") {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: "no_default_agent",
|
|
24
|
+
message: "Unsupported channel.",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const chat = await this.repositories.telegram.findChat(message.chatId);
|
|
29
|
+
|
|
30
|
+
if (!chat) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
reason: "no_default_agent",
|
|
34
|
+
message: "No default agent configured for this chat.",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!chat.enabled) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
reason: "chat_disabled",
|
|
42
|
+
message: "This chat is disabled.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parsed = parseAgentCommand(message.text);
|
|
47
|
+
const agentId = parsed.agentId ?? chat.defaultAgentId;
|
|
48
|
+
|
|
49
|
+
if (!agentId) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: "no_default_agent",
|
|
53
|
+
message: "No default agent configured for this chat.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const agent = await this.repositories.agents.findById(agentId);
|
|
58
|
+
if (!agent) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
reason: "unknown_agent",
|
|
62
|
+
message: `Unknown agent: ${agentId}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const allowed = await this.repositories.telegram.isAgentAllowed(message.chatId, agentId);
|
|
67
|
+
if (!allowed) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: "disallowed_agent",
|
|
71
|
+
message: `Agent is not allowed in this chat: ${agentId}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
agentId,
|
|
78
|
+
text: parsed.text,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseAgentCommand(text: string): { agentId: string | null; text: string } {
|
|
84
|
+
const trimmed = text.trim();
|
|
85
|
+
|
|
86
|
+
const mentionMatch = trimmed.match(/^@([a-zA-Z0-9_-]+)\b\s*(.*)$/s);
|
|
87
|
+
if (mentionMatch) {
|
|
88
|
+
return {
|
|
89
|
+
agentId: mentionMatch[1],
|
|
90
|
+
text: mentionMatch[2]?.trim() ?? "",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const slashMatch = trimmed.match(/^\/agent\s+([a-zA-Z0-9_-]+)\b\s*(.*)$/s);
|
|
95
|
+
if (slashMatch) {
|
|
96
|
+
return {
|
|
97
|
+
agentId: slashMatch[1],
|
|
98
|
+
text: slashMatch[2]?.trim() ?? "",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
agentId: null,
|
|
104
|
+
text: trimmed,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { TelegramGetMeResponse, TelegramGetUpdatesResponse, TelegramSendMessageResponse, TelegramUpdate, TelegramUser } from "./types";
|
|
2
|
+
|
|
3
|
+
export type TelegramApiOptions = {
|
|
4
|
+
token: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type GetUpdatesOptions = {
|
|
9
|
+
offset?: number;
|
|
10
|
+
timeoutSeconds?: number;
|
|
11
|
+
limit?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class TelegramApi {
|
|
15
|
+
private readonly baseUrl: string;
|
|
16
|
+
|
|
17
|
+
constructor(options: TelegramApiOptions) {
|
|
18
|
+
this.baseUrl = options.baseUrl ?? `https://api.telegram.org/bot${options.token}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getMe(): Promise<TelegramUser> {
|
|
22
|
+
const response = await this.request<TelegramGetMeResponse>("getMe", {});
|
|
23
|
+
return response.result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getUpdates(options: GetUpdatesOptions = {}): Promise<TelegramUpdate[]> {
|
|
27
|
+
const response = await this.request<TelegramGetUpdatesResponse>("getUpdates", {
|
|
28
|
+
offset: options.offset,
|
|
29
|
+
timeout: options.timeoutSeconds ?? 30,
|
|
30
|
+
limit: options.limit ?? 50,
|
|
31
|
+
allowed_updates: ["message"],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return response.result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async sendMessage(input: { chatId: string; text: string; threadId?: string | null }): Promise<void> {
|
|
38
|
+
await this.request<TelegramSendMessageResponse>("sendMessage", {
|
|
39
|
+
chat_id: input.chatId,
|
|
40
|
+
text: input.text,
|
|
41
|
+
message_thread_id: input.threadId ? Number(input.threadId) : undefined,
|
|
42
|
+
disable_web_page_preview: true,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async request<T extends { ok: boolean; description?: string }>(method: string, body: unknown): Promise<T> {
|
|
47
|
+
const response = await fetch(`${this.baseUrl}/${method}`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
"content-type": "application/json",
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`telegram ${method} failed with HTTP ${response.status}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const payload = (await response.json()) as T;
|
|
60
|
+
|
|
61
|
+
if (!payload.ok) {
|
|
62
|
+
throw new Error(`telegram ${method} failed: ${payload.description ?? "unknown error"}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { RuntimeKvRepository } from "../../db/repositories/RuntimeKvRepository";
|
|
2
|
+
import { logger } from "../../logging/logger";
|
|
3
|
+
import type { ChannelAdapter, InboundMessage, MessageHandler, OutboundMessage } from "../types";
|
|
4
|
+
import { TelegramApi } from "./TelegramApi";
|
|
5
|
+
import type { TelegramMessage, TelegramUpdate } from "./types";
|
|
6
|
+
|
|
7
|
+
export type TelegramPollingAdapterOptions = {
|
|
8
|
+
api: TelegramApi;
|
|
9
|
+
runtimeKv: RuntimeKvRepository;
|
|
10
|
+
pollTimeoutSeconds?: number;
|
|
11
|
+
pollIntervalMs?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const offsetKey = "telegram:update_offset";
|
|
15
|
+
|
|
16
|
+
export class TelegramPollingAdapter implements ChannelAdapter {
|
|
17
|
+
readonly id = "telegram" as const;
|
|
18
|
+
|
|
19
|
+
private running = false;
|
|
20
|
+
private loopPromise: Promise<void> | null = null;
|
|
21
|
+
private botUserId: string | null = null;
|
|
22
|
+
|
|
23
|
+
constructor(private readonly options: TelegramPollingAdapterOptions) {}
|
|
24
|
+
|
|
25
|
+
async start(handler: MessageHandler): Promise<void> {
|
|
26
|
+
if (this.running) return;
|
|
27
|
+
|
|
28
|
+
const botUser = await this.options.api.getMe();
|
|
29
|
+
this.botUserId = String(botUser.id);
|
|
30
|
+
|
|
31
|
+
this.running = true;
|
|
32
|
+
this.loopPromise = this.poll(handler);
|
|
33
|
+
logger.info("telegram polling started", {
|
|
34
|
+
botUserId: this.botUserId,
|
|
35
|
+
username: botUser.username,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async stop(): Promise<void> {
|
|
40
|
+
if (!this.running) return;
|
|
41
|
+
|
|
42
|
+
this.running = false;
|
|
43
|
+
await this.loopPromise;
|
|
44
|
+
this.loopPromise = null;
|
|
45
|
+
logger.info("telegram polling stopped");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async send(message: OutboundMessage): Promise<void> {
|
|
49
|
+
if (message.channel !== "telegram") {
|
|
50
|
+
throw new Error(`unsupported channel for telegram adapter: ${message.channel}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await this.options.api.sendMessage({
|
|
54
|
+
chatId: message.chatId,
|
|
55
|
+
threadId: message.threadId,
|
|
56
|
+
text: message.text,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async poll(handler: MessageHandler): Promise<void> {
|
|
61
|
+
while (this.running) {
|
|
62
|
+
try {
|
|
63
|
+
const offset = await this.loadOffset();
|
|
64
|
+
const updates = await this.options.api.getUpdates({
|
|
65
|
+
offset,
|
|
66
|
+
timeoutSeconds: this.options.pollTimeoutSeconds ?? 30,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
for (const update of updates) {
|
|
70
|
+
await this.handleUpdate(update, handler);
|
|
71
|
+
await this.saveOffset(update.update_id + 1);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.error("telegram polling error", {
|
|
75
|
+
error: error instanceof Error ? error.message : String(error),
|
|
76
|
+
});
|
|
77
|
+
await sleep(this.options.pollIntervalMs ?? 1000);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async handleUpdate(update: TelegramUpdate, handler: MessageHandler): Promise<void> {
|
|
83
|
+
const message = update.message;
|
|
84
|
+
if (!message?.text) return;
|
|
85
|
+
if (message.from?.is_bot) return;
|
|
86
|
+
if (this.botUserId && message.from?.id && String(message.from.id) === this.botUserId) return;
|
|
87
|
+
|
|
88
|
+
const normalized = normalizeTelegramMessage(update.update_id, message);
|
|
89
|
+
await handler(normalized);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async loadOffset(): Promise<number | undefined> {
|
|
93
|
+
const value = await this.options.runtimeKv.get(offsetKey);
|
|
94
|
+
if (!value) return undefined;
|
|
95
|
+
|
|
96
|
+
const offset = Number(value);
|
|
97
|
+
return Number.isSafeInteger(offset) ? offset : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async saveOffset(offset: number): Promise<void> {
|
|
101
|
+
await this.options.runtimeKv.set(offsetKey, String(offset));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeTelegramMessage(updateId: number, message: TelegramMessage): InboundMessage {
|
|
106
|
+
const senderName = [message.from?.first_name, message.from?.last_name].filter(Boolean).join(" ");
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: String(updateId),
|
|
110
|
+
channel: "telegram",
|
|
111
|
+
chatId: String(message.chat.id),
|
|
112
|
+
threadId: message.message_thread_id ? String(message.message_thread_id) : null,
|
|
113
|
+
senderId: message.from ? String(message.from.id) : null,
|
|
114
|
+
senderName: senderName || message.from?.username || null,
|
|
115
|
+
text: message.text ?? "",
|
|
116
|
+
raw: message,
|
|
117
|
+
receivedAt: new Date(message.date * 1000),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sleep(ms: number): Promise<void> {
|
|
122
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type TelegramUser = {
|
|
2
|
+
id: number;
|
|
3
|
+
is_bot?: boolean;
|
|
4
|
+
first_name?: string;
|
|
5
|
+
last_name?: string;
|
|
6
|
+
username?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type TelegramChat = {
|
|
10
|
+
id: number;
|
|
11
|
+
type: "private" | "group" | "supergroup" | "channel";
|
|
12
|
+
title?: string;
|
|
13
|
+
username?: string;
|
|
14
|
+
first_name?: string;
|
|
15
|
+
last_name?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TelegramMessage = {
|
|
19
|
+
message_id: number;
|
|
20
|
+
message_thread_id?: number;
|
|
21
|
+
from?: TelegramUser;
|
|
22
|
+
sender_chat?: TelegramChat;
|
|
23
|
+
chat: TelegramChat;
|
|
24
|
+
date: number;
|
|
25
|
+
text?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type TelegramUpdate = {
|
|
29
|
+
update_id: number;
|
|
30
|
+
message?: TelegramMessage;
|
|
31
|
+
edited_message?: TelegramMessage;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type TelegramGetUpdatesResponse = {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
result: TelegramUpdate[];
|
|
37
|
+
description?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TelegramGetMeResponse = {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
result: TelegramUser;
|
|
43
|
+
description?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type TelegramSendMessageResponse = {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
result?: TelegramMessage;
|
|
49
|
+
description?: string;
|
|
50
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type ChannelId = "telegram";
|
|
2
|
+
|
|
3
|
+
export type InboundMessage = {
|
|
4
|
+
id: string;
|
|
5
|
+
channel: ChannelId;
|
|
6
|
+
chatId: string;
|
|
7
|
+
threadId?: string | null;
|
|
8
|
+
senderId?: string | null;
|
|
9
|
+
senderName?: string | null;
|
|
10
|
+
text: string;
|
|
11
|
+
raw: unknown;
|
|
12
|
+
receivedAt: Date;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type OutboundMessage = {
|
|
16
|
+
channel: ChannelId;
|
|
17
|
+
chatId: string;
|
|
18
|
+
threadId?: string | null;
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type MessageHandler = (message: InboundMessage) => Promise<void>;
|
|
23
|
+
|
|
24
|
+
export interface ChannelAdapter {
|
|
25
|
+
readonly id: ChannelId;
|
|
26
|
+
start(handler: MessageHandler): Promise<void>;
|
|
27
|
+
stop(): Promise<void>;
|
|
28
|
+
send(message: OutboundMessage): Promise<void>;
|
|
29
|
+
}
|
package/src/cli/run.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { cp, mkdir } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const flags = new Set(args.filter((arg) => arg.startsWith("--")));
|
|
10
|
+
const targetDirArg = args.find((arg) => !arg.startsWith("--"));
|
|
11
|
+
const forceSetup = flags.has("--setup") || flags.has("--reconfigure");
|
|
12
|
+
const skipInstall = flags.has("--skip-install");
|
|
13
|
+
const skipMigrate = flags.has("--skip-migrate");
|
|
14
|
+
const skipTypecheck = flags.has("--skip-typecheck");
|
|
15
|
+
|
|
16
|
+
async function main(): Promise<void> {
|
|
17
|
+
if (targetDirArg) {
|
|
18
|
+
const targetDir = resolve(process.cwd(), targetDirArg);
|
|
19
|
+
await ensureProject(targetDir);
|
|
20
|
+
process.chdir(targetDir);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { runSetup } = await import("./setup");
|
|
24
|
+
|
|
25
|
+
if (forceSetup || needsSetup(process.cwd())) {
|
|
26
|
+
await runSetup({ skipInstall, skipMigrate, skipTypecheck });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await $`bun run src/main.ts`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function ensureProject(targetDir: string): Promise<void> {
|
|
33
|
+
await mkdir(targetDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
if (existsSync(join(targetDir, "package.json"))) {
|
|
36
|
+
console.log(`[pigent] using existing project ${targetDir}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sourceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
41
|
+
console.log(`[pigent] creating project ${targetDir}`);
|
|
42
|
+
|
|
43
|
+
await cp(sourceRoot, targetDir, {
|
|
44
|
+
recursive: true,
|
|
45
|
+
force: false,
|
|
46
|
+
filter: (source) => shouldCopy(sourceRoot, source),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function shouldCopy(sourceRoot: string, source: string): boolean {
|
|
51
|
+
const relative = source.slice(sourceRoot.length).replace(/^\/+/, "");
|
|
52
|
+
if (!relative) return true;
|
|
53
|
+
|
|
54
|
+
const blocked = [
|
|
55
|
+
".git",
|
|
56
|
+
"node_modules",
|
|
57
|
+
"dist",
|
|
58
|
+
"coverage",
|
|
59
|
+
"pigent.db",
|
|
60
|
+
"pigent.db-shm",
|
|
61
|
+
"pigent.db-wal",
|
|
62
|
+
".env",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
return !blocked.some((entry) => relative === entry || relative.startsWith(`${entry}/`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function needsSetup(rootDir: string): boolean {
|
|
69
|
+
if (!existsSync(join(rootDir, ".env"))) return true;
|
|
70
|
+
if (!existsSync(join(rootDir, "pigent.db"))) return true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main().catch((error) => {
|
|
75
|
+
console.error("[pigent] failed", error instanceof Error ? error.message : error);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|