@llblab/pi-telegram 0.5.1 → 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.
- package/README.md +57 -14
- package/docs/README.md +1 -0
- package/docs/architecture.md +32 -27
- package/docs/attachment-handlers.md +9 -17
- package/docs/command-templates.md +102 -32
- package/docs/outbound-handlers.md +110 -0
- package/index.ts +17 -3
- package/lib/{handlers.ts → attachment-handlers.ts} +135 -123
- package/lib/command-templates.ts +292 -0
- package/lib/config.ts +16 -1
- package/lib/media.ts +54 -0
- package/lib/outbound-handlers.ts +874 -0
- package/lib/preview.ts +29 -9
- package/lib/prompts.ts +5 -1
- package/lib/queue.ts +44 -2
- package/lib/replies.ts +21 -11
- package/lib/routing.ts +39 -2
- package/lib/turns.ts +32 -12
- package/package.json +1 -1
|
@@ -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 {
|
|
7
|
-
|
|
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
|
-
|
|
26
|
+
template?: string | TelegramAttachmentCommandTemplateConfig[];
|
|
27
|
+
pipe?: TelegramAttachmentCommandTemplateConfig[];
|
|
28
|
+
args?: string[];
|
|
17
29
|
defaults?: Record<string, unknown>;
|
|
18
|
-
|
|
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
|
-
|
|
266
|
-
handler: TelegramAttachmentHandlerConfig,
|
|
190
|
+
handler: CommandTemplateConfig,
|
|
267
191
|
file: TelegramAttachmentHandlerFile,
|
|
268
192
|
cwd: string,
|
|
193
|
+
appendFileIfMissing = true,
|
|
269
194
|
): AttachmentHandlerInvocation {
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
.
|
|
282
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
327
|
-
handler:
|
|
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
|
|
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
|
|
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
|
},
|