@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
package/src/cli/setup.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { $ } from "bun";
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
|
|
7
|
+
const rootDir = process.cwd();
|
|
8
|
+
const envPath = join(rootDir, ".env");
|
|
9
|
+
const envExamplePath = join(rootDir, ".env.example");
|
|
10
|
+
const pigentConfigPath = join(rootDir, "pigent.yaml");
|
|
11
|
+
|
|
12
|
+
export type SetupOptions = {
|
|
13
|
+
skipInstall?: boolean;
|
|
14
|
+
skipMigrate?: boolean;
|
|
15
|
+
skipTypecheck?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
19
|
+
const skipInstall = options.skipInstall ?? false;
|
|
20
|
+
const skipMigrate = options.skipMigrate ?? false;
|
|
21
|
+
const skipTypecheck = options.skipTypecheck ?? false;
|
|
22
|
+
|
|
23
|
+
banner();
|
|
24
|
+
|
|
25
|
+
const mode = select("Setup mode", ["quick", "custom"], "quick");
|
|
26
|
+
const isCustom = mode === "custom";
|
|
27
|
+
|
|
28
|
+
await ensureEnvFile();
|
|
29
|
+
await configureEnvironment(isCustom);
|
|
30
|
+
await configureTelegramChat(isCustom);
|
|
31
|
+
|
|
32
|
+
if (!skipInstall && confirm("Install dependencies?", true)) {
|
|
33
|
+
await runStep("Install dependencies", async () => {
|
|
34
|
+
await $`bun install`;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!skipMigrate && confirm("Apply database migrations?", true)) {
|
|
39
|
+
await runStep("Apply database migrations", async () => {
|
|
40
|
+
await $`bun run db:migrate`;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!skipTypecheck && confirm("Run typecheck?", true)) {
|
|
45
|
+
await runStep("Typecheck", async () => {
|
|
46
|
+
await $`bun run typecheck`;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
summary();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function banner(): void {
|
|
54
|
+
console.log("\nPigent interactive setup");
|
|
55
|
+
console.log("------------------------");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function ensureEnvFile(): Promise<void> {
|
|
59
|
+
if (existsSync(envPath)) {
|
|
60
|
+
log(".env exists");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await copyFile(envExamplePath, envPath);
|
|
65
|
+
log("Created .env from .env.example");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function configureEnvironment(isCustom: boolean): Promise<void> {
|
|
69
|
+
let env = await readFile(envPath, "utf8");
|
|
70
|
+
|
|
71
|
+
if (isCustom || !getEnvValue(env, "DATABASE_URL")) {
|
|
72
|
+
const databaseUrl = input("Database URL", getEnvValue(env, "DATABASE_URL") ?? "file:./pigent.db");
|
|
73
|
+
env = setEnvValue(env, "DATABASE_URL", databaseUrl);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const currentToken = getEnvValue(env, "TELEGRAM_BOT_TOKEN");
|
|
77
|
+
if (currentToken) {
|
|
78
|
+
log("TELEGRAM_BOT_TOKEN already set");
|
|
79
|
+
if (confirm("Replace Telegram bot token?", false)) {
|
|
80
|
+
const nextToken = input("Telegram bot token", currentToken);
|
|
81
|
+
env = setEnvValue(env, "TELEGRAM_BOT_TOKEN", nextToken);
|
|
82
|
+
}
|
|
83
|
+
} else if (confirm("Configure Telegram bot token now?", true)) {
|
|
84
|
+
const token = input("Telegram bot token", "");
|
|
85
|
+
if (token) {
|
|
86
|
+
env = setEnvValue(env, "TELEGRAM_BOT_TOKEN", token);
|
|
87
|
+
} else {
|
|
88
|
+
log("Telegram token skipped; Telegram polling disabled until TELEGRAM_BOT_TOKEN is set");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (confirm("Auto-configure new Telegram chats?", getEnvValue(env, "PIGENT_AUTO_SETUP_CHATS") === "1")) {
|
|
93
|
+
env = setEnvValue(env, "PIGENT_AUTO_SETUP_CHATS", "1");
|
|
94
|
+
env = setEnvValue(
|
|
95
|
+
env,
|
|
96
|
+
"PIGENT_AUTO_SETUP_DEFAULT_AGENT",
|
|
97
|
+
input("Auto-setup default agent", getEnvValue(env, "PIGENT_AUTO_SETUP_DEFAULT_AGENT") ?? "coder"),
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
env = setEnvValue(env, "PIGENT_AUTO_SETUP_CHATS", "0");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isCustom) {
|
|
104
|
+
env = setEnvValue(env, "LOG_LEVEL", select("Log level", ["debug", "info", "warn", "error"], getEnvValue(env, "LOG_LEVEL") ?? "info"));
|
|
105
|
+
env = setEnvValue(
|
|
106
|
+
env,
|
|
107
|
+
"TELEGRAM_POLL_TIMEOUT_SECONDS",
|
|
108
|
+
input("Telegram poll timeout seconds", getEnvValue(env, "TELEGRAM_POLL_TIMEOUT_SECONDS") ?? "30"),
|
|
109
|
+
);
|
|
110
|
+
env = setEnvValue(
|
|
111
|
+
env,
|
|
112
|
+
"TELEGRAM_POLL_INTERVAL_MS",
|
|
113
|
+
input("Telegram poll error backoff ms", getEnvValue(env, "TELEGRAM_POLL_INTERVAL_MS") ?? "1000"),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await writeFile(envPath, env);
|
|
118
|
+
log("Environment configured");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function configureTelegramChat(isCustom: boolean): Promise<void> {
|
|
122
|
+
if (!existsSync(pigentConfigPath)) {
|
|
123
|
+
await writeFile(pigentConfigPath, "telegramChats: []\n");
|
|
124
|
+
log("Created pigent.yaml");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!confirm("Configure Telegram chat routing now? Requires chat id.", false)) {
|
|
128
|
+
log("Telegram chat config skipped");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const chatId = input("Telegram chat id", "");
|
|
133
|
+
if (!chatId) {
|
|
134
|
+
log("No chat id entered; skipped chat config");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const title = input("Chat title", "");
|
|
139
|
+
const defaultAgent = input("Default agent", "coder") || "coder";
|
|
140
|
+
const allowedAgentsInput = input("Allowed agents, comma-separated", defaultAgent);
|
|
141
|
+
const instructions = isCustom ? inputMultiline("Chat instructions", "") : "";
|
|
142
|
+
const allowedAgents = allowedAgentsInput
|
|
143
|
+
.split(",")
|
|
144
|
+
.map((agent) => agent.trim())
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
|
|
147
|
+
const content = await readFile(pigentConfigPath, "utf8");
|
|
148
|
+
const parsed = YAML.parse(content || "telegramChats: []\n") as { telegramChats?: Array<Record<string, unknown>> } | null;
|
|
149
|
+
const root = parsed ?? {};
|
|
150
|
+
const chats = Array.isArray(root.telegramChats) ? root.telegramChats : [];
|
|
151
|
+
const existingIndex = chats.findIndex((chat) => chat.chatId === chatId);
|
|
152
|
+
const nextChat = {
|
|
153
|
+
chatId,
|
|
154
|
+
title: title || undefined,
|
|
155
|
+
defaultAgent,
|
|
156
|
+
allowedAgents: allowedAgents.length > 0 ? allowedAgents : [defaultAgent],
|
|
157
|
+
instructions,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (existingIndex >= 0) {
|
|
161
|
+
chats[existingIndex] = {
|
|
162
|
+
...chats[existingIndex],
|
|
163
|
+
...nextChat,
|
|
164
|
+
};
|
|
165
|
+
} else {
|
|
166
|
+
chats.push(nextChat);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
root.telegramChats = chats;
|
|
170
|
+
await mkdir(dirname(pigentConfigPath), { recursive: true });
|
|
171
|
+
await writeFile(pigentConfigPath, YAML.stringify(root));
|
|
172
|
+
log(`Configured Telegram chat ${chatId}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function runStep(name: string, step: () => Promise<void>): Promise<void> {
|
|
176
|
+
log(name);
|
|
177
|
+
await step();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function input(label: string, defaultValue: string): string {
|
|
181
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
182
|
+
const value = prompt(`${label}${suffix}:`)?.trim();
|
|
183
|
+
return value || defaultValue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function inputMultiline(label: string, defaultValue: string): string {
|
|
187
|
+
console.log(`${label} (finish with a single . line):`);
|
|
188
|
+
const lines: string[] = [];
|
|
189
|
+
|
|
190
|
+
while (true) {
|
|
191
|
+
const value = prompt(">") ?? "";
|
|
192
|
+
if (value === ".") break;
|
|
193
|
+
lines.push(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = lines.join("\n").trim();
|
|
197
|
+
return result || defaultValue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function confirm(label: string, defaultValue: boolean): boolean {
|
|
201
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
202
|
+
const value = prompt(`${label} [${suffix}]`)?.trim().toLowerCase();
|
|
203
|
+
|
|
204
|
+
if (!value) return defaultValue;
|
|
205
|
+
return value === "y" || value === "yes";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function select<T extends string>(label: string, options: T[], defaultValue: T): T {
|
|
209
|
+
const value = input(`${label} (${options.join("/")})`, defaultValue);
|
|
210
|
+
return options.includes(value as T) ? (value as T) : defaultValue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getEnvValue(content: string, key: string): string | null {
|
|
214
|
+
const line = content
|
|
215
|
+
.split("\n")
|
|
216
|
+
.find((candidate) => candidate.trim().startsWith(`${key}=`));
|
|
217
|
+
|
|
218
|
+
if (!line) return null;
|
|
219
|
+
|
|
220
|
+
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
221
|
+
return value || null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function setEnvValue(content: string, key: string, value: string): string {
|
|
225
|
+
const lines = content.split("\n");
|
|
226
|
+
const index = lines.findIndex((line) => line.trim().startsWith(`${key}=`));
|
|
227
|
+
const nextLine = `${key}=${value}`;
|
|
228
|
+
|
|
229
|
+
if (index >= 0) {
|
|
230
|
+
lines[index] = nextLine;
|
|
231
|
+
} else {
|
|
232
|
+
lines.push(nextLine);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function summary(): void {
|
|
239
|
+
console.log("\nSetup complete");
|
|
240
|
+
console.log("--------------");
|
|
241
|
+
console.log("Start daemon: bun run start");
|
|
242
|
+
console.log("Dev mode: bun run dev");
|
|
243
|
+
console.log("Typecheck: bun run typecheck");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function log(message: string): void {
|
|
247
|
+
console.log(`[setup] ${message}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (import.meta.main) {
|
|
251
|
+
const flags = new Set(process.argv.slice(2));
|
|
252
|
+
|
|
253
|
+
runSetup({
|
|
254
|
+
skipInstall: flags.has("--skip-install"),
|
|
255
|
+
skipMigrate: flags.has("--skip-migrate"),
|
|
256
|
+
skipTypecheck: flags.has("--skip-typecheck"),
|
|
257
|
+
}).catch((error) => {
|
|
258
|
+
console.error("[setup] failed", error instanceof Error ? error.message : error);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import {
|
|
6
|
+
AgentConfigSchema,
|
|
7
|
+
type LoadedAgentConfig,
|
|
8
|
+
type LoadedConfig,
|
|
9
|
+
ProfileConfigSchema,
|
|
10
|
+
RootConfigSchema,
|
|
11
|
+
} from "./schemas";
|
|
12
|
+
|
|
13
|
+
export type LoadConfigOptions = {
|
|
14
|
+
rootDir?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function readYamlFile(path: string): Promise<unknown> {
|
|
18
|
+
const content = await readFile(path, "utf8");
|
|
19
|
+
return YAML.parse(content) as unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readOptionalTextFile(path: string): Promise<string> {
|
|
23
|
+
try {
|
|
24
|
+
return await readFile(path, "utf8");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadAgents(rootDir: string): Promise<LoadedAgentConfig[]> {
|
|
35
|
+
const agentsDir = join(rootDir, "agents");
|
|
36
|
+
const entries = await readdir(agentsDir, { withFileTypes: true }).catch(() => []);
|
|
37
|
+
const agents: LoadedAgentConfig[] = [];
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (!entry.isDirectory()) continue;
|
|
41
|
+
|
|
42
|
+
const baseDir = join(agentsDir, entry.name);
|
|
43
|
+
const configPath = join(baseDir, "agent.yaml");
|
|
44
|
+
const parsed = await readYamlFile(configPath);
|
|
45
|
+
const agent = AgentConfigSchema.parse(parsed);
|
|
46
|
+
const systemPromptPath = agent.systemPromptFile ? resolve(baseDir, agent.systemPromptFile) : join(baseDir, "SYSTEM.md");
|
|
47
|
+
const systemPrompt = await readOptionalTextFile(systemPromptPath);
|
|
48
|
+
|
|
49
|
+
agents.push({
|
|
50
|
+
...agent,
|
|
51
|
+
workspace: expandPath(agent.workspace),
|
|
52
|
+
baseDir,
|
|
53
|
+
systemPrompt,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return agents;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function loadProfiles(rootDir: string) {
|
|
61
|
+
const profilesDir = join(rootDir, "profiles");
|
|
62
|
+
const entries = await readdir(profilesDir, { withFileTypes: true }).catch(() => []);
|
|
63
|
+
const profiles = [];
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (!entry.isFile()) continue;
|
|
67
|
+
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
68
|
+
|
|
69
|
+
const configPath = join(profilesDir, entry.name);
|
|
70
|
+
const parsed = await readYamlFile(configPath);
|
|
71
|
+
profiles.push(ProfileConfigSchema.parse(parsed));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return profiles;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function loadRootConfig(rootDir: string) {
|
|
78
|
+
const candidates = ["pigent.yaml", "pigent.yml"];
|
|
79
|
+
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = await readYamlFile(join(rootDir, candidate));
|
|
83
|
+
return RootConfigSchema.parse(parsed);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return RootConfigSchema.parse({});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function expandPath(path: string): string {
|
|
97
|
+
if (path === "~") return homedir();
|
|
98
|
+
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function loadConfig(options: LoadConfigOptions = {}): Promise<LoadedConfig> {
|
|
103
|
+
const rootDir = resolve(options.rootDir ?? process.cwd());
|
|
104
|
+
const [agents, profiles, rootConfig] = await Promise.all([
|
|
105
|
+
loadAgents(rootDir),
|
|
106
|
+
loadProfiles(rootDir),
|
|
107
|
+
loadRootConfig(rootDir),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
agents,
|
|
112
|
+
profiles,
|
|
113
|
+
telegramChats: rootConfig.telegramChats,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const relativeOrAbsolutePathSchema = z.string().min(1);
|
|
4
|
+
|
|
5
|
+
export const PermissionConfigSchema = z.object({
|
|
6
|
+
canRunShell: z.boolean().default(false),
|
|
7
|
+
canEditFiles: z.boolean().default(false),
|
|
8
|
+
allowedPaths: z.array(z.string()).default([]),
|
|
9
|
+
blockedTools: z.array(z.string()).default([]),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const HeartbeatConfigSchema = z.object({
|
|
13
|
+
enabled: z.boolean().default(false),
|
|
14
|
+
intervalMs: z.number().int().positive().default(600_000),
|
|
15
|
+
prompt: z.string().min(1).default("If no useful action is needed, reply exactly: NOOP."),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const AgentTelegramChannelConfigSchema = z.object({
|
|
19
|
+
enabled: z.boolean().default(false),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const AgentChannelsConfigSchema = z.object({
|
|
23
|
+
telegram: AgentTelegramChannelConfigSchema.default({ enabled: false }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const AgentConfigSchema = z.object({
|
|
27
|
+
id: z.string().min(1).regex(/^[a-zA-Z0-9_-]+$/),
|
|
28
|
+
name: z.string().min(1),
|
|
29
|
+
profile: z.string().min(1),
|
|
30
|
+
workspace: relativeOrAbsolutePathSchema,
|
|
31
|
+
systemPromptFile: relativeOrAbsolutePathSchema.optional(),
|
|
32
|
+
skills: z.array(z.string()).default([]),
|
|
33
|
+
extensions: z.array(z.string()).default([]),
|
|
34
|
+
channels: AgentChannelsConfigSchema.default({ telegram: { enabled: false } }),
|
|
35
|
+
heartbeat: HeartbeatConfigSchema.default({
|
|
36
|
+
enabled: false,
|
|
37
|
+
intervalMs: 600_000,
|
|
38
|
+
prompt: "If no useful action is needed, reply exactly: NOOP.",
|
|
39
|
+
}),
|
|
40
|
+
permissions: PermissionConfigSchema.default({
|
|
41
|
+
canRunShell: false,
|
|
42
|
+
canEditFiles: false,
|
|
43
|
+
allowedPaths: [],
|
|
44
|
+
blockedTools: [],
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const ProfileConfigSchema = z.object({
|
|
49
|
+
id: z.string().min(1).regex(/^[a-zA-Z0-9_-]+$/),
|
|
50
|
+
name: z.string().min(1),
|
|
51
|
+
model: z.string().min(1).nullable().default(null),
|
|
52
|
+
instructions: z.string().default(""),
|
|
53
|
+
defaultSkills: z.array(z.string()).default([]),
|
|
54
|
+
defaultExtensions: z.array(z.string()).default([]),
|
|
55
|
+
permissions: PermissionConfigSchema.default({
|
|
56
|
+
canRunShell: false,
|
|
57
|
+
canEditFiles: false,
|
|
58
|
+
allowedPaths: [],
|
|
59
|
+
blockedTools: [],
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const TelegramChatConfigSchema = z.object({
|
|
64
|
+
chatId: z.string().min(1),
|
|
65
|
+
title: z.string().optional(),
|
|
66
|
+
defaultAgent: z.string().min(1),
|
|
67
|
+
allowedAgents: z.array(z.string()).default([]),
|
|
68
|
+
instructions: z.string().default(""),
|
|
69
|
+
enabled: z.boolean().default(true),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const RootConfigSchema = z.object({
|
|
73
|
+
telegramChats: z.array(TelegramChatConfigSchema).default([]),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export type PermissionConfig = z.infer<typeof PermissionConfigSchema>;
|
|
77
|
+
export type HeartbeatConfig = z.infer<typeof HeartbeatConfigSchema>;
|
|
78
|
+
export type AgentConfig = z.infer<typeof AgentConfigSchema>;
|
|
79
|
+
export type ProfileConfig = z.infer<typeof ProfileConfigSchema>;
|
|
80
|
+
export type TelegramChatConfig = z.infer<typeof TelegramChatConfigSchema>;
|
|
81
|
+
export type RootConfig = z.infer<typeof RootConfigSchema>;
|
|
82
|
+
|
|
83
|
+
export type LoadedAgentConfig = AgentConfig & {
|
|
84
|
+
baseDir: string;
|
|
85
|
+
systemPrompt: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type LoadedConfig = {
|
|
89
|
+
agents: LoadedAgentConfig[];
|
|
90
|
+
profiles: ProfileConfig[];
|
|
91
|
+
telegramChats: TelegramChatConfig[];
|
|
92
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { AgentRunner } from "../agents/AgentRunner";
|
|
2
|
+
import { BotCommandHandler } from "../agents/BotCommandHandler";
|
|
3
|
+
import { MessageRouter } from "../agents/MessageRouter";
|
|
4
|
+
import { TelegramApi } from "../channels/telegram/TelegramApi";
|
|
5
|
+
import { TelegramPollingAdapter } from "../channels/telegram/TelegramPollingAdapter";
|
|
6
|
+
import type { ChannelAdapter, InboundMessage, OutboundMessage } from "../channels/types";
|
|
7
|
+
import { loadConfig } from "../config/loadConfig";
|
|
8
|
+
import type { LoadedConfig } from "../config/schemas";
|
|
9
|
+
import { createRepositories, type Repositories } from "../db/repositories";
|
|
10
|
+
import { logger } from "../logging/logger";
|
|
11
|
+
|
|
12
|
+
export class AgentDaemon {
|
|
13
|
+
private running = false;
|
|
14
|
+
private readonly adapters: ChannelAdapter[];
|
|
15
|
+
private readonly commands: BotCommandHandler;
|
|
16
|
+
private readonly router: MessageRouter;
|
|
17
|
+
private readonly runner: AgentRunner;
|
|
18
|
+
|
|
19
|
+
private constructor(
|
|
20
|
+
private readonly config: LoadedConfig,
|
|
21
|
+
private readonly repositories: Repositories,
|
|
22
|
+
) {
|
|
23
|
+
this.adapters = createAdapters(repositories);
|
|
24
|
+
this.commands = new BotCommandHandler(config, repositories);
|
|
25
|
+
this.router = new MessageRouter(repositories);
|
|
26
|
+
this.runner = new AgentRunner(config, repositories);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static async create(): Promise<AgentDaemon> {
|
|
30
|
+
const config = await loadConfig();
|
|
31
|
+
return new AgentDaemon(config, createRepositories());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async start(): Promise<void> {
|
|
35
|
+
if (this.running) return;
|
|
36
|
+
|
|
37
|
+
await this.syncConfiguredState();
|
|
38
|
+
|
|
39
|
+
this.running = true;
|
|
40
|
+
|
|
41
|
+
for (const adapter of this.adapters) {
|
|
42
|
+
await adapter.start((message) => this.handleInboundMessage(message));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.info("pigent daemon started", {
|
|
46
|
+
agents: this.config.agents.length,
|
|
47
|
+
profiles: this.config.profiles.length,
|
|
48
|
+
telegramChats: this.config.telegramChats.length,
|
|
49
|
+
adapters: this.adapters.map((adapter) => adapter.id),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async stop(): Promise<void> {
|
|
54
|
+
if (!this.running) return;
|
|
55
|
+
|
|
56
|
+
this.running = false;
|
|
57
|
+
|
|
58
|
+
for (const adapter of this.adapters) {
|
|
59
|
+
await adapter.stop();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logger.info("pigent daemon stopped");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async syncConfiguredState(): Promise<void> {
|
|
66
|
+
for (const agent of this.config.agents) {
|
|
67
|
+
await this.repositories.agents.upsertLoadedAgent(agent);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const chat of this.config.telegramChats) {
|
|
71
|
+
await this.repositories.telegram.upsertConfiguredChat(chat);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async handleInboundMessage(message: InboundMessage): Promise<void> {
|
|
76
|
+
logger.info("inbound message received", {
|
|
77
|
+
channel: message.channel,
|
|
78
|
+
chatId: message.chatId,
|
|
79
|
+
threadId: message.threadId,
|
|
80
|
+
senderId: message.senderId,
|
|
81
|
+
textLength: message.text.length,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await this.autoConfigureChat(message);
|
|
85
|
+
|
|
86
|
+
const command = await this.commands.handle(message);
|
|
87
|
+
|
|
88
|
+
if (command.handled) {
|
|
89
|
+
await this.send({
|
|
90
|
+
channel: message.channel,
|
|
91
|
+
chatId: message.chatId,
|
|
92
|
+
threadId: message.threadId,
|
|
93
|
+
text: command.text,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const route = await this.router.route(message);
|
|
99
|
+
|
|
100
|
+
if (!route.ok) {
|
|
101
|
+
await this.send({
|
|
102
|
+
channel: message.channel,
|
|
103
|
+
chatId: message.chatId,
|
|
104
|
+
threadId: message.threadId,
|
|
105
|
+
text: route.message,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const response = await this.runner.run({
|
|
111
|
+
agentId: route.agentId,
|
|
112
|
+
text: route.text,
|
|
113
|
+
message,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await this.send({
|
|
117
|
+
channel: message.channel,
|
|
118
|
+
chatId: message.chatId,
|
|
119
|
+
threadId: message.threadId,
|
|
120
|
+
text: response,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async autoConfigureChat(message: InboundMessage): Promise<void> {
|
|
125
|
+
if (message.channel !== "telegram") return;
|
|
126
|
+
if (process.env.PIGENT_AUTO_SETUP_CHATS !== "1") return;
|
|
127
|
+
|
|
128
|
+
const defaultAgentId = process.env.PIGENT_AUTO_SETUP_DEFAULT_AGENT ?? this.config.agents[0]?.id;
|
|
129
|
+
if (!defaultAgentId) return;
|
|
130
|
+
|
|
131
|
+
await this.repositories.telegram.autoConfigureChat(message, defaultAgentId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async send(message: OutboundMessage): Promise<void> {
|
|
135
|
+
const adapter = this.adapters.find((candidate) => candidate.id === message.channel);
|
|
136
|
+
if (!adapter) {
|
|
137
|
+
logger.warn("no adapter available for outbound message", { channel: message.channel });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await adapter.send(message);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function createAdapters(repositories: Repositories): ChannelAdapter[] {
|
|
146
|
+
const adapters: ChannelAdapter[] = [];
|
|
147
|
+
const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
148
|
+
|
|
149
|
+
if (telegramToken) {
|
|
150
|
+
adapters.push(
|
|
151
|
+
new TelegramPollingAdapter({
|
|
152
|
+
api: new TelegramApi({ token: telegramToken }),
|
|
153
|
+
runtimeKv: repositories.runtimeKv,
|
|
154
|
+
pollTimeoutSeconds: Number(process.env.TELEGRAM_POLL_TIMEOUT_SECONDS ?? 30),
|
|
155
|
+
pollIntervalMs: Number(process.env.TELEGRAM_POLL_INTERVAL_MS ?? 1000),
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return adapters;
|
|
161
|
+
}
|
package/src/db/client.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
import * as schema from "./schema";
|
|
6
|
+
|
|
7
|
+
function databasePathFromEnv(): string {
|
|
8
|
+
const raw = process.env.DATABASE_URL ?? "file:./pigent.db";
|
|
9
|
+
const withoutPrefix = raw.startsWith("file:") ? raw.slice("file:".length) : raw;
|
|
10
|
+
return resolve(withoutPrefix);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const databasePath = databasePathFromEnv();
|
|
14
|
+
mkdirSync(dirname(databasePath), { recursive: true });
|
|
15
|
+
|
|
16
|
+
export const sqlite = new Database(databasePath);
|
|
17
|
+
export const db = drizzle(sqlite, { schema });
|
|
18
|
+
|
|
19
|
+
export type DbClient = typeof db;
|
|
20
|
+
|
|
21
|
+
export function getDatabasePath(): string {
|
|
22
|
+
return databasePath;
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import type { LoadedAgentConfig } from "../../config/schemas";
|
|
3
|
+
import type { DbClient } from "../client";
|
|
4
|
+
import { agents, type AgentRow } from "../schema";
|
|
5
|
+
|
|
6
|
+
export class AgentRepository {
|
|
7
|
+
constructor(private readonly db: DbClient) {}
|
|
8
|
+
|
|
9
|
+
async upsertLoadedAgent(agent: LoadedAgentConfig): Promise<void> {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
const configJson = JSON.stringify(agent);
|
|
12
|
+
|
|
13
|
+
await this.db
|
|
14
|
+
.insert(agents)
|
|
15
|
+
.values({
|
|
16
|
+
id: agent.id,
|
|
17
|
+
name: agent.name,
|
|
18
|
+
profile: agent.profile,
|
|
19
|
+
workspace: agent.workspace,
|
|
20
|
+
configJson,
|
|
21
|
+
systemPrompt: agent.systemPrompt,
|
|
22
|
+
createdAt: now,
|
|
23
|
+
updatedAt: now,
|
|
24
|
+
})
|
|
25
|
+
.onConflictDoUpdate({
|
|
26
|
+
target: agents.id,
|
|
27
|
+
set: {
|
|
28
|
+
name: agent.name,
|
|
29
|
+
profile: agent.profile,
|
|
30
|
+
workspace: agent.workspace,
|
|
31
|
+
configJson,
|
|
32
|
+
systemPrompt: agent.systemPrompt,
|
|
33
|
+
updatedAt: now,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async findById(id: string): Promise<AgentRow | null> {
|
|
39
|
+
const row = await this.db.query.agents.findFirst({
|
|
40
|
+
where: eq(agents.id, id),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return row ?? null;
|
|
44
|
+
}
|
|
45
|
+
}
|