@llblab/pi-telegram 0.5.2 → 0.6.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.
@@ -3,19 +3,31 @@
3
3
  * Owns MIME/type matching, command-template execution, fallback handling, and prompt injection before prompt enqueueing
4
4
  */
5
5
 
6
- import { homedir } from "node:os";
7
- import { basename, isAbsolute, resolve } from "node:path";
6
+ import { basename } from "node:path";
7
+
8
+ import {
9
+ buildCommandTemplateInvocation,
10
+ expandCommandTemplateConfigs,
11
+ normalizeCommandTemplateConfig,
12
+ type CommandTemplateConfig,
13
+ type CommandTemplateObjectConfig,
14
+ } from "./command-templates.ts";
8
15
 
9
16
  const DEFAULT_ATTACHMENT_HANDLER_TIMEOUT_MS = 120_000;
10
17
 
18
+ type TelegramAttachmentCommandTemplateConfig =
19
+ | string
20
+ | CommandTemplateObjectConfig;
21
+
11
22
  export interface TelegramAttachmentHandlerConfig {
12
23
  match?: string | string[];
13
24
  mime?: string | string[];
14
25
  type?: string | string[];
15
- template?: string;
16
- args?: string | string[];
26
+ template?: string | TelegramAttachmentCommandTemplateConfig[];
27
+ pipe?: TelegramAttachmentCommandTemplateConfig[];
28
+ args?: string[];
17
29
  defaults?: Record<string, unknown>;
18
- timeoutMs?: number;
30
+ timeout?: number;
19
31
  }
20
32
 
21
33
  export interface TelegramAttachmentHandlerFile {
@@ -45,6 +57,7 @@ export interface TelegramAttachmentHandlerExecOptions {
45
57
  cwd?: string;
46
58
  timeout?: number;
47
59
  signal?: AbortSignal;
60
+ stdin?: string;
48
61
  }
49
62
 
50
63
  export interface TelegramAttachmentHandlerExecResult {
@@ -163,151 +176,96 @@ function hasAttachmentFilePlaceholder(value: string): boolean {
163
176
  return /\{file\}/.test(value);
164
177
  }
165
178
 
166
- function normalizeTelegramAttachmentHandlerArgs(
167
- value: string | string[] | undefined,
168
- ): string[] {
169
- if (Array.isArray(value)) return value.map(String).map((item) => item.trim());
170
- if (typeof value !== "string") return [];
171
- return value.split(",").map((item) => item.trim());
172
- }
173
-
174
- function getTelegramAttachmentHandlerArgDefaults(
175
- handler: TelegramAttachmentHandlerConfig,
176
- ): Record<string, string> {
177
- const defaults: Record<string, string> = {};
178
- for (const item of normalizeTelegramAttachmentHandlerArgs(handler.args)) {
179
- if (!item) continue;
180
- const [name, ...defaultParts] = item.split("=");
181
- if (!name || defaultParts.length === 0) continue;
182
- defaults[name.trim()] = defaultParts.join("=").trim();
183
- }
184
- for (const [key, value] of Object.entries(handler.defaults ?? {})) {
185
- defaults[key] = value === undefined || value === null ? "" : String(value);
186
- }
187
- return defaults;
188
- }
189
-
190
179
  function getTelegramAttachmentHandlerTemplateValues(
191
- handler: TelegramAttachmentHandlerConfig,
192
180
  file: TelegramAttachmentHandlerFile,
193
181
  ): Record<string, string> {
194
182
  return {
195
- ...getTelegramAttachmentHandlerArgDefaults(handler),
196
183
  file: file.path,
197
184
  mime: file.mimeType ?? "",
198
185
  type: file.kind ?? "",
199
186
  };
200
187
  }
201
188
 
202
- function substituteTelegramAttachmentHandlerTemplateToken(
203
- token: string,
204
- values: Record<string, string>,
205
- ): string {
206
- return token.replace(/\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (_match, name) => {
207
- if (Object.hasOwn(values, name)) return values[name] ?? "";
208
- throw new Error(`Missing attachment handler template value: ${name}`);
209
- });
210
- }
211
-
212
- export function splitTelegramAttachmentHandlerTemplate(input: string): string[] {
213
- const words: string[] = [];
214
- let current = "";
215
- let quote: "'" | '"' | undefined;
216
- let escaped = false;
217
- let active = false;
218
- for (const char of input) {
219
- if (escaped) {
220
- current += char;
221
- escaped = false;
222
- active = true;
223
- continue;
224
- }
225
- if (char === "\\" && quote !== "'") {
226
- escaped = true;
227
- active = true;
228
- continue;
229
- }
230
- if (quote) {
231
- if (char === quote) quote = undefined;
232
- else current += char;
233
- active = true;
234
- continue;
235
- }
236
- if (char === "'" || char === '"') {
237
- quote = char;
238
- active = true;
239
- continue;
240
- }
241
- if (/\s/.test(char)) {
242
- if (active) words.push(current);
243
- if (active) current = "";
244
- active = false;
245
- continue;
246
- }
247
- current += char;
248
- active = true;
249
- }
250
- if (escaped) current += "\\";
251
- if (active || current) words.push(current);
252
- return words;
253
- }
254
-
255
- function expandExecutablePath(command: string, cwd: string): string {
256
- if (command === "~") return homedir();
257
- if (command.startsWith("~/")) return resolve(homedir(), command.slice(2));
258
- if (command.includes("/") && !isAbsolute(command)) {
259
- return resolve(cwd, command);
260
- }
261
- return command;
262
- }
263
-
264
189
  function buildTelegramAttachmentTemplateInvocation(
265
- template: string,
266
- handler: TelegramAttachmentHandlerConfig,
190
+ handler: CommandTemplateConfig,
267
191
  file: TelegramAttachmentHandlerFile,
268
192
  cwd: string,
193
+ appendFileIfMissing = true,
269
194
  ): AttachmentHandlerInvocation {
270
- const parts = splitTelegramAttachmentHandlerTemplate(template);
271
- const commandPart = parts[0];
272
- if (!commandPart) throw new Error("Attachment handler template is empty");
273
- const values = getTelegramAttachmentHandlerTemplateValues(handler, file);
274
- const hadFilePlaceholder = parts.some(hasAttachmentFilePlaceholder);
275
- const command = expandExecutablePath(
276
- substituteTelegramAttachmentHandlerTemplateToken(commandPart, values),
277
- cwd,
278
- );
279
- const args = parts
280
- .slice(1)
281
- .map((part) =>
282
- substituteTelegramAttachmentHandlerTemplateToken(part, values),
283
- );
284
- if (!hadFilePlaceholder) args.push(file.path);
285
- return { command, args };
195
+ const values = getTelegramAttachmentHandlerTemplateValues(file);
196
+ const templateConfig = normalizeCommandTemplateConfig(handler);
197
+ const hadFilePlaceholder =
198
+ typeof templateConfig.template === "string"
199
+ ? hasAttachmentFilePlaceholder(templateConfig.template)
200
+ : false;
201
+ const invocation = buildCommandTemplateInvocation(handler, values, cwd, {
202
+ emptyMessage: "Attachment handler template is empty",
203
+ missingLabel: "attachment handler template",
204
+ });
205
+ if (appendFileIfMissing && !hadFilePlaceholder)
206
+ invocation.args.push(file.path);
207
+ return invocation;
286
208
  }
287
209
 
288
210
  export function buildTelegramAttachmentHandlerInvocation(
289
- handler: TelegramAttachmentHandlerConfig,
211
+ handler: CommandTemplateConfig,
290
212
  file: TelegramAttachmentHandlerFile,
291
213
  cwd: string,
214
+ appendFileIfMissing = true,
292
215
  ): AttachmentHandlerInvocation {
293
- const { template } = handler;
216
+ const { template } = normalizeCommandTemplateConfig(handler);
294
217
  if (!template) throw new Error("Attachment handler template is required");
295
- return buildTelegramAttachmentTemplateInvocation(template, handler, file, cwd);
218
+ return buildTelegramAttachmentTemplateInvocation(
219
+ handler,
220
+ file,
221
+ cwd,
222
+ appendFileIfMissing,
223
+ );
224
+ }
225
+
226
+ function getTelegramAttachmentHandlerConfiguredTimeout(
227
+ handler: TelegramAttachmentCommandTemplateConfig,
228
+ ): number | undefined {
229
+ const timeout = typeof handler === "string" ? undefined : handler.timeout;
230
+ return typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
231
+ ? timeout
232
+ : undefined;
296
233
  }
297
234
 
298
235
  function getTelegramAttachmentHandlerTimeout(
236
+ handler: TelegramAttachmentCommandTemplateConfig,
237
+ ): number {
238
+ return (
239
+ getTelegramAttachmentHandlerConfiguredTimeout(handler) ??
240
+ DEFAULT_ATTACHMENT_HANDLER_TIMEOUT_MS
241
+ );
242
+ }
243
+
244
+ function getRemainingTelegramAttachmentTimeout(
245
+ timeout: number,
246
+ startedAt: number,
247
+ ): number {
248
+ return Math.max(1, timeout - (Date.now() - startedAt));
249
+ }
250
+
251
+ function getTelegramAttachmentCompositionStepTimeout(
299
252
  handler: TelegramAttachmentHandlerConfig,
253
+ step: TelegramAttachmentCommandTemplateConfig,
254
+ startedAt: number,
300
255
  ): number {
301
- return typeof handler.timeoutMs === "number" &&
302
- Number.isFinite(handler.timeoutMs) &&
303
- handler.timeoutMs > 0
304
- ? handler.timeoutMs
305
- : DEFAULT_ATTACHMENT_HANDLER_TIMEOUT_MS;
256
+ const remaining = getRemainingTelegramAttachmentTimeout(
257
+ getTelegramAttachmentHandlerTimeout(handler),
258
+ startedAt,
259
+ );
260
+ const stepTimeout = getTelegramAttachmentHandlerConfiguredTimeout(step);
261
+ return stepTimeout === undefined ? remaining : Math.min(stepTimeout, remaining);
306
262
  }
307
263
 
308
264
  function getTelegramAttachmentHandlerKind(
309
265
  handler: TelegramAttachmentHandlerConfig,
310
266
  ): string {
267
+ if (Array.isArray(handler.template) || handler.pipe?.length)
268
+ return "composition";
311
269
  if (handler.template) return "template";
312
270
  return "unknown";
313
271
  }
@@ -323,24 +281,78 @@ function formatTelegramAttachmentHandlerFailure(
323
281
  return parts.join("\n\n");
324
282
  }
325
283
 
326
- async function executeTelegramAttachmentHandler(
327
- handler: TelegramAttachmentHandlerConfig,
284
+ async function executeTelegramAttachmentHandlerInvocation(
285
+ handler: TelegramAttachmentCommandTemplateConfig,
328
286
  file: TelegramAttachmentHandlerFile,
329
287
  cwd: string,
330
288
  deps: Pick<TelegramAttachmentHandlerRuntimeDeps<unknown>, "execCommand">,
289
+ appendFileIfMissing = true,
290
+ timeout = getTelegramAttachmentHandlerTimeout(handler),
291
+ stdin?: string,
331
292
  ): Promise<string> {
332
293
  const invocation = buildTelegramAttachmentHandlerInvocation(
333
294
  handler,
334
295
  file,
335
296
  cwd,
297
+ appendFileIfMissing,
336
298
  );
337
299
  const result = await deps.execCommand(invocation.command, invocation.args, {
338
300
  cwd,
339
- timeout: getTelegramAttachmentHandlerTimeout(handler),
301
+ timeout,
302
+ ...(stdin !== undefined ? { stdin } : {}),
340
303
  });
341
304
  if (result.code !== 0)
342
305
  throw new Error(formatTelegramAttachmentHandlerFailure(result));
343
- return result.stdout.trim();
306
+ return result.stdout;
307
+ }
308
+
309
+ function getTelegramAttachmentHandlerCompositionSteps(
310
+ handler: TelegramAttachmentHandlerConfig,
311
+ ): TelegramAttachmentCommandTemplateConfig[] {
312
+ if (Array.isArray(handler.template)) {
313
+ return expandCommandTemplateConfigs(
314
+ handler,
315
+ ) as TelegramAttachmentCommandTemplateConfig[];
316
+ }
317
+ if (handler.pipe?.length) {
318
+ return expandCommandTemplateConfigs({
319
+ ...handler,
320
+ template: handler.pipe,
321
+ }) as TelegramAttachmentCommandTemplateConfig[];
322
+ }
323
+ return [];
324
+ }
325
+
326
+ async function executeTelegramAttachmentHandler(
327
+ handler: TelegramAttachmentHandlerConfig,
328
+ file: TelegramAttachmentHandlerFile,
329
+ cwd: string,
330
+ deps: Pick<TelegramAttachmentHandlerRuntimeDeps<unknown>, "execCommand">,
331
+ ): Promise<string> {
332
+ const steps = getTelegramAttachmentHandlerCompositionSteps(handler);
333
+ if (steps.length === 0) {
334
+ const output = await executeTelegramAttachmentHandlerInvocation(
335
+ handler,
336
+ file,
337
+ cwd,
338
+ deps,
339
+ );
340
+ return output.trim();
341
+ }
342
+ const startedAt = Date.now();
343
+ let output = "";
344
+ for (const [index, step] of steps.entries()) {
345
+ output = await executeTelegramAttachmentHandlerInvocation(
346
+ step,
347
+ file,
348
+ cwd,
349
+ deps,
350
+ false,
351
+ getTelegramAttachmentCompositionStepTimeout(handler, step, startedAt),
352
+ index === 0 ? undefined : output,
353
+ );
354
+ }
355
+ return output.trim();
344
356
  }
345
357
 
346
358
  export async function processTelegramAttachmentHandlers<
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Command-template standard helpers
3
+ * Owns shell-free command-template splitting, placeholder defaults, composition expansion, executable path expansion, and direct execution
4
+ */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import { homedir } from "node:os";
8
+ import { isAbsolute, resolve } from "node:path";
9
+
10
+ export interface CommandTemplateObjectConfig {
11
+ template?: CommandTemplateValue;
12
+ args?: string[];
13
+ defaults?: Record<string, unknown>;
14
+ timeout?: number;
15
+ output?: string;
16
+ }
17
+
18
+ export type CommandTemplateValue = string | CommandTemplateConfig[];
19
+
20
+ export type CommandTemplateConfig = string | CommandTemplateObjectConfig;
21
+
22
+ export interface CommandTemplateLeafConfig extends CommandTemplateObjectConfig {
23
+ template: string;
24
+ }
25
+
26
+ export interface CommandTemplateInvocation {
27
+ command: string;
28
+ args: string[];
29
+ }
30
+
31
+ export interface CommandTemplateExecOptions {
32
+ cwd?: string;
33
+ timeout?: number;
34
+ signal?: AbortSignal;
35
+ stdin?: string;
36
+ }
37
+
38
+ export interface CommandTemplateExecResult {
39
+ stdout: string;
40
+ stderr: string;
41
+ code: number;
42
+ killed: boolean;
43
+ }
44
+
45
+ export type CommandTemplateExecCommand = (
46
+ command: string,
47
+ args: string[],
48
+ options?: CommandTemplateExecOptions,
49
+ ) => Promise<CommandTemplateExecResult>;
50
+
51
+ function normalizeCommandTemplateArgs(value: string[] | undefined): string[] {
52
+ if (!Array.isArray(value)) return [];
53
+ return value.map(String).map((item) => item.trim());
54
+ }
55
+
56
+ export function normalizeCommandTemplateConfig(
57
+ config: CommandTemplateConfig,
58
+ ): CommandTemplateObjectConfig {
59
+ return typeof config === "string" ? { template: config } : config;
60
+ }
61
+
62
+ function normalizeCommandTemplateDefaults(
63
+ defaults: Record<string, unknown> | undefined,
64
+ ): Record<string, unknown> | undefined {
65
+ if (!defaults) return undefined;
66
+ const normalized: Record<string, unknown> = {};
67
+ for (const [key, value] of Object.entries(defaults)) {
68
+ normalized[key] =
69
+ value === undefined || value === null ? "" : String(value);
70
+ }
71
+ return normalized;
72
+ }
73
+
74
+ export function expandCommandTemplateConfigs(
75
+ config: CommandTemplateConfig,
76
+ inherited: Pick<CommandTemplateObjectConfig, "args" | "defaults"> = {},
77
+ ): CommandTemplateLeafConfig[] {
78
+ const normalizedConfig = normalizeCommandTemplateConfig(config);
79
+ const inheritedDefaults = normalizeCommandTemplateDefaults(
80
+ inherited.defaults,
81
+ );
82
+ const ownDefaults = normalizeCommandTemplateDefaults(
83
+ normalizedConfig.defaults,
84
+ );
85
+ const context = {
86
+ ...(inherited.args !== undefined ? { args: inherited.args } : {}),
87
+ ...(inheritedDefaults ? { defaults: inheritedDefaults } : {}),
88
+ ...(normalizedConfig.args !== undefined
89
+ ? { args: normalizedConfig.args }
90
+ : {}),
91
+ ...(ownDefaults
92
+ ? { defaults: { ...(inheritedDefaults ?? {}), ...ownDefaults } }
93
+ : {}),
94
+ };
95
+ if (Array.isArray(normalizedConfig.template)) {
96
+ return normalizedConfig.template.flatMap((step) =>
97
+ expandCommandTemplateConfigs(step, context),
98
+ );
99
+ }
100
+ if (typeof normalizedConfig.template !== "string") return [];
101
+ return [
102
+ { ...normalizedConfig, ...context, template: normalizedConfig.template },
103
+ ];
104
+ }
105
+
106
+ export function getCommandTemplateDefaults(
107
+ config: CommandTemplateConfig | undefined,
108
+ ): Record<string, string> {
109
+ const normalizedConfig = config
110
+ ? normalizeCommandTemplateConfig(config)
111
+ : undefined;
112
+ const defaults: Record<string, string> = {};
113
+ for (const item of normalizeCommandTemplateArgs(normalizedConfig?.args)) {
114
+ if (!item) continue;
115
+ const [name, ...defaultParts] = item.split("=");
116
+ if (!name || defaultParts.length === 0) continue;
117
+ defaults[name.trim()] = defaultParts.join("=").trim();
118
+ }
119
+ for (const [key, value] of Object.entries(normalizedConfig?.defaults ?? {})) {
120
+ defaults[key] = value === undefined || value === null ? "" : String(value);
121
+ }
122
+ return defaults;
123
+ }
124
+
125
+ export function splitCommandTemplate(input: string): string[] {
126
+ const words: string[] = [];
127
+ let current = "";
128
+ let quote: "'" | '"' | undefined;
129
+ let escaped = false;
130
+ let active = false;
131
+ for (const char of input) {
132
+ if (escaped) {
133
+ current += char;
134
+ escaped = false;
135
+ active = true;
136
+ continue;
137
+ }
138
+ if (char === "\\" && quote !== "'") {
139
+ escaped = true;
140
+ active = true;
141
+ continue;
142
+ }
143
+ if (quote) {
144
+ if (char === quote) quote = undefined;
145
+ else current += char;
146
+ active = true;
147
+ continue;
148
+ }
149
+ if (char === "'" || char === '"') {
150
+ quote = char;
151
+ active = true;
152
+ continue;
153
+ }
154
+ if (/\s/.test(char)) {
155
+ if (active) words.push(current);
156
+ if (active) current = "";
157
+ active = false;
158
+ continue;
159
+ }
160
+ current += char;
161
+ active = true;
162
+ }
163
+ if (escaped) current += "\\";
164
+ if (active || current) words.push(current);
165
+ return words;
166
+ }
167
+
168
+ export function expandCommandTemplateExecutable(
169
+ command: string,
170
+ cwd: string,
171
+ ): string {
172
+ if (command === "~") return homedir();
173
+ if (command.startsWith("~/")) return resolve(homedir(), command.slice(2));
174
+ if (command.includes("/") && !isAbsolute(command))
175
+ return resolve(cwd, command);
176
+ return command;
177
+ }
178
+
179
+ export function substituteCommandTemplateToken(
180
+ token: string,
181
+ values: Record<string, string>,
182
+ missingLabel = "command template",
183
+ ): string {
184
+ return token.replace(
185
+ /\{([A-Za-z_][A-Za-z0-9_-]*)(?:=([^}]*))?\}/g,
186
+ (_match, name, inlineDefault: string | undefined) => {
187
+ if (Object.hasOwn(values, name)) return values[name] ?? "";
188
+ if (inlineDefault !== undefined) return inlineDefault;
189
+ throw new Error(`Missing ${missingLabel} value: ${name}`);
190
+ },
191
+ );
192
+ }
193
+
194
+ export function execCommandTemplate(
195
+ command: string,
196
+ args: string[],
197
+ options: CommandTemplateExecOptions = {},
198
+ ): Promise<CommandTemplateExecResult> {
199
+ return new Promise((resolve) => {
200
+ const proc = spawn(command, args, {
201
+ cwd: options.cwd,
202
+ shell: false,
203
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
204
+ });
205
+ let stdout = "";
206
+ let stderr = "";
207
+ let killed = false;
208
+ let settled = false;
209
+ let timeoutId: NodeJS.Timeout | undefined;
210
+ const killProcess = (): void => {
211
+ if (killed) return;
212
+ killed = true;
213
+ proc.kill("SIGTERM");
214
+ setTimeout(() => {
215
+ if (!proc.killed) proc.kill("SIGKILL");
216
+ }, 5000);
217
+ };
218
+ const settle = (code: number): void => {
219
+ if (settled) return;
220
+ settled = true;
221
+ if (timeoutId) clearTimeout(timeoutId);
222
+ if (options.signal)
223
+ options.signal.removeEventListener("abort", killProcess);
224
+ resolve({ stdout, stderr, code, killed });
225
+ };
226
+ if (options.signal) {
227
+ if (options.signal.aborted) killProcess();
228
+ else
229
+ options.signal.addEventListener("abort", killProcess, { once: true });
230
+ }
231
+ if (options.timeout && options.timeout > 0)
232
+ timeoutId = setTimeout(killProcess, options.timeout);
233
+ proc.stdout?.on("data", (data) => {
234
+ stdout += data.toString();
235
+ });
236
+ proc.stderr?.on("data", (data) => {
237
+ stderr += data.toString();
238
+ });
239
+ proc.stdin?.on("error", () => {});
240
+ if (options.stdin !== undefined) proc.stdin?.end(options.stdin);
241
+ proc.on("error", (error) => {
242
+ stderr += error instanceof Error ? error.message : String(error);
243
+ settle(1);
244
+ });
245
+ proc.on("close", (code) => {
246
+ settle(code ?? (killed ? 1 : 0));
247
+ });
248
+ });
249
+ }
250
+
251
+ export function buildCommandTemplateInvocation(
252
+ config: CommandTemplateConfig,
253
+ values: Record<string, string>,
254
+ cwd: string,
255
+ options: { emptyMessage?: string; missingLabel?: string } = {},
256
+ ): CommandTemplateInvocation {
257
+ const normalizedConfig = normalizeCommandTemplateConfig(config);
258
+ if (Array.isArray(normalizedConfig.template)) {
259
+ throw new Error(
260
+ options.emptyMessage ??
261
+ "Command template sequence cannot be executed as one command",
262
+ );
263
+ }
264
+ if (!normalizedConfig.template)
265
+ throw new Error(options.emptyMessage ?? "Command template is required");
266
+ const parts = splitCommandTemplate(normalizedConfig.template);
267
+ const commandPart = parts[0];
268
+ if (!commandPart)
269
+ throw new Error(options.emptyMessage ?? "Command template is empty");
270
+ const resolvedValues = {
271
+ ...getCommandTemplateDefaults(normalizedConfig),
272
+ ...values,
273
+ };
274
+ const command = expandCommandTemplateExecutable(
275
+ substituteCommandTemplateToken(
276
+ commandPart,
277
+ resolvedValues,
278
+ options.missingLabel,
279
+ ),
280
+ cwd,
281
+ );
282
+ const args = parts
283
+ .slice(1)
284
+ .map((part) =>
285
+ substituteCommandTemplateToken(
286
+ part,
287
+ resolvedValues,
288
+ options.missingLabel,
289
+ ),
290
+ );
291
+ return { command, args };
292
+ }
package/lib/config.ts CHANGED
@@ -7,7 +7,8 @@ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
7
7
  import { homedir } from "node:os";
8
8
  import { join, resolve } from "node:path";
9
9
 
10
- import type { TelegramAttachmentHandlerConfig } from "./handlers.ts";
10
+ import type { TelegramAttachmentHandlerConfig } from "./attachment-handlers.ts";
11
+ import type { CommandTemplateObjectConfig } from "./command-templates.ts";
11
12
 
12
13
  function getAgentDir(): string {
13
14
  return process.env.PI_CODING_AGENT_DIR
@@ -19,6 +20,17 @@ function getConfigPath(): string {
19
20
  return join(getAgentDir(), "telegram.json");
20
21
  }
21
22
 
23
+ export type TelegramOutboundCommandTemplateConfig =
24
+ | string
25
+ | CommandTemplateObjectConfig;
26
+ export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConfig {
27
+ type?: string;
28
+ match?: string | string[];
29
+ pipe?: TelegramOutboundCommandTemplateConfig[];
30
+ output?: string;
31
+ timeout?: number;
32
+ }
33
+
22
34
  export interface TelegramConfig {
23
35
  botToken?: string;
24
36
  botUsername?: string;
@@ -26,6 +38,7 @@ export interface TelegramConfig {
26
38
  allowedUserId?: number;
27
39
  lastUpdateId?: number;
28
40
  attachmentHandlers?: TelegramAttachmentHandlerConfig[];
41
+ outboundHandlers?: TelegramOutboundHandlerConfig[];
29
42
  }
30
43
 
31
44
  export interface TelegramConfigStore {
@@ -36,6 +49,7 @@ export interface TelegramConfigStore {
36
49
  hasBotToken: () => boolean;
37
50
  getAllowedUserId: () => number | undefined;
38
51
  getAttachmentHandlers: () => TelegramAttachmentHandlerConfig[] | undefined;
52
+ getOutboundHandlers: () => TelegramOutboundHandlerConfig[] | undefined;
39
53
  setAllowedUserId: (userId: number) => void;
40
54
  load: () => Promise<void>;
41
55
  persist: (config?: TelegramConfig) => Promise<void>;
@@ -89,6 +103,7 @@ export function createTelegramConfigStore(
89
103
  hasBotToken: () => !!config.botToken,
90
104
  getAllowedUserId: () => config.allowedUserId,
91
105
  getAttachmentHandlers: () => config.attachmentHandlers,
106
+ getOutboundHandlers: () => config.outboundHandlers,
92
107
  setAllowedUserId: (userId) => {
93
108
  config.allowedUserId = userId;
94
109
  },