@llblab/pi-telegram 0.12.0 → 0.13.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/AGENTS.md +6 -5
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +25 -3
- package/README.md +3 -33
- package/docs/README.md +1 -1
- package/docs/architecture.md +2 -1
- package/docs/command-templates.md +18 -16
- package/docs/inbound.md +2 -2
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +158 -4
- package/docs/sections.md +5 -5
- package/docs/voice.md +13 -8
- package/index.ts +4 -4
- package/lib/bindings.ts +2 -4
- package/lib/command-templates.ts +249 -60
- package/lib/config.ts +1 -2
- package/lib/inbound.ts +26 -17
- package/lib/locks.ts +4 -1
- package/lib/outbound-buttons.ts +226 -0
- package/lib/outbound-markup.ts +357 -0
- package/lib/outbound-voice.ts +263 -0
- package/lib/outbound.ts +87 -852
- package/lib/preview.ts +1 -1
- package/lib/queue.ts +3 -0
- package/lib/replies.ts +4 -1
- package/lib/status.ts +13 -0
- package/lib/{api.ts → telegram-api.ts} +4 -4
- package/lib/text-groups.ts +3 -2
- package/lib/voice.ts +35 -8
- package/package.json +12 -12
package/index.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Keeps the runtime wiring in one place while delegating reusable domain logic to /lib modules
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import * as Api from "./lib/api.ts";
|
|
8
7
|
import * as Bindings from "./lib/bindings.ts";
|
|
9
8
|
import * as CommandTemplates from "./lib/command-templates.ts";
|
|
10
9
|
import * as Commands from "./lib/commands.ts";
|
|
@@ -28,6 +27,7 @@ import * as Routing from "./lib/routing.ts";
|
|
|
28
27
|
import * as Runtime from "./lib/runtime.ts";
|
|
29
28
|
import * as Sections from "./lib/sections.ts";
|
|
30
29
|
import * as Status from "./lib/status.ts";
|
|
30
|
+
import * as TelegramApi from "./lib/telegram-api.ts";
|
|
31
31
|
import * as TextGroups from "./lib/text-groups.ts";
|
|
32
32
|
import * as TimeInjection from "./lib/time-injection.ts";
|
|
33
33
|
import * as Updates from "./lib/updates.ts";
|
|
@@ -82,11 +82,11 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
82
82
|
const hasPendingMessages = Pi.hasExtensionContextPendingMessages;
|
|
83
83
|
const compact = Pi.compactExtensionContext;
|
|
84
84
|
const mediaGroupRuntime = Media.createTelegramMediaGroupController<
|
|
85
|
-
|
|
85
|
+
TelegramApi.TelegramMessage,
|
|
86
86
|
Pi.ExtensionContext
|
|
87
87
|
>();
|
|
88
88
|
const textGroupRuntime = TextGroups.createTelegramTextGroupController<
|
|
89
|
-
|
|
89
|
+
TelegramApi.TelegramMessage,
|
|
90
90
|
Pi.ExtensionContext
|
|
91
91
|
>();
|
|
92
92
|
const telegramQueueStore =
|
|
@@ -153,7 +153,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
153
153
|
answerGuestQuery,
|
|
154
154
|
deleteMessage: deleteTelegramMessage,
|
|
155
155
|
prepareTempDir,
|
|
156
|
-
} =
|
|
156
|
+
} = TelegramApi.createDefaultTelegramBridgeApiRuntime({
|
|
157
157
|
getBotToken: configStore.getBotToken,
|
|
158
158
|
recordRuntimeEvent,
|
|
159
159
|
});
|
package/lib/bindings.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Owns pi-facing tool, command, and lifecycle hook registration for the entrypoint
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import * as Api from "./api.ts";
|
|
8
7
|
import * as CommandTemplates from "./command-templates.ts";
|
|
9
8
|
import * as Commands from "./commands.ts";
|
|
10
9
|
import * as Config from "./config.ts";
|
|
@@ -22,6 +21,7 @@ import * as Replies from "./replies.ts";
|
|
|
22
21
|
import * as Runtime from "./runtime.ts";
|
|
23
22
|
import * as Setup from "./setup.ts";
|
|
24
23
|
import * as Status from "./status.ts";
|
|
24
|
+
import * as TelegramApi from "./telegram-api.ts";
|
|
25
25
|
|
|
26
26
|
type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
|
|
27
27
|
|
|
@@ -59,13 +59,12 @@ export function registerTelegramCommandsAndTools({
|
|
|
59
59
|
getActiveTurn: activeTurnRuntime.get,
|
|
60
60
|
recordRuntimeEvent,
|
|
61
61
|
});
|
|
62
|
-
|
|
63
62
|
Commands.registerTelegramBridgeCommands(pi, {
|
|
64
63
|
promptForConfig: Setup.createTelegramSetupPromptRuntime({
|
|
65
64
|
getConfig: configStore.get,
|
|
66
65
|
setConfig: configStore.set,
|
|
67
66
|
setupGuard: setup,
|
|
68
|
-
getMe:
|
|
67
|
+
getMe: TelegramApi.fetchTelegramBotIdentity,
|
|
69
68
|
persistConfig: configStore.persist,
|
|
70
69
|
startPolling: lockedPollingRuntime.start,
|
|
71
70
|
updateStatus,
|
|
@@ -260,7 +259,6 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
260
259
|
setActiveToolExecutions: lifecycle.setActiveToolExecutions,
|
|
261
260
|
triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
|
|
262
261
|
});
|
|
263
|
-
|
|
264
262
|
Lifecycle.setResetTransportReplyDedup(Replies.resetTransportReplyDedup);
|
|
265
263
|
const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
|
|
266
264
|
agentLifecycleHooks.onAgentStart,
|
package/lib/command-templates.ts
CHANGED
|
@@ -8,26 +8,28 @@ import { spawn } from "node:child_process";
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { isAbsolute, resolve } from "node:path";
|
|
10
10
|
|
|
11
|
-
export type CommandTemplateMode = "sequence" | "parallel";
|
|
12
11
|
export type CommandTemplateFailureScope = "continue" | "branch" | "root";
|
|
13
12
|
|
|
14
13
|
export interface CommandTemplateObjectConfig {
|
|
15
14
|
label?: string;
|
|
16
|
-
|
|
15
|
+
parallel?: boolean;
|
|
16
|
+
when?: boolean | string;
|
|
17
17
|
template?: CommandTemplateValue;
|
|
18
18
|
args?: string[];
|
|
19
19
|
defaults?: Record<string, unknown>;
|
|
20
|
-
timeout?: number;
|
|
21
|
-
delay?: number;
|
|
20
|
+
timeout?: number | string;
|
|
21
|
+
delay?: number | string;
|
|
22
22
|
output?: string;
|
|
23
|
-
retry?: number;
|
|
24
|
-
critical?: boolean;
|
|
23
|
+
retry?: number | string;
|
|
25
24
|
failure?: CommandTemplateFailureScope;
|
|
26
25
|
recover?: CommandTemplateValue;
|
|
27
26
|
repeat?: number | string;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export type CommandTemplateValue =
|
|
29
|
+
export type CommandTemplateValue =
|
|
30
|
+
| string
|
|
31
|
+
| CommandTemplateConfig[]
|
|
32
|
+
| CommandTemplateObjectConfig;
|
|
31
33
|
|
|
32
34
|
export type CommandTemplateConfig = string | CommandTemplateObjectConfig;
|
|
33
35
|
|
|
@@ -88,11 +90,34 @@ function normalizeCommandTemplateDefaults(
|
|
|
88
90
|
for (const [key, value] of Object.entries(defaults)) {
|
|
89
91
|
normalized[key] = Array.isArray(value)
|
|
90
92
|
? value
|
|
91
|
-
: value === undefined || value === null
|
|
93
|
+
: value === undefined || value === null
|
|
94
|
+
? ""
|
|
95
|
+
: String(value);
|
|
92
96
|
}
|
|
93
97
|
return normalized;
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
export function resolveInheritedDefaultReferences(
|
|
101
|
+
ownDefaults: Record<string, unknown> | undefined,
|
|
102
|
+
inheritedDefaults: Record<string, unknown> | undefined,
|
|
103
|
+
runtimeValues: Record<string, unknown> = {},
|
|
104
|
+
): Record<string, unknown> | undefined {
|
|
105
|
+
if (!ownDefaults || !inheritedDefaults) return ownDefaults;
|
|
106
|
+
const resolved = { ...ownDefaults };
|
|
107
|
+
for (const [key, value] of Object.entries(ownDefaults)) {
|
|
108
|
+
if (typeof value !== "string") continue;
|
|
109
|
+
const exact = /^\{([A-Za-z_][A-Za-z0-9_-]*)\}$/.exec(value);
|
|
110
|
+
if (
|
|
111
|
+
!exact ||
|
|
112
|
+
Object.hasOwn(runtimeValues, exact[1]) ||
|
|
113
|
+
!Object.hasOwn(inheritedDefaults, exact[1])
|
|
114
|
+
)
|
|
115
|
+
continue;
|
|
116
|
+
resolved[key] = inheritedDefaults[exact[1]];
|
|
117
|
+
}
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
|
|
96
121
|
export function resolveCommandTemplateRepeat(
|
|
97
122
|
value: number | string | undefined,
|
|
98
123
|
values: Record<string, unknown> = {},
|
|
@@ -105,13 +130,17 @@ export function resolveCommandTemplateRepeat(
|
|
|
105
130
|
}
|
|
106
131
|
const trimmed = value.trim();
|
|
107
132
|
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
108
|
-
const lengthMatch = trimmed.match(
|
|
133
|
+
const lengthMatch = trimmed.match(
|
|
134
|
+
/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/,
|
|
135
|
+
);
|
|
109
136
|
if (lengthMatch) {
|
|
110
137
|
const source = values[lengthMatch[1]];
|
|
111
138
|
if (Array.isArray(source)) return source.length;
|
|
112
139
|
if (source === undefined) return undefined;
|
|
113
140
|
}
|
|
114
|
-
throw new Error(
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Command template repeat must be a positive integer or {array.length}.",
|
|
143
|
+
);
|
|
115
144
|
}
|
|
116
145
|
|
|
117
146
|
function getExecutableName(command: string | undefined): string {
|
|
@@ -124,14 +153,15 @@ function hasAnyFlag(args: string[], flags: string[]): boolean {
|
|
|
124
153
|
}
|
|
125
154
|
|
|
126
155
|
function hasRiskyPathArg(args: string[]): boolean {
|
|
127
|
-
return args.some(
|
|
128
|
-
arg
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
156
|
+
return args.some(
|
|
157
|
+
(arg) =>
|
|
158
|
+
arg === "/" ||
|
|
159
|
+
arg === "~" ||
|
|
160
|
+
arg === "./" ||
|
|
161
|
+
arg === "../" ||
|
|
162
|
+
arg.includes("{") ||
|
|
163
|
+
arg.startsWith("~/") ||
|
|
164
|
+
arg.startsWith("/"),
|
|
135
165
|
);
|
|
136
166
|
}
|
|
137
167
|
|
|
@@ -143,20 +173,42 @@ function getLeafCommandTemplateWarnings(
|
|
|
143
173
|
const args = parts.slice(1);
|
|
144
174
|
const warnings: string[] = [];
|
|
145
175
|
if (["bash", "sh", "zsh", "fish"].includes(command)) {
|
|
146
|
-
const
|
|
147
|
-
|
|
176
|
+
const shellContent = hasAnyFlag(args, ["-c"])
|
|
177
|
+
? "shell command strings"
|
|
178
|
+
: "shell scripts";
|
|
179
|
+
warnings.push(
|
|
180
|
+
`${config.label ?? command}: invokes ${command}; ${shellContent} are trusted executable content and are not sandboxed by command-template argv splitting.`,
|
|
181
|
+
);
|
|
148
182
|
}
|
|
149
|
-
if (
|
|
150
|
-
|
|
183
|
+
if (
|
|
184
|
+
["node", "deno", "bun"].includes(command) &&
|
|
185
|
+
hasAnyFlag(args, ["-e", "--eval"])
|
|
186
|
+
) {
|
|
187
|
+
warnings.push(
|
|
188
|
+
`${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed.`,
|
|
189
|
+
);
|
|
151
190
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
191
|
+
if (
|
|
192
|
+
["python", "python3", "perl", "ruby"].includes(command) &&
|
|
193
|
+
hasAnyFlag(args, ["-c", "-e"])
|
|
194
|
+
) {
|
|
195
|
+
warnings.push(
|
|
196
|
+
`${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed.`,
|
|
197
|
+
);
|
|
154
198
|
}
|
|
155
|
-
if (
|
|
156
|
-
|
|
199
|
+
if (
|
|
200
|
+
command === "rm" &&
|
|
201
|
+
(args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
|
|
202
|
+
hasRiskyPathArg(args))
|
|
203
|
+
) {
|
|
204
|
+
warnings.push(
|
|
205
|
+
`${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands.`,
|
|
206
|
+
);
|
|
157
207
|
}
|
|
158
208
|
if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
|
|
159
|
-
warnings.push(
|
|
209
|
+
warnings.push(
|
|
210
|
+
`${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands.`,
|
|
211
|
+
);
|
|
160
212
|
}
|
|
161
213
|
return warnings;
|
|
162
214
|
}
|
|
@@ -184,7 +236,10 @@ export function getCommandTemplateRepeatDefaults(
|
|
|
184
236
|
for (const name of ["index", "prev", "next", "repeat"]) {
|
|
185
237
|
const numeric = Number(values[name]);
|
|
186
238
|
for (let underscores = 1; underscores <= 6; underscores += 1) {
|
|
187
|
-
values[`${"_".repeat(underscores)}${name}`] = pad(
|
|
239
|
+
values[`${"_".repeat(underscores)}${name}`] = pad(
|
|
240
|
+
numeric,
|
|
241
|
+
underscores + 1,
|
|
242
|
+
);
|
|
188
243
|
}
|
|
189
244
|
}
|
|
190
245
|
return values;
|
|
@@ -194,7 +249,10 @@ function expandRepeatConfig(
|
|
|
194
249
|
config: CommandTemplateObjectConfig,
|
|
195
250
|
context: Pick<CommandTemplateObjectConfig, "args" | "defaults">,
|
|
196
251
|
): CommandTemplateObjectConfig[] | undefined {
|
|
197
|
-
const repeat = resolveCommandTemplateRepeat(
|
|
252
|
+
const repeat = resolveCommandTemplateRepeat(
|
|
253
|
+
config.repeat,
|
|
254
|
+
context.defaults ?? {},
|
|
255
|
+
);
|
|
198
256
|
if (repeat === undefined) return undefined;
|
|
199
257
|
return Array.from({ length: repeat }, (_unused, index0) => {
|
|
200
258
|
const { repeat: _repeat, ...rest } = config;
|
|
@@ -217,8 +275,9 @@ export function expandCommandTemplateConfigs(
|
|
|
217
275
|
const inheritedDefaults = normalizeCommandTemplateDefaults(
|
|
218
276
|
inherited.defaults,
|
|
219
277
|
);
|
|
220
|
-
const ownDefaults =
|
|
221
|
-
normalizedConfig.defaults,
|
|
278
|
+
const ownDefaults = resolveInheritedDefaultReferences(
|
|
279
|
+
normalizeCommandTemplateDefaults(normalizedConfig.defaults),
|
|
280
|
+
inheritedDefaults,
|
|
222
281
|
);
|
|
223
282
|
const context = {
|
|
224
283
|
...(inherited.args !== undefined ? { args: inherited.args } : {}),
|
|
@@ -232,7 +291,9 @@ export function expandCommandTemplateConfigs(
|
|
|
232
291
|
};
|
|
233
292
|
const repeated = expandRepeatConfig(normalizedConfig, context);
|
|
234
293
|
if (repeated) {
|
|
235
|
-
return repeated.flatMap((step) =>
|
|
294
|
+
return repeated.flatMap((step) =>
|
|
295
|
+
expandCommandTemplateConfigs(step, context),
|
|
296
|
+
);
|
|
236
297
|
}
|
|
237
298
|
const recoverConfig = normalizeRecoverConfig(normalizedConfig.recover);
|
|
238
299
|
const recoverSteps = recoverConfig
|
|
@@ -253,7 +314,6 @@ export function expandCommandTemplateConfigs(
|
|
|
253
314
|
...context,
|
|
254
315
|
template: normalizedConfig.template,
|
|
255
316
|
retry: normalizedConfig.retry,
|
|
256
|
-
critical: normalizedConfig.critical,
|
|
257
317
|
},
|
|
258
318
|
...recoverSteps,
|
|
259
319
|
];
|
|
@@ -264,26 +324,40 @@ export function getCommandTemplateWarnings(
|
|
|
264
324
|
): string[] {
|
|
265
325
|
return [
|
|
266
326
|
...new Set(
|
|
267
|
-
expandCommandTemplateConfigs(config)
|
|
268
|
-
|
|
327
|
+
expandCommandTemplateConfigs(config).flatMap((leaf) =>
|
|
328
|
+
getLeafCommandTemplateWarnings(leaf),
|
|
329
|
+
),
|
|
269
330
|
),
|
|
270
331
|
];
|
|
271
332
|
}
|
|
272
333
|
|
|
273
|
-
function parseCommandTemplateArgToken(value: string): {
|
|
334
|
+
function parseCommandTemplateArgToken(value: string): {
|
|
335
|
+
name: string;
|
|
336
|
+
defaultValue?: string;
|
|
337
|
+
} {
|
|
274
338
|
const separatorIndex = value.indexOf("=");
|
|
275
|
-
const rawName =
|
|
339
|
+
const rawName =
|
|
340
|
+
separatorIndex === -1 ? value : value.slice(0, separatorIndex);
|
|
276
341
|
const colonIndex = rawName.indexOf(":");
|
|
277
342
|
return {
|
|
278
343
|
name: (colonIndex === -1 ? rawName : rawName.slice(0, colonIndex)).trim(),
|
|
279
|
-
...(separatorIndex === -1
|
|
344
|
+
...(separatorIndex === -1
|
|
345
|
+
? {}
|
|
346
|
+
: { defaultValue: value.slice(separatorIndex + 1).trim() }),
|
|
280
347
|
};
|
|
281
348
|
}
|
|
282
349
|
|
|
283
|
-
function parseCommandTemplatePlaceholderContent(
|
|
284
|
-
|
|
350
|
+
function parseCommandTemplatePlaceholderContent(
|
|
351
|
+
content: string,
|
|
352
|
+
): { name: string; inlineDefault?: string } | undefined {
|
|
353
|
+
const match = content.match(
|
|
354
|
+
/^([A-Za-z_][A-Za-z0-9_-]*)(?::(?:string|path|int|number|bool|array|enum\([^)]*\)))?(?:=([^}]*))?$/,
|
|
355
|
+
);
|
|
285
356
|
if (!match) return undefined;
|
|
286
|
-
return {
|
|
357
|
+
return {
|
|
358
|
+
name: match[1],
|
|
359
|
+
...(match[2] !== undefined ? { inlineDefault: match[2] } : {}),
|
|
360
|
+
};
|
|
287
361
|
}
|
|
288
362
|
|
|
289
363
|
export function getCommandTemplateDefaults(
|
|
@@ -376,7 +450,8 @@ function evaluateCommandTemplateExpression(
|
|
|
376
450
|
function parsePrimary(): number {
|
|
377
451
|
if (consume("(")) {
|
|
378
452
|
const value = parseExpression();
|
|
379
|
-
if (!consume(")"))
|
|
453
|
+
if (!consume(")"))
|
|
454
|
+
throw new Error(`Invalid command template expression: ${expression}`);
|
|
380
455
|
return value;
|
|
381
456
|
}
|
|
382
457
|
const numberMatch = source.slice(index).match(/^\d+/);
|
|
@@ -389,7 +464,9 @@ function evaluateCommandTemplateExpression(
|
|
|
389
464
|
index += nameMatch[0].length;
|
|
390
465
|
const value = values[nameMatch[0]];
|
|
391
466
|
if (value === undefined || !/^-?\d+$/.test(String(value)))
|
|
392
|
-
throw new Error(
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Invalid command template expression variable: ${nameMatch[0]}`,
|
|
469
|
+
);
|
|
393
470
|
return Number(value);
|
|
394
471
|
}
|
|
395
472
|
throw new Error(`Invalid command template expression: ${expression}`);
|
|
@@ -412,7 +489,8 @@ function evaluateCommandTemplateExpression(
|
|
|
412
489
|
}
|
|
413
490
|
}
|
|
414
491
|
const value = parseExpression();
|
|
415
|
-
if (index !== source.length)
|
|
492
|
+
if (index !== source.length)
|
|
493
|
+
throw new Error(`Invalid command template expression: ${expression}`);
|
|
416
494
|
return value;
|
|
417
495
|
}
|
|
418
496
|
|
|
@@ -422,25 +500,125 @@ function substituteCommandTemplateExpression(
|
|
|
422
500
|
): string | undefined {
|
|
423
501
|
const padded = content.match(/^(_{1,6})\((.+)\)$/);
|
|
424
502
|
if (padded) {
|
|
425
|
-
return pad(
|
|
503
|
+
return pad(
|
|
504
|
+
evaluateCommandTemplateExpression(padded[2], values),
|
|
505
|
+
padded[1].length + 1,
|
|
506
|
+
);
|
|
426
507
|
}
|
|
427
508
|
if (!/[()+\-*\/%]/.test(content)) return undefined;
|
|
428
509
|
return String(evaluateCommandTemplateExpression(content, values));
|
|
429
510
|
}
|
|
430
511
|
|
|
512
|
+
function shouldResolveEmbeddedCommandTemplateToken(
|
|
513
|
+
token: string,
|
|
514
|
+
values: Record<string, unknown>,
|
|
515
|
+
): boolean {
|
|
516
|
+
const matches = [...token.matchAll(/\{([^{}]+)\}/g)];
|
|
517
|
+
if (matches.length === 0) return false;
|
|
518
|
+
return matches.every((match) => {
|
|
519
|
+
const content = match[1];
|
|
520
|
+
if (resolveCommandTemplateNullish(content, values) !== undefined)
|
|
521
|
+
return true;
|
|
522
|
+
if (resolveCommandTemplateTernary(content, values) !== undefined)
|
|
523
|
+
return true;
|
|
524
|
+
const indexed = content.match(
|
|
525
|
+
/^([A-Za-z_][A-Za-z0-9_-]*)\[([A-Za-z_][A-Za-z0-9_-]*|\d+)\]$/,
|
|
526
|
+
);
|
|
527
|
+
if (indexed) return Object.hasOwn(values, indexed[1]);
|
|
528
|
+
const simple = parseCommandTemplatePlaceholderContent(content);
|
|
529
|
+
if (simple)
|
|
530
|
+
return (
|
|
531
|
+
Object.hasOwn(values, simple.name) || simple.inlineDefault !== undefined
|
|
532
|
+
);
|
|
533
|
+
try {
|
|
534
|
+
return substituteCommandTemplateExpression(content, values) !== undefined;
|
|
535
|
+
} catch {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isFalsyCommandTemplateValue(value: unknown): boolean {
|
|
542
|
+
if (value === undefined || value === null || value === false) return true;
|
|
543
|
+
const normalized = String(value).trim().toLowerCase();
|
|
544
|
+
return normalized === "" || normalized === "0" || normalized === "false" || normalized === "no";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function resolveCommandTemplateCondition(
|
|
548
|
+
condition: string,
|
|
549
|
+
values: Record<string, unknown>,
|
|
550
|
+
): unknown {
|
|
551
|
+
const trimmed = condition.trim();
|
|
552
|
+
const negated = trimmed.startsWith("!");
|
|
553
|
+
const name = negated ? trimmed.slice(1).trim() : trimmed;
|
|
554
|
+
const value = /^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)
|
|
555
|
+
? values[name]
|
|
556
|
+
: undefined;
|
|
557
|
+
return negated ? isFalsyCommandTemplateValue(value) : value;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function shouldRunCommandTemplateNode(
|
|
561
|
+
value: boolean | string | undefined,
|
|
562
|
+
values: Record<string, unknown>,
|
|
563
|
+
): boolean {
|
|
564
|
+
if (value === undefined) return true;
|
|
565
|
+
if (typeof value === "boolean") return value;
|
|
566
|
+
const trimmed = value.trim();
|
|
567
|
+
if (!trimmed) return false;
|
|
568
|
+
const exact = /^\{([^{}]+)\}$/.exec(trimmed);
|
|
569
|
+
const resolved = exact
|
|
570
|
+
? resolveCommandTemplateValue(exact[1], values, "command template when")
|
|
571
|
+
: resolveCommandTemplateCondition(trimmed, values);
|
|
572
|
+
return !isFalsyCommandTemplateValue(resolved);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function resolveCommandTemplateNullish(
|
|
576
|
+
content: string,
|
|
577
|
+
values: Record<string, unknown>,
|
|
578
|
+
): string | undefined {
|
|
579
|
+
const coalescing = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)\?\?(.*)$/);
|
|
580
|
+
if (!coalescing) return undefined;
|
|
581
|
+
const value = values[coalescing[1]];
|
|
582
|
+
return isFalsyCommandTemplateValue(value) ? coalescing[2] : String(value);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function resolveCommandTemplateTernary(
|
|
586
|
+
content: string,
|
|
587
|
+
values: Record<string, unknown>,
|
|
588
|
+
): string | undefined {
|
|
589
|
+
const ternary = content.match(/^([^?:]+)\?([^:]*):(.*)$/);
|
|
590
|
+
if (!ternary) return undefined;
|
|
591
|
+
const condition = resolveCommandTemplateCondition(ternary[1], values);
|
|
592
|
+
return isFalsyCommandTemplateValue(condition) ? ternary[3] : ternary[2];
|
|
593
|
+
}
|
|
594
|
+
|
|
431
595
|
function resolveCommandTemplateValue(
|
|
432
596
|
content: string,
|
|
433
597
|
values: Record<string, unknown>,
|
|
434
598
|
missingLabel: string,
|
|
435
599
|
depth = 0,
|
|
436
600
|
): string | undefined {
|
|
437
|
-
if (depth > 5)
|
|
438
|
-
|
|
601
|
+
if (depth > 5)
|
|
602
|
+
throw new Error(`Command template value recursion exceeded: ${content}`);
|
|
603
|
+
const nullish = resolveCommandTemplateNullish(content, values);
|
|
604
|
+
if (nullish !== undefined) return nullish;
|
|
605
|
+
const ternary = resolveCommandTemplateTernary(content, values);
|
|
606
|
+
if (ternary !== undefined) return ternary;
|
|
607
|
+
const indexed = content.match(
|
|
608
|
+
/^([A-Za-z_][A-Za-z0-9_-]*)\[([A-Za-z_][A-Za-z0-9_-]*|\d+)\]$/,
|
|
609
|
+
);
|
|
439
610
|
if (indexed) {
|
|
440
611
|
const source = values[indexed[1]];
|
|
441
|
-
const indexValue = /^\d+$/.test(indexed[2])
|
|
612
|
+
const indexValue = /^\d+$/.test(indexed[2])
|
|
613
|
+
? indexed[2]
|
|
614
|
+
: values[indexed[2]];
|
|
442
615
|
const index = Number(indexValue);
|
|
443
|
-
if (
|
|
616
|
+
if (
|
|
617
|
+
!Array.isArray(source) ||
|
|
618
|
+
!Number.isInteger(index) ||
|
|
619
|
+
index < 0 ||
|
|
620
|
+
index >= source.length
|
|
621
|
+
) {
|
|
444
622
|
throw new Error(`Missing ${missingLabel} value: ${content}`);
|
|
445
623
|
}
|
|
446
624
|
return String(source[index] ?? "");
|
|
@@ -449,8 +627,16 @@ function resolveCommandTemplateValue(
|
|
|
449
627
|
if (simple) {
|
|
450
628
|
if (Object.hasOwn(values, simple.name)) {
|
|
451
629
|
const raw = values[simple.name] ?? "";
|
|
452
|
-
if (
|
|
453
|
-
|
|
630
|
+
if (
|
|
631
|
+
typeof raw === "string" &&
|
|
632
|
+
shouldResolveEmbeddedCommandTemplateToken(raw, values)
|
|
633
|
+
) {
|
|
634
|
+
return substituteCommandTemplateToken(
|
|
635
|
+
raw,
|
|
636
|
+
values,
|
|
637
|
+
missingLabel,
|
|
638
|
+
depth + 1,
|
|
639
|
+
);
|
|
454
640
|
}
|
|
455
641
|
return Array.isArray(raw) ? JSON.stringify(raw) : String(raw);
|
|
456
642
|
}
|
|
@@ -467,14 +653,16 @@ export function substituteCommandTemplateToken(
|
|
|
467
653
|
missingLabel = "command template",
|
|
468
654
|
depth = 0,
|
|
469
655
|
): string {
|
|
470
|
-
return token.replace(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
656
|
+
return token.replace(/\{([^{}]+)\}/g, (_match, content: string) => {
|
|
657
|
+
const resolved = resolveCommandTemplateValue(
|
|
658
|
+
content,
|
|
659
|
+
values,
|
|
660
|
+
missingLabel,
|
|
661
|
+
depth,
|
|
662
|
+
);
|
|
663
|
+
if (resolved !== undefined) return resolved;
|
|
664
|
+
throw new Error(`Missing ${missingLabel} value: ${content}`);
|
|
665
|
+
});
|
|
478
666
|
}
|
|
479
667
|
|
|
480
668
|
export async function execCommandTemplate(
|
|
@@ -601,6 +789,7 @@ export function buildCommandTemplateInvocation(
|
|
|
601
789
|
resolvedValues,
|
|
602
790
|
options.missingLabel,
|
|
603
791
|
),
|
|
604
|
-
)
|
|
792
|
+
)
|
|
793
|
+
.filter((part) => part !== "");
|
|
605
794
|
return { command, args };
|
|
606
795
|
}
|
package/lib/config.ts
CHANGED
|
@@ -30,9 +30,8 @@ export type TelegramOutboundCommandTemplateConfig =
|
|
|
30
30
|
export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConfig {
|
|
31
31
|
type?: string;
|
|
32
32
|
match?: string | string[];
|
|
33
|
-
pipe?: TelegramOutboundCommandTemplateConfig[];
|
|
34
33
|
output?: string;
|
|
35
|
-
timeout?: number;
|
|
34
|
+
timeout?: number | string;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
export type TelegramTimeMode = "hidden" | "always" | "interval";
|
package/lib/inbound.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
buildCommandTemplateInvocation,
|
|
12
12
|
expandCommandTemplateConfigs,
|
|
13
13
|
normalizeCommandTemplateConfig,
|
|
14
|
+
substituteCommandTemplateToken,
|
|
14
15
|
type CommandTemplateConfig,
|
|
15
16
|
type CommandTemplateObjectConfig,
|
|
16
17
|
} from "./command-templates.ts";
|
|
@@ -28,10 +29,9 @@ export interface TelegramInboundHandlerConfig {
|
|
|
28
29
|
mime?: string | string[];
|
|
29
30
|
type?: string | string[];
|
|
30
31
|
template?: string | TelegramInboundCommandTemplateConfig[];
|
|
31
|
-
pipe?: TelegramInboundCommandTemplateConfig[];
|
|
32
32
|
args?: string[];
|
|
33
33
|
defaults?: Record<string, unknown>;
|
|
34
|
-
timeout?: number;
|
|
34
|
+
timeout?: number | string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export interface TelegramInboundHandlerFile {
|
|
@@ -62,6 +62,7 @@ export interface TelegramInboundHandlerExecOptions {
|
|
|
62
62
|
timeout?: number;
|
|
63
63
|
signal?: AbortSignal;
|
|
64
64
|
stdin?: string;
|
|
65
|
+
retry?: number;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export interface TelegramInboundHandlerExecResult {
|
|
@@ -322,13 +323,28 @@ export function buildTelegramInboundHandlerInvocation(
|
|
|
322
323
|
);
|
|
323
324
|
}
|
|
324
325
|
|
|
326
|
+
function resolveTelegramInboundNumericControlField(
|
|
327
|
+
value: number | string | undefined,
|
|
328
|
+
values: Record<string, unknown>,
|
|
329
|
+
label: string,
|
|
330
|
+
): number | undefined {
|
|
331
|
+
if (value === undefined) return undefined;
|
|
332
|
+
const resolved =
|
|
333
|
+
typeof value === "string"
|
|
334
|
+
? substituteCommandTemplateToken(value, values, label)
|
|
335
|
+
: value;
|
|
336
|
+
if (resolved === "") return undefined;
|
|
337
|
+
const numeric = Number(resolved);
|
|
338
|
+
if (!Number.isFinite(numeric) || numeric < 0)
|
|
339
|
+
throw new Error(`Command template ${label} must be a non-negative number.`);
|
|
340
|
+
return numeric;
|
|
341
|
+
}
|
|
342
|
+
|
|
325
343
|
function getTelegramInboundHandlerConfiguredTimeout(
|
|
326
344
|
handler: TelegramInboundCommandTemplateConfig,
|
|
327
345
|
): number | undefined {
|
|
328
346
|
const timeout = typeof handler === "string" ? undefined : handler.timeout;
|
|
329
|
-
return
|
|
330
|
-
? timeout
|
|
331
|
-
: undefined;
|
|
347
|
+
return resolveTelegramInboundNumericControlField(timeout, {}, "timeout");
|
|
332
348
|
}
|
|
333
349
|
|
|
334
350
|
function getTelegramInboundHandlerTimeout(
|
|
@@ -374,8 +390,7 @@ function getTelegramInboundCompositionStepTimeout(
|
|
|
374
390
|
function getTelegramInboundHandlerKind(
|
|
375
391
|
handler: TelegramInboundHandlerConfig,
|
|
376
392
|
): string {
|
|
377
|
-
if (Array.isArray(handler.template)
|
|
378
|
-
return "composition";
|
|
393
|
+
if (Array.isArray(handler.template)) return "composition";
|
|
379
394
|
if (handler.template) return "template";
|
|
380
395
|
return "unknown";
|
|
381
396
|
}
|
|
@@ -410,7 +425,7 @@ async function executeTelegramInboundHandlerInvocation(
|
|
|
410
425
|
cwd,
|
|
411
426
|
timeout,
|
|
412
427
|
...(typeof handler === "object" && handler.retry !== undefined
|
|
413
|
-
? { retry: handler.retry }
|
|
428
|
+
? { retry: resolveTelegramInboundNumericControlField(handler.retry, {}, "retry") }
|
|
414
429
|
: {}),
|
|
415
430
|
...(stdin !== undefined ? { stdin } : {}),
|
|
416
431
|
});
|
|
@@ -427,12 +442,6 @@ function getTelegramInboundHandlerCompositionSteps(
|
|
|
427
442
|
handler,
|
|
428
443
|
) as TelegramInboundCommandTemplateConfig[];
|
|
429
444
|
}
|
|
430
|
-
if (handler.pipe?.length) {
|
|
431
|
-
return expandCommandTemplateConfigs({
|
|
432
|
-
...handler,
|
|
433
|
-
template: handler.pipe,
|
|
434
|
-
}) as TelegramInboundCommandTemplateConfig[];
|
|
435
|
-
}
|
|
436
445
|
return [];
|
|
437
446
|
}
|
|
438
447
|
|
|
@@ -490,7 +499,7 @@ async function executeTelegramTextHandlerInvocation(
|
|
|
490
499
|
timeout,
|
|
491
500
|
stdin: text,
|
|
492
501
|
...(typeof handler === "object" && handler.retry !== undefined
|
|
493
|
-
? { retry: handler.retry }
|
|
502
|
+
? { retry: resolveTelegramInboundNumericControlField(handler.retry, {}, "retry") }
|
|
494
503
|
: {}),
|
|
495
504
|
});
|
|
496
505
|
if (result.code !== 0)
|
|
@@ -524,7 +533,7 @@ async function executeTelegramTextHandler(
|
|
|
524
533
|
: getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
|
|
525
534
|
);
|
|
526
535
|
} catch (error) {
|
|
527
|
-
if (typeof step === "object" && step.
|
|
536
|
+
if (typeof step === "object" && step.failure === "root") throw error;
|
|
528
537
|
output = "";
|
|
529
538
|
}
|
|
530
539
|
if (index > 0 && !output) output = text;
|
|
@@ -682,7 +691,7 @@ async function executeTelegramInboundHandler(
|
|
|
682
691
|
index === 0 ? undefined : output,
|
|
683
692
|
);
|
|
684
693
|
} catch (error) {
|
|
685
|
-
if (typeof step === "object" && step.
|
|
694
|
+
if (typeof step === "object" && step.failure === "root") throw error;
|
|
686
695
|
output = "";
|
|
687
696
|
}
|
|
688
697
|
}
|