@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/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
- Api.TelegramMessage,
85
+ TelegramApi.TelegramMessage,
86
86
  Pi.ExtensionContext
87
87
  >();
88
88
  const textGroupRuntime = TextGroups.createTelegramTextGroupController<
89
- Api.TelegramMessage,
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
- } = Api.createDefaultTelegramBridgeApiRuntime({
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: Api.fetchTelegramBotIdentity,
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,
@@ -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
- mode?: CommandTemplateMode;
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 = string | CommandTemplateConfig[] | CommandTemplateObjectConfig;
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 ? "" : String(value);
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(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/);
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("Command template repeat must be a positive integer or {array.length}.");
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((arg) =>
128
- arg === "/" ||
129
- arg === "~" ||
130
- arg === "./" ||
131
- arg === "../" ||
132
- arg.includes("{") ||
133
- arg.startsWith("~/") ||
134
- arg.startsWith("/"),
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 mode = hasAnyFlag(args, ["-c"]) ? "shell command strings" : "shell scripts";
147
- warnings.push(`${config.label ?? command}: invokes ${command}; ${mode} are trusted executable content and are not sandboxed by command-template argv splitting.`);
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 (["node", "deno", "bun"].includes(command) && hasAnyFlag(args, ["-e", "--eval"])) {
150
- warnings.push(`${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed.`);
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 (["python", "python3", "perl", "ruby"].includes(command) && hasAnyFlag(args, ["-c", "-e"])) {
153
- warnings.push(`${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed.`);
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 (command === "rm" && (args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) || hasRiskyPathArg(args))) {
156
- warnings.push(`${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands.`);
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(`${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands.`);
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(numeric, underscores + 1);
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(config.repeat, context.defaults ?? {});
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 = normalizeCommandTemplateDefaults(
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) => expandCommandTemplateConfigs(step, context));
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
- .flatMap((leaf) => getLeafCommandTemplateWarnings(leaf)),
327
+ expandCommandTemplateConfigs(config).flatMap((leaf) =>
328
+ getLeafCommandTemplateWarnings(leaf),
329
+ ),
269
330
  ),
270
331
  ];
271
332
  }
272
333
 
273
- function parseCommandTemplateArgToken(value: string): { name: string; defaultValue?: string } {
334
+ function parseCommandTemplateArgToken(value: string): {
335
+ name: string;
336
+ defaultValue?: string;
337
+ } {
274
338
  const separatorIndex = value.indexOf("=");
275
- const rawName = separatorIndex === -1 ? value : value.slice(0, separatorIndex);
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 ? {} : { defaultValue: value.slice(separatorIndex + 1).trim() }),
344
+ ...(separatorIndex === -1
345
+ ? {}
346
+ : { defaultValue: value.slice(separatorIndex + 1).trim() }),
280
347
  };
281
348
  }
282
349
 
283
- function parseCommandTemplatePlaceholderContent(content: string): { name: string; inlineDefault?: string } | undefined {
284
- const match = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)(?::(?:string|path|int|number|bool|array|enum\([^)]*\)))?(?:=([^}]*))?$/);
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 { name: match[1], ...(match[2] !== undefined ? { inlineDefault: match[2] } : {}) };
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(")")) throw new Error(`Invalid command template expression: ${expression}`);
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(`Invalid command template expression variable: ${nameMatch[0]}`);
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) throw new Error(`Invalid command template expression: ${expression}`);
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(evaluateCommandTemplateExpression(padded[2], values), padded[1].length + 1);
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) throw new Error(`Command template value recursion exceeded: ${content}`);
438
- const indexed = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)\[([A-Za-z_][A-Za-z0-9_-]*|\d+)\]$/);
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]) ? indexed[2] : values[indexed[2]];
612
+ const indexValue = /^\d+$/.test(indexed[2])
613
+ ? indexed[2]
614
+ : values[indexed[2]];
442
615
  const index = Number(indexValue);
443
- if (!Array.isArray(source) || !Number.isInteger(index) || index < 0 || index >= source.length) {
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 (typeof raw === "string" && /^\{[^{}]+\}$/.test(raw)) {
453
- return substituteCommandTemplateToken(raw, values, missingLabel, depth + 1);
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
- /\{([^{}]+)\}/g,
472
- (_match, content: string) => {
473
- const resolved = resolveCommandTemplateValue(content, values, missingLabel, depth);
474
- if (resolved !== undefined) return resolved;
475
- throw new Error(`Missing ${missingLabel} value: ${content}`);
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 typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
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) || handler.pipe?.length)
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.critical) throw error;
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.critical) throw error;
694
+ if (typeof step === "object" && step.failure === "root") throw error;
686
695
  output = "";
687
696
  }
688
697
  }