@aaroncql/pim-agent 0.0.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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/bin/pim.ts +109 -0
  4. package/package.json +49 -0
  5. package/src/extensions/_init/index.ts +109 -0
  6. package/src/extensions/bash/capture.test.ts +126 -0
  7. package/src/extensions/bash/capture.ts +80 -0
  8. package/src/extensions/bash/format.test.ts +240 -0
  9. package/src/extensions/bash/format.ts +76 -0
  10. package/src/extensions/bash/index.ts +86 -0
  11. package/src/extensions/bash/run.test.ts +262 -0
  12. package/src/extensions/bash/run.ts +207 -0
  13. package/src/extensions/bash/schema.ts +54 -0
  14. package/src/extensions/command-picker/index.ts +52 -0
  15. package/src/extensions/command-picker/ranker.test.ts +46 -0
  16. package/src/extensions/command-picker/ranker.ts +17 -0
  17. package/src/extensions/edit/edit.test.ts +285 -0
  18. package/src/extensions/edit/edit.ts +382 -0
  19. package/src/extensions/edit/index.ts +54 -0
  20. package/src/extensions/edit/schema.ts +37 -0
  21. package/src/extensions/file-picker/catalog.test.ts +263 -0
  22. package/src/extensions/file-picker/catalog.ts +219 -0
  23. package/src/extensions/file-picker/index.test.ts +168 -0
  24. package/src/extensions/file-picker/index.ts +119 -0
  25. package/src/extensions/file-picker/ranker.test.ts +94 -0
  26. package/src/extensions/file-picker/ranker.ts +76 -0
  27. package/src/extensions/footer/git.test.ts +76 -0
  28. package/src/extensions/footer/git.ts +87 -0
  29. package/src/extensions/footer/index.test.ts +161 -0
  30. package/src/extensions/footer/index.ts +148 -0
  31. package/src/extensions/footer/powerline.ts +87 -0
  32. package/src/extensions/footer/segments.test.ts +164 -0
  33. package/src/extensions/footer/segments.ts +234 -0
  34. package/src/extensions/glob/glob.test.ts +171 -0
  35. package/src/extensions/glob/glob.ts +34 -0
  36. package/src/extensions/glob/index.test.ts +68 -0
  37. package/src/extensions/glob/index.ts +136 -0
  38. package/src/extensions/glob/render.test.ts +126 -0
  39. package/src/extensions/glob/render.ts +74 -0
  40. package/src/extensions/glob/schema.ts +52 -0
  41. package/src/extensions/grep/grep.test.ts +387 -0
  42. package/src/extensions/grep/grep.ts +215 -0
  43. package/src/extensions/grep/index.test.ts +68 -0
  44. package/src/extensions/grep/index.ts +158 -0
  45. package/src/extensions/grep/render.test.ts +269 -0
  46. package/src/extensions/grep/render.ts +243 -0
  47. package/src/extensions/grep/schema.ts +92 -0
  48. package/src/extensions/read/index.ts +84 -0
  49. package/src/extensions/read/read.test.ts +177 -0
  50. package/src/extensions/read/read.ts +206 -0
  51. package/src/extensions/read/render.test.ts +61 -0
  52. package/src/extensions/read/render.ts +33 -0
  53. package/src/extensions/read/schema.ts +27 -0
  54. package/src/extensions/subagent/index.test.ts +44 -0
  55. package/src/extensions/subagent/index.ts +30 -0
  56. package/src/extensions/subagent/render.test.ts +292 -0
  57. package/src/extensions/subagent/render.ts +359 -0
  58. package/src/extensions/subagent/schema.ts +9 -0
  59. package/src/extensions/subagent/subagent.test.ts +315 -0
  60. package/src/extensions/subagent/subagent.ts +418 -0
  61. package/src/extensions/system-prompt/index.ts +28 -0
  62. package/src/extensions/system-prompt/prompt.test.ts +64 -0
  63. package/src/extensions/system-prompt/prompt.ts +213 -0
  64. package/src/extensions/todo/index.test.ts +244 -0
  65. package/src/extensions/todo/index.ts +122 -0
  66. package/src/extensions/todo/render.test.ts +180 -0
  67. package/src/extensions/todo/render.ts +172 -0
  68. package/src/extensions/todo/schema.ts +24 -0
  69. package/src/extensions/todo/todo.test.ts +222 -0
  70. package/src/extensions/todo/todo.ts +188 -0
  71. package/src/extensions/tps/index.test.ts +254 -0
  72. package/src/extensions/tps/index.ts +136 -0
  73. package/src/extensions/web-fetch/JinaReaderClient.ts +230 -0
  74. package/src/extensions/web-fetch/WebViewFetchClient.ts +186 -0
  75. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +119 -0
  76. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +511 -0
  77. package/src/extensions/web-fetch/fetch.test.ts +244 -0
  78. package/src/extensions/web-fetch/fetch.ts +249 -0
  79. package/src/extensions/web-fetch/index.ts +107 -0
  80. package/src/extensions/web-fetch/render.test.ts +56 -0
  81. package/src/extensions/web-fetch/render.ts +39 -0
  82. package/src/extensions/web-fetch/schema.ts +23 -0
  83. package/src/extensions/web-search/ExaMcpClient.test.ts +143 -0
  84. package/src/extensions/web-search/ExaMcpClient.ts +258 -0
  85. package/src/extensions/web-search/index.ts +118 -0
  86. package/src/extensions/web-search/render.test.ts +21 -0
  87. package/src/extensions/web-search/render.ts +9 -0
  88. package/src/extensions/web-search/schema.ts +21 -0
  89. package/src/extensions/web-search/search.test.ts +53 -0
  90. package/src/extensions/web-search/search.ts +23 -0
  91. package/src/extensions/working-indicator/index.test.ts +21 -0
  92. package/src/extensions/working-indicator/index.ts +77 -0
  93. package/src/extensions/write/index.ts +76 -0
  94. package/src/extensions/write/render.test.ts +64 -0
  95. package/src/extensions/write/schema.ts +14 -0
  96. package/src/extensions/write/write.test.ts +108 -0
  97. package/src/extensions/write/write.ts +104 -0
  98. package/src/shared/DiffLines.test.ts +193 -0
  99. package/src/shared/DiffLines.ts +307 -0
  100. package/src/shared/DiffRenderer.test.ts +206 -0
  101. package/src/shared/DiffRenderer.ts +396 -0
  102. package/src/shared/DiffView.ts +199 -0
  103. package/src/shared/EditMatcher.test.ts +123 -0
  104. package/src/shared/EditMatcher.ts +826 -0
  105. package/src/shared/FileScanner.test.ts +158 -0
  106. package/src/shared/FileScanner.ts +41 -0
  107. package/src/shared/Fs.ts +46 -0
  108. package/src/shared/FsErrors.ts +72 -0
  109. package/src/shared/FuzzyMatcher.test.ts +114 -0
  110. package/src/shared/FuzzyMatcher.ts +73 -0
  111. package/src/shared/GitignoreFilter.test.ts +64 -0
  112. package/src/shared/GitignoreFilter.ts +142 -0
  113. package/src/shared/GlobExclusions.ts +23 -0
  114. package/src/shared/Levenshtein.ts +33 -0
  115. package/src/shared/Lines.test.ts +25 -0
  116. package/src/shared/Lines.ts +77 -0
  117. package/src/shared/McpClient.test.ts +235 -0
  118. package/src/shared/McpClient.ts +406 -0
  119. package/src/shared/OutputBudget.test.ts +99 -0
  120. package/src/shared/OutputBudget.ts +79 -0
  121. package/src/shared/Paths.test.ts +51 -0
  122. package/src/shared/Paths.ts +52 -0
  123. package/src/shared/PimSettings.test.ts +90 -0
  124. package/src/shared/PimSettings.ts +124 -0
  125. package/src/shared/Renderer.test.ts +190 -0
  126. package/src/shared/Renderer.ts +256 -0
  127. package/src/shared/SpillCache.test.ts +94 -0
  128. package/src/shared/SpillCache.ts +89 -0
  129. package/src/shared/Tools.test.ts +392 -0
  130. package/src/shared/Tools.ts +636 -0
  131. package/src/telegram/Bot.ts +198 -0
  132. package/src/telegram/Commands.ts +721 -0
  133. package/src/telegram/Config.test.ts +275 -0
  134. package/src/telegram/Config.ts +162 -0
  135. package/src/telegram/Markdown.test.ts +143 -0
  136. package/src/telegram/Markdown.ts +177 -0
  137. package/src/telegram/Message.ts +211 -0
  138. package/src/telegram/Renderer.test.ts +216 -0
  139. package/src/telegram/Renderer.ts +713 -0
  140. package/src/telegram/SendFileSchema.ts +19 -0
  141. package/src/telegram/SendFileTool.ts +94 -0
  142. package/src/telegram/Session.ts +579 -0
  143. package/src/telegram/SessionRegistry.test.ts +89 -0
  144. package/src/telegram/SessionRegistry.ts +170 -0
  145. package/src/telegram/Supervisor.ts +357 -0
  146. package/src/telegram/TaskScheduler.test.ts +278 -0
  147. package/src/telegram/TaskScheduler.ts +293 -0
  148. package/src/telegram/TaskSchema.ts +88 -0
  149. package/src/telegram/TaskStore.ts +73 -0
  150. package/src/telegram/TaskTool.test.ts +179 -0
  151. package/src/telegram/TaskTool.ts +159 -0
  152. package/src/telegram/TypingIndicator.ts +43 -0
  153. package/src/telegram/index.ts +32 -0
  154. package/src/themes/pim-dark.json +84 -0
  155. package/src/themes/pim-light.json +84 -0
@@ -0,0 +1,198 @@
1
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
+ import { Bot as Grammy } from "grammy";
3
+
4
+ import { Commands } from "./Commands";
5
+ import type { TelegramConfig } from "./Config";
6
+ import { Message, type Prompt } from "./Message";
7
+ import { Renderer, type TurnEndState } from "./Renderer";
8
+ import { Session, type SessionId } from "./Session";
9
+ import { SessionRegistry } from "./SessionRegistry";
10
+ import { Supervisor } from "./Supervisor";
11
+ import type { ScheduledTask } from "./TaskSchema";
12
+ import { TaskScheduler } from "./TaskScheduler";
13
+
14
+ export class Bot {
15
+ private readonly grammy: Grammy;
16
+ private readonly allowSet: ReadonlySet<number>;
17
+ private readonly registry: SessionRegistry;
18
+ private readonly scheduler: TaskScheduler;
19
+ private readonly config: TelegramConfig;
20
+ private readonly commands: Commands;
21
+
22
+ public constructor(config: TelegramConfig) {
23
+ this.config = config;
24
+ this.grammy = new Grammy(config.token);
25
+ this.allowSet = new Set(config.allow);
26
+ this.scheduler = new TaskScheduler({
27
+ configDir: config.configDir,
28
+ runTask: (task) => this.runScheduledTask(task),
29
+ });
30
+ this.registry = new SessionRegistry(
31
+ config,
32
+ this.grammy.api,
33
+ this.scheduler
34
+ );
35
+ this.commands = new Commands(config, this.grammy.api, this.registry);
36
+
37
+ this.grammy.on("callback_query:data", async (ctx) => {
38
+ if (!this.allowSet.has(ctx.chat?.id ?? -1)) {
39
+ return;
40
+ }
41
+ await this.commands.handleCallback(ctx);
42
+ });
43
+
44
+ this.grammy.on("message", async (ctx) => {
45
+ const chatId = ctx.chat.id;
46
+ if (!this.allowSet.has(chatId)) {
47
+ console.log(`[recv] reject chatId=${chatId} (not in allow-list)`);
48
+ return;
49
+ }
50
+ const sessionId: SessionId = {
51
+ chatId,
52
+ threadId: ctx.message.message_thread_id,
53
+ };
54
+ let prompt: Prompt | undefined;
55
+ try {
56
+ prompt = await Message.toPrompt(
57
+ ctx,
58
+ this.config.token,
59
+ this.config.configDir,
60
+ sessionId
61
+ );
62
+ } catch (err) {
63
+ const msg = (err as Error).message ?? String(err);
64
+ console.error(`[recv] attachment download failed:`, err);
65
+ await this.grammy.api
66
+ .sendMessage(sessionId.chatId, `⚠️ ${msg}`, {
67
+ message_thread_id: sessionId.threadId,
68
+ link_preview_options: { is_disabled: true },
69
+ })
70
+ .catch((e) => console.error(`[send] plain failed:`, e));
71
+ return;
72
+ }
73
+ if (!prompt) {
74
+ return;
75
+ }
76
+ const preview = prompt.text.slice(0, 120).replace(/\s+/g, " ");
77
+ console.log(
78
+ `[recv] chatId=${chatId} threadId=${sessionId.threadId ?? "main"} ${preview}`
79
+ );
80
+
81
+ const session = this.registry.get(sessionId);
82
+ if (prompt.text.startsWith("/") && !prompt.options.images?.length) {
83
+ await this.commands.handleCommand(ctx, session, prompt.text);
84
+ return;
85
+ }
86
+
87
+ void session.run(
88
+ (agent) => this.handleTurn(session, agent, prompt),
89
+ session.temporary ? { isolated: true } : undefined
90
+ );
91
+ });
92
+
93
+ this.grammy.catch((err) => {
94
+ console.error("[bot] handler error:", err.error);
95
+ });
96
+ }
97
+
98
+ public async start(): Promise<void> {
99
+ await this.registry.init();
100
+ await this.grammy.init();
101
+ await this.processBootUpdateConfirm();
102
+ await this.grammy.api.deleteWebhook({ drop_pending_updates: true });
103
+ const username = this.grammy.botInfo.username;
104
+ this.registry.setBotUsername(username);
105
+ console.log(`bot @${username} ready`);
106
+ await this.scheduler.start();
107
+ await this.grammy.start();
108
+ }
109
+
110
+ public async stop(): Promise<void> {
111
+ await this.scheduler.stop();
112
+ await this.grammy.stop();
113
+ await this.registry.disposeAll();
114
+ }
115
+
116
+ private async runScheduledTask(task: ScheduledTask): Promise<void> {
117
+ const sessionId: SessionId = {
118
+ chatId: task.chatId,
119
+ threadId: task.threadId,
120
+ };
121
+ const prompt: Prompt = { text: task.prompt, options: {} };
122
+ const session = this.registry.get(sessionId);
123
+ await session.run(
124
+ (agent) => this.handleTurn(session, agent, prompt),
125
+ task.isolatedSession ? { isolated: true } : undefined
126
+ );
127
+ }
128
+
129
+ private async processBootUpdateConfirm(): Promise<void> {
130
+ const entries = await Supervisor.readUpdateConfirm(this.config.configDir);
131
+ if (entries.length === 0) {
132
+ return;
133
+ }
134
+ const version = await Supervisor.readVersion();
135
+ const text = `✅ Pim Agent updated to v${version}!`;
136
+ await Promise.all(
137
+ entries.map((e) =>
138
+ this.grammy.api
139
+ .editMessageText(e.chatId, e.messageId, text, {
140
+ link_preview_options: { is_disabled: true },
141
+ })
142
+ .catch((err) => console.warn(`[update-confirm] edit failed:`, err))
143
+ )
144
+ );
145
+ await Supervisor.clearUpdateConfirm(this.config.configDir);
146
+ }
147
+
148
+ private async handleTurn(
149
+ session: Session,
150
+ agent: AgentSession,
151
+ prompt: Prompt
152
+ ): Promise<void> {
153
+ const renderer = new Renderer(session, this.grammy.api);
154
+ const unsubscribe = agent.subscribe((event) => renderer.handleEvent(event));
155
+ await renderer.start();
156
+ try {
157
+ await agent.prompt(prompt.text, {
158
+ ...prompt.options,
159
+ source: "rpc",
160
+ });
161
+ const final = Bot.extractFinalResult(agent);
162
+ await renderer.finish(final.text, final.state);
163
+ } catch (err) {
164
+ const msg = (err as Error).message ?? String(err);
165
+ console.error(`[bot] turn failed:`, err);
166
+ await renderer.finish(`⚠️ ${msg}`, "error");
167
+ } finally {
168
+ unsubscribe();
169
+ }
170
+ }
171
+
172
+ private static extractFinalResult(agent: AgentSession): {
173
+ readonly text: string;
174
+ readonly state: TurnEndState;
175
+ } {
176
+ const messages = agent.messages;
177
+ for (let i = messages.length - 1; i >= 0; i--) {
178
+ const msg = messages[i]!;
179
+ if (msg.role !== "assistant") {
180
+ continue;
181
+ }
182
+ if (msg.stopReason === "error") {
183
+ return { text: msg.errorMessage ?? "", state: "error" };
184
+ }
185
+ if (msg.stopReason === "aborted") {
186
+ return { text: msg.errorMessage ?? "", state: "cancelled" };
187
+ }
188
+ const parts: string[] = [];
189
+ for (const block of msg.content) {
190
+ if (block.type === "text") {
191
+ parts.push(block.text);
192
+ }
193
+ }
194
+ return { text: parts.join("").trim(), state: "ok" };
195
+ }
196
+ return { text: "", state: "ok" };
197
+ }
198
+ }