@llblab/pi-telegram 0.11.1 → 0.11.2
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 +2 -1
- package/CHANGELOG.md +15 -2
- package/README.md +25 -12
- package/docs/architecture.md +132 -26
- package/docs/command-templates.md +81 -24
- package/index.ts +21 -42
- package/lib/command-templates.ts +163 -32
- package/lib/config.ts +2 -2
- package/lib/lifecycle.ts +86 -0
- package/lib/menu-settings.ts +42 -22
- package/lib/pi.ts +4 -0
- package/lib/routing.ts +1 -10
- package/lib/time-injection.ts +1 -1
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -11,7 +11,6 @@ import * as Config from "./lib/config.ts";
|
|
|
11
11
|
import {
|
|
12
12
|
createTelegramExtensionSectionRegistry,
|
|
13
13
|
setGlobalTelegramSectionRegistry,
|
|
14
|
-
registerTelegramSection,
|
|
15
14
|
type TelegramSectionRegistry,
|
|
16
15
|
} from "./lib/extension-sections.ts";
|
|
17
16
|
import { createTelegramExternalHandleUpdate } from "./lib/external-handlers.ts";
|
|
@@ -46,42 +45,6 @@ const VOICE_EVENT_RECORDER_KEY = "__piTelegramVoiceEventRecorder__";
|
|
|
46
45
|
type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
|
|
47
46
|
type RuntimeTelegramQueueItem = Queue.TelegramQueueItem<Pi.ExtensionContext>;
|
|
48
47
|
|
|
49
|
-
export {
|
|
50
|
-
registerTelegramOutboundHandler,
|
|
51
|
-
hasTelegramOutboundHandler,
|
|
52
|
-
getTelegramOutboundProgrammaticHandlers,
|
|
53
|
-
recordTelegramRuntimeEvent,
|
|
54
|
-
} from "./lib/outbound-handlers.ts";
|
|
55
|
-
|
|
56
|
-
// --- Voice Integration Exports ---
|
|
57
|
-
// Prefer domain imports from ./lib/voice.ts; root exports stay for compatibility.
|
|
58
|
-
export {
|
|
59
|
-
registerTelegramVoiceSynthesisProvider,
|
|
60
|
-
getTelegramVoiceSynthesisProviders,
|
|
61
|
-
hasTelegramVoiceSynthesisProvider,
|
|
62
|
-
clearTelegramVoiceSynthesisProviders,
|
|
63
|
-
planTelegramVoiceReply,
|
|
64
|
-
getTelegramVoiceReplyMode,
|
|
65
|
-
computeVoiceTurnFlags,
|
|
66
|
-
isVoiceTurn,
|
|
67
|
-
shouldSuppressPreviewForVoice,
|
|
68
|
-
computeVoicePromptContribution,
|
|
69
|
-
type TelegramVoiceSynthesisProvider,
|
|
70
|
-
type TelegramVoiceTurnView,
|
|
71
|
-
type TelegramVoiceSynthesisProviderResult,
|
|
72
|
-
type TelegramVoiceReplyMode,
|
|
73
|
-
} from "./lib/voice.ts";
|
|
74
|
-
|
|
75
|
-
// --- Extension Section Exports ---
|
|
76
|
-
export {
|
|
77
|
-
registerTelegramSection,
|
|
78
|
-
type TelegramSectionRegistration,
|
|
79
|
-
type TelegramSectionContext,
|
|
80
|
-
type TelegramSectionCallbackContext,
|
|
81
|
-
type TelegramSectionView,
|
|
82
|
-
type TelegramSectionSettingsRegistration,
|
|
83
|
-
} from "./lib/extension-sections.ts";
|
|
84
|
-
|
|
85
48
|
// --- Extension Runtime ---
|
|
86
49
|
|
|
87
50
|
export default function (pi: Pi.ExtensionAPI) {
|
|
@@ -99,7 +62,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
99
62
|
Config.setGlobalTelegramConfigRuntime({
|
|
100
63
|
updateVoiceConfig(voice) {
|
|
101
64
|
const current = configStore.get();
|
|
102
|
-
const next = {
|
|
65
|
+
const next = {
|
|
66
|
+
...current,
|
|
67
|
+
voice: { ...(current.voice ?? {}), ...voice },
|
|
68
|
+
};
|
|
103
69
|
configStore.set(next);
|
|
104
70
|
void configStore.persist(next);
|
|
105
71
|
},
|
|
@@ -137,7 +103,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
137
103
|
createTelegramExtensionSectionRegistry();
|
|
138
104
|
setGlobalTelegramSectionRegistry(sectionRegistry);
|
|
139
105
|
|
|
140
|
-
|
|
141
106
|
const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
|
|
142
107
|
getBotToken: configStore.getBotToken,
|
|
143
108
|
});
|
|
@@ -146,9 +111,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
146
111
|
getConfig: Config.createTelegramTimeConfigGetter(configStore),
|
|
147
112
|
recordRuntimeEvent,
|
|
148
113
|
});
|
|
149
|
-
(globalThis as Record<string, unknown>)[
|
|
150
|
-
|
|
151
|
-
] = recordRuntimeEvent;
|
|
114
|
+
(globalThis as Record<string, unknown>)[VOICE_EVENT_RECORDER_KEY] =
|
|
115
|
+
recordRuntimeEvent;
|
|
152
116
|
const getContextModel = Pi.getExtensionContextModel;
|
|
153
117
|
const isIdle = Pi.isExtensionContextIdle;
|
|
154
118
|
const hasPendingMessages = Pi.hasExtensionContextPendingMessages;
|
|
@@ -165,6 +129,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
165
129
|
Queue.createTelegramQueueStore<Pi.ExtensionContext>();
|
|
166
130
|
const deferredQueueDispatchRuntime =
|
|
167
131
|
Queue.createTelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>({
|
|
132
|
+
delayMs: 50,
|
|
168
133
|
recordRuntimeEvent,
|
|
169
134
|
});
|
|
170
135
|
const pollingControllerState = Polling.createTelegramPollingControllerState();
|
|
@@ -623,9 +588,23 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
623
588
|
const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
|
|
624
589
|
agentLifecycleHooks.onAgentStart,
|
|
625
590
|
);
|
|
591
|
+
const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
|
|
592
|
+
setCompactionInProgress: lifecycle.setCompactionInProgress,
|
|
593
|
+
updateStatus,
|
|
594
|
+
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
595
|
+
deferredQueueDispatchRuntime.request,
|
|
596
|
+
dispatchNextQueuedTelegramTurn,
|
|
597
|
+
recordRuntimeEvent,
|
|
598
|
+
});
|
|
626
599
|
Lifecycle.registerTelegramLifecycleHooks(pi, {
|
|
627
600
|
...sessionLifecycleRuntime,
|
|
628
601
|
...agentLifecycleHooks,
|
|
602
|
+
async onSessionShutdown(event, ctx) {
|
|
603
|
+
compactionObserver.onSessionShutdown();
|
|
604
|
+
await sessionLifecycleRuntime.onSessionShutdown(event, ctx);
|
|
605
|
+
},
|
|
606
|
+
onSessionBeforeCompact: compactionObserver.onSessionBeforeCompact,
|
|
607
|
+
onSessionCompact: compactionObserver.onSessionCompact,
|
|
629
608
|
onAgentStart: agentStartWithDedupReset,
|
|
630
609
|
onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
|
|
631
610
|
isProactivePushEnabled,
|
package/lib/command-templates.ts
CHANGED
|
@@ -8,9 +8,8 @@ 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 const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
|
|
12
|
-
|
|
13
11
|
export type CommandTemplateMode = "sequence" | "parallel";
|
|
12
|
+
export type CommandTemplateFailureScope = "continue" | "branch" | "root";
|
|
14
13
|
|
|
15
14
|
export interface CommandTemplateObjectConfig {
|
|
16
15
|
label?: string;
|
|
@@ -23,7 +22,9 @@ export interface CommandTemplateObjectConfig {
|
|
|
23
22
|
output?: string;
|
|
24
23
|
retry?: number;
|
|
25
24
|
critical?: boolean;
|
|
26
|
-
|
|
25
|
+
failure?: CommandTemplateFailureScope;
|
|
26
|
+
recover?: CommandTemplateValue;
|
|
27
|
+
repeat?: number | string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export type CommandTemplateValue = string | CommandTemplateConfig[] | CommandTemplateObjectConfig;
|
|
@@ -72,23 +73,92 @@ export function normalizeCommandTemplateConfig(
|
|
|
72
73
|
return typeof config === "string" ? { template: config } : config;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function normalizeRecoverConfig(
|
|
77
|
+
config: CommandTemplateValue | undefined,
|
|
78
|
+
): CommandTemplateConfig | undefined {
|
|
79
|
+
if (config === undefined) return undefined;
|
|
80
|
+
return Array.isArray(config) ? { template: config } : config;
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
function normalizeCommandTemplateDefaults(
|
|
76
84
|
defaults: Record<string, unknown> | undefined,
|
|
77
85
|
): Record<string, unknown> | undefined {
|
|
78
86
|
if (!defaults) return undefined;
|
|
79
87
|
const normalized: Record<string, unknown> = {};
|
|
80
88
|
for (const [key, value] of Object.entries(defaults)) {
|
|
81
|
-
normalized[key] =
|
|
82
|
-
|
|
89
|
+
normalized[key] = Array.isArray(value)
|
|
90
|
+
? value
|
|
91
|
+
: value === undefined || value === null ? "" : String(value);
|
|
83
92
|
}
|
|
84
93
|
return normalized;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
function
|
|
96
|
+
export function resolveCommandTemplateRepeat(
|
|
97
|
+
value: number | string | undefined,
|
|
98
|
+
values: Record<string, unknown> = {},
|
|
99
|
+
): number | undefined {
|
|
88
100
|
if (value === undefined) return undefined;
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
if (typeof value === "number") {
|
|
102
|
+
if (!Number.isInteger(value) || value < 1)
|
|
103
|
+
throw new Error("Command template repeat must be a positive integer.");
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
const trimmed = value.trim();
|
|
107
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
108
|
+
const lengthMatch = trimmed.match(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/);
|
|
109
|
+
if (lengthMatch) {
|
|
110
|
+
const source = values[lengthMatch[1]];
|
|
111
|
+
if (Array.isArray(source)) return source.length;
|
|
112
|
+
if (source === undefined) return undefined;
|
|
113
|
+
}
|
|
114
|
+
throw new Error("Command template repeat must be a positive integer or {array.length}.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getExecutableName(command: string | undefined): string {
|
|
118
|
+
if (!command) return "";
|
|
119
|
+
return command.split(/[\\/]/).pop()?.toLowerCase() ?? "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasAnyFlag(args: string[], flags: string[]): boolean {
|
|
123
|
+
return args.some((arg) => flags.includes(arg));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
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("/"),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getLeafCommandTemplateWarnings(
|
|
139
|
+
config: CommandTemplateLeafConfig,
|
|
140
|
+
): string[] {
|
|
141
|
+
const parts = splitCommandTemplate(config.template);
|
|
142
|
+
const command = getExecutableName(parts[0]);
|
|
143
|
+
const args = parts.slice(1);
|
|
144
|
+
const warnings: string[] = [];
|
|
145
|
+
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.`);
|
|
148
|
+
}
|
|
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.`);
|
|
151
|
+
}
|
|
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.`);
|
|
154
|
+
}
|
|
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.`);
|
|
157
|
+
}
|
|
158
|
+
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.`);
|
|
160
|
+
}
|
|
161
|
+
return warnings;
|
|
92
162
|
}
|
|
93
163
|
|
|
94
164
|
function pad(value: number, width: number): string {
|
|
@@ -124,7 +194,7 @@ function expandRepeatConfig(
|
|
|
124
194
|
config: CommandTemplateObjectConfig,
|
|
125
195
|
context: Pick<CommandTemplateObjectConfig, "args" | "defaults">,
|
|
126
196
|
): CommandTemplateObjectConfig[] | undefined {
|
|
127
|
-
const repeat =
|
|
197
|
+
const repeat = resolveCommandTemplateRepeat(config.repeat, context.defaults ?? {});
|
|
128
198
|
if (repeat === undefined) return undefined;
|
|
129
199
|
return Array.from({ length: repeat }, (_unused, index0) => {
|
|
130
200
|
const { repeat: _repeat, ...rest } = config;
|
|
@@ -164,12 +234,19 @@ export function expandCommandTemplateConfigs(
|
|
|
164
234
|
if (repeated) {
|
|
165
235
|
return repeated.flatMap((step) => expandCommandTemplateConfigs(step, context));
|
|
166
236
|
}
|
|
237
|
+
const recoverConfig = normalizeRecoverConfig(normalizedConfig.recover);
|
|
238
|
+
const recoverSteps = recoverConfig
|
|
239
|
+
? expandCommandTemplateConfigs(recoverConfig, context)
|
|
240
|
+
: [];
|
|
167
241
|
if (Array.isArray(normalizedConfig.template)) {
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
|
|
242
|
+
return [
|
|
243
|
+
...normalizedConfig.template.flatMap((step) =>
|
|
244
|
+
expandCommandTemplateConfigs(step, context),
|
|
245
|
+
),
|
|
246
|
+
...recoverSteps,
|
|
247
|
+
];
|
|
171
248
|
}
|
|
172
|
-
if (typeof normalizedConfig.template !== "string") return
|
|
249
|
+
if (typeof normalizedConfig.template !== "string") return recoverSteps;
|
|
173
250
|
return [
|
|
174
251
|
{
|
|
175
252
|
...normalizedConfig,
|
|
@@ -178,9 +255,37 @@ export function expandCommandTemplateConfigs(
|
|
|
178
255
|
retry: normalizedConfig.retry,
|
|
179
256
|
critical: normalizedConfig.critical,
|
|
180
257
|
},
|
|
258
|
+
...recoverSteps,
|
|
181
259
|
];
|
|
182
260
|
}
|
|
183
261
|
|
|
262
|
+
export function getCommandTemplateWarnings(
|
|
263
|
+
config: CommandTemplateConfig,
|
|
264
|
+
): string[] {
|
|
265
|
+
return [
|
|
266
|
+
...new Set(
|
|
267
|
+
expandCommandTemplateConfigs(config)
|
|
268
|
+
.flatMap((leaf) => getLeafCommandTemplateWarnings(leaf)),
|
|
269
|
+
),
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseCommandTemplateArgToken(value: string): { name: string; defaultValue?: string } {
|
|
274
|
+
const separatorIndex = value.indexOf("=");
|
|
275
|
+
const rawName = separatorIndex === -1 ? value : value.slice(0, separatorIndex);
|
|
276
|
+
const colonIndex = rawName.indexOf(":");
|
|
277
|
+
return {
|
|
278
|
+
name: (colonIndex === -1 ? rawName : rawName.slice(0, colonIndex)).trim(),
|
|
279
|
+
...(separatorIndex === -1 ? {} : { defaultValue: value.slice(separatorIndex + 1).trim() }),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
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\([^)]*\)))?(?:=([^}]*))?$/);
|
|
285
|
+
if (!match) return undefined;
|
|
286
|
+
return { name: match[1], ...(match[2] !== undefined ? { inlineDefault: match[2] } : {}) };
|
|
287
|
+
}
|
|
288
|
+
|
|
184
289
|
export function getCommandTemplateDefaults(
|
|
185
290
|
config: CommandTemplateConfig | undefined,
|
|
186
291
|
): Record<string, string> {
|
|
@@ -190,9 +295,9 @@ export function getCommandTemplateDefaults(
|
|
|
190
295
|
const defaults: Record<string, string> = {};
|
|
191
296
|
for (const item of normalizeCommandTemplateArgs(normalizedConfig?.args)) {
|
|
192
297
|
if (!item) continue;
|
|
193
|
-
const
|
|
194
|
-
if (!name ||
|
|
195
|
-
defaults[name
|
|
298
|
+
const parsed = parseCommandTemplateArgToken(item);
|
|
299
|
+
if (!parsed.name || parsed.defaultValue === undefined) continue;
|
|
300
|
+
defaults[parsed.name] = parsed.defaultValue;
|
|
196
301
|
}
|
|
197
302
|
for (const [key, value] of Object.entries(normalizedConfig?.defaults ?? {})) {
|
|
198
303
|
defaults[key] = value === undefined || value === null ? "" : String(value);
|
|
@@ -256,7 +361,7 @@ export function expandCommandTemplateExecutable(
|
|
|
256
361
|
|
|
257
362
|
function evaluateCommandTemplateExpression(
|
|
258
363
|
expression: string,
|
|
259
|
-
values: Record<string,
|
|
364
|
+
values: Record<string, unknown>,
|
|
260
365
|
): number {
|
|
261
366
|
let index = 0;
|
|
262
367
|
const source = expression.replace(/\s+/g, "");
|
|
@@ -283,7 +388,7 @@ function evaluateCommandTemplateExpression(
|
|
|
283
388
|
if (nameMatch) {
|
|
284
389
|
index += nameMatch[0].length;
|
|
285
390
|
const value = values[nameMatch[0]];
|
|
286
|
-
if (value === undefined || !/^-?\d+$/.test(value))
|
|
391
|
+
if (value === undefined || !/^-?\d+$/.test(String(value)))
|
|
287
392
|
throw new Error(`Invalid command template expression variable: ${nameMatch[0]}`);
|
|
288
393
|
return Number(value);
|
|
289
394
|
}
|
|
@@ -313,7 +418,7 @@ function evaluateCommandTemplateExpression(
|
|
|
313
418
|
|
|
314
419
|
function substituteCommandTemplateExpression(
|
|
315
420
|
content: string,
|
|
316
|
-
values: Record<string,
|
|
421
|
+
values: Record<string, unknown>,
|
|
317
422
|
): string | undefined {
|
|
318
423
|
const padded = content.match(/^(_{1,6})\((.+)\)$/);
|
|
319
424
|
if (padded) {
|
|
@@ -323,22 +428,50 @@ function substituteCommandTemplateExpression(
|
|
|
323
428
|
return String(evaluateCommandTemplateExpression(content, values));
|
|
324
429
|
}
|
|
325
430
|
|
|
431
|
+
function resolveCommandTemplateValue(
|
|
432
|
+
content: string,
|
|
433
|
+
values: Record<string, unknown>,
|
|
434
|
+
missingLabel: string,
|
|
435
|
+
depth = 0,
|
|
436
|
+
): 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+)\]$/);
|
|
439
|
+
if (indexed) {
|
|
440
|
+
const source = values[indexed[1]];
|
|
441
|
+
const indexValue = /^\d+$/.test(indexed[2]) ? indexed[2] : values[indexed[2]];
|
|
442
|
+
const index = Number(indexValue);
|
|
443
|
+
if (!Array.isArray(source) || !Number.isInteger(index) || index < 0 || index >= source.length) {
|
|
444
|
+
throw new Error(`Missing ${missingLabel} value: ${content}`);
|
|
445
|
+
}
|
|
446
|
+
return String(source[index] ?? "");
|
|
447
|
+
}
|
|
448
|
+
const simple = parseCommandTemplatePlaceholderContent(content);
|
|
449
|
+
if (simple) {
|
|
450
|
+
if (Object.hasOwn(values, simple.name)) {
|
|
451
|
+
const raw = values[simple.name] ?? "";
|
|
452
|
+
if (typeof raw === "string" && /^\{[^{}]+\}$/.test(raw)) {
|
|
453
|
+
return substituteCommandTemplateToken(raw, values, missingLabel, depth + 1);
|
|
454
|
+
}
|
|
455
|
+
return Array.isArray(raw) ? JSON.stringify(raw) : String(raw);
|
|
456
|
+
}
|
|
457
|
+
if (simple.inlineDefault !== undefined) return simple.inlineDefault;
|
|
458
|
+
}
|
|
459
|
+
const expression = substituteCommandTemplateExpression(content, values);
|
|
460
|
+
if (expression !== undefined) return expression;
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
326
464
|
export function substituteCommandTemplateToken(
|
|
327
465
|
token: string,
|
|
328
|
-
values: Record<string,
|
|
466
|
+
values: Record<string, unknown>,
|
|
329
467
|
missingLabel = "command template",
|
|
468
|
+
depth = 0,
|
|
330
469
|
): string {
|
|
331
470
|
return token.replace(
|
|
332
471
|
/\{([^{}]+)\}/g,
|
|
333
472
|
(_match, content: string) => {
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
336
|
-
const [, name, inlineDefault] = simple;
|
|
337
|
-
if (Object.hasOwn(values, name)) return values[name] ?? "";
|
|
338
|
-
if (inlineDefault !== undefined) return inlineDefault;
|
|
339
|
-
}
|
|
340
|
-
const expression = substituteCommandTemplateExpression(content, values);
|
|
341
|
-
if (expression !== undefined) return expression;
|
|
473
|
+
const resolved = resolveCommandTemplateValue(content, values, missingLabel, depth);
|
|
474
|
+
if (resolved !== undefined) return resolved;
|
|
342
475
|
throw new Error(`Missing ${missingLabel} value: ${content}`);
|
|
343
476
|
},
|
|
344
477
|
);
|
|
@@ -405,8 +538,6 @@ function execCommandTemplateOnce(
|
|
|
405
538
|
}
|
|
406
539
|
if (options.timeout !== undefined && options.timeout > 0)
|
|
407
540
|
timeoutId = setTimeout(killProcess, options.timeout);
|
|
408
|
-
else if (options.timeout === undefined)
|
|
409
|
-
timeoutId = setTimeout(killProcess, DEFAULT_COMMAND_TIMEOUT_MS);
|
|
410
541
|
proc.stdout?.on("data", (data) => {
|
|
411
542
|
stdout += data.toString();
|
|
412
543
|
});
|
|
@@ -427,7 +558,7 @@ function execCommandTemplateOnce(
|
|
|
427
558
|
|
|
428
559
|
export function buildCommandTemplateInvocation(
|
|
429
560
|
config: CommandTemplateConfig,
|
|
430
|
-
values: Record<string,
|
|
561
|
+
values: Record<string, unknown>,
|
|
431
562
|
cwd: string,
|
|
432
563
|
options: { emptyMessage?: string; missingLabel?: string } = {},
|
|
433
564
|
): CommandTemplateInvocation {
|
package/lib/config.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConf
|
|
|
35
35
|
timeout?: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export type TelegramTimeMode = "
|
|
38
|
+
export type TelegramTimeMode = "hidden" | "always" | "interval";
|
|
39
39
|
|
|
40
40
|
export interface TelegramTimeConfig {
|
|
41
41
|
injectionMode?: TelegramTimeMode;
|
|
@@ -240,7 +240,7 @@ export function resolveTelegramTimeConfig(
|
|
|
240
240
|
const injectionMode: TelegramTimeMode =
|
|
241
241
|
raw?.injectionMode === "always" || raw?.injectionMode === "interval"
|
|
242
242
|
? raw.injectionMode
|
|
243
|
-
: "
|
|
243
|
+
: "hidden";
|
|
244
244
|
const interval =
|
|
245
245
|
typeof raw?.interval === "number" && raw.interval > 0
|
|
246
246
|
? raw.interval
|
package/lib/lifecycle.ts
CHANGED
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
BeforeAgentStartEvent,
|
|
11
11
|
ExtensionAPI,
|
|
12
12
|
ExtensionContext,
|
|
13
|
+
SessionBeforeCompactEvent,
|
|
14
|
+
SessionCompactEvent,
|
|
13
15
|
SessionShutdownEvent,
|
|
14
16
|
SessionStartEvent,
|
|
15
17
|
} from "./pi.ts";
|
|
@@ -50,6 +52,14 @@ export interface TelegramLifecycleRegistrationDeps {
|
|
|
50
52
|
event: SessionShutdownEvent,
|
|
51
53
|
ctx: ExtensionContext,
|
|
52
54
|
) => Promise<void>;
|
|
55
|
+
onSessionBeforeCompact?: (
|
|
56
|
+
event: SessionBeforeCompactEvent,
|
|
57
|
+
ctx: ExtensionContext,
|
|
58
|
+
) => Promise<void> | void;
|
|
59
|
+
onSessionCompact?: (
|
|
60
|
+
event: SessionCompactEvent,
|
|
61
|
+
ctx: ExtensionContext,
|
|
62
|
+
) => Promise<void> | void;
|
|
53
63
|
onBeforeAgentStart: (
|
|
54
64
|
event: BeforeAgentStartEvent,
|
|
55
65
|
ctx: ExtensionContext,
|
|
@@ -92,6 +102,76 @@ export interface TelegramSessionLifecycleHooks {
|
|
|
92
102
|
) => Promise<void>;
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
type TelegramLifecycleTimer = number | ReturnType<typeof setTimeout>;
|
|
106
|
+
|
|
107
|
+
export interface TelegramCompactionObserverRuntimeDeps<TContext> {
|
|
108
|
+
setCompactionInProgress: (inProgress: boolean) => void;
|
|
109
|
+
updateStatus: (ctx: TContext) => void;
|
|
110
|
+
requestDeferredDispatchNextQueuedTelegramTurn: (
|
|
111
|
+
dispatch: (ctx: TContext) => void,
|
|
112
|
+
) => void;
|
|
113
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
114
|
+
recordRuntimeEvent?: (category: string, error: unknown) => void;
|
|
115
|
+
timeoutMs?: number;
|
|
116
|
+
setTimer?: (
|
|
117
|
+
callback: () => void,
|
|
118
|
+
ms: number,
|
|
119
|
+
) => TelegramLifecycleTimer;
|
|
120
|
+
clearTimer?: (timer: TelegramLifecycleTimer) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface TelegramCompactionObserverRuntime<TContext> {
|
|
124
|
+
onSessionBeforeCompact: (
|
|
125
|
+
event: SessionBeforeCompactEvent,
|
|
126
|
+
ctx: TContext,
|
|
127
|
+
) => void;
|
|
128
|
+
onSessionCompact: (event: SessionCompactEvent, ctx: TContext) => void;
|
|
129
|
+
onSessionShutdown: () => void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createTelegramCompactionObserverRuntime<TContext>(
|
|
133
|
+
deps: TelegramCompactionObserverRuntimeDeps<TContext>,
|
|
134
|
+
): TelegramCompactionObserverRuntime<TContext> {
|
|
135
|
+
const timeoutMs = deps.timeoutMs ?? 300_000;
|
|
136
|
+
const setTimer = deps.setTimer ?? setTimeout;
|
|
137
|
+
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
138
|
+
let fallbackTimer: TelegramLifecycleTimer | undefined;
|
|
139
|
+
const clearFallbackTimer = (): void => {
|
|
140
|
+
if (!fallbackTimer) return;
|
|
141
|
+
clearTimer(fallbackTimer);
|
|
142
|
+
fallbackTimer = undefined;
|
|
143
|
+
};
|
|
144
|
+
const requestDispatch = (): void => {
|
|
145
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(
|
|
146
|
+
deps.dispatchNextQueuedTelegramTurn,
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
onSessionBeforeCompact: (_event, ctx) => {
|
|
151
|
+
deps.setCompactionInProgress(true);
|
|
152
|
+
deps.updateStatus(ctx);
|
|
153
|
+
clearFallbackTimer();
|
|
154
|
+
fallbackTimer = setTimer(() => {
|
|
155
|
+
fallbackTimer = undefined;
|
|
156
|
+
deps.setCompactionInProgress(false);
|
|
157
|
+
deps.updateStatus(ctx);
|
|
158
|
+
deps.recordRuntimeEvent?.(
|
|
159
|
+
"compact",
|
|
160
|
+
new Error("Compaction observer timed out"),
|
|
161
|
+
);
|
|
162
|
+
requestDispatch();
|
|
163
|
+
}, timeoutMs);
|
|
164
|
+
},
|
|
165
|
+
onSessionCompact: (_event, ctx) => {
|
|
166
|
+
clearFallbackTimer();
|
|
167
|
+
deps.setCompactionInProgress(false);
|
|
168
|
+
deps.updateStatus(ctx);
|
|
169
|
+
requestDispatch();
|
|
170
|
+
},
|
|
171
|
+
onSessionShutdown: clearFallbackTimer,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
95
175
|
export function createDedupAgentStartHook(
|
|
96
176
|
dedup: { reset(): void },
|
|
97
177
|
inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>,
|
|
@@ -139,6 +219,12 @@ export function registerTelegramLifecycleHooks(
|
|
|
139
219
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
140
220
|
await deps.onSessionShutdown(event, ctx);
|
|
141
221
|
});
|
|
222
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
223
|
+
await deps.onSessionBeforeCompact?.(event, ctx);
|
|
224
|
+
});
|
|
225
|
+
pi.on("session_compact", async (event, ctx) => {
|
|
226
|
+
await deps.onSessionCompact?.(event, ctx);
|
|
227
|
+
});
|
|
142
228
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
143
229
|
return deps.onBeforeAgentStart(event, ctx);
|
|
144
230
|
});
|
package/lib/menu-settings.ts
CHANGED
|
@@ -135,17 +135,24 @@ export function buildTelegramSettingsMenuText(): string {
|
|
|
135
135
|
return SETTINGS_MENU_TITLE;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
export function buildProactivePushSettingsText(
|
|
138
|
+
export function buildProactivePushSettingsText(
|
|
139
|
+
proactivePushEnabled: boolean,
|
|
140
|
+
): string {
|
|
139
141
|
return [
|
|
140
|
-
PROACTIVE_PUSH_SETTINGS_TITLE
|
|
142
|
+
`${PROACTIVE_PUSH_SETTINGS_TITLE} <code>${proactivePushEnabled ? "on" : "off"}</code>`,
|
|
141
143
|
"",
|
|
142
144
|
"Send successful local π task results to Telegram when the bridge is connected.",
|
|
143
145
|
].join("\n");
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
export function buildVoiceReplyModeSettingsText(
|
|
148
|
+
export function buildVoiceReplyModeSettingsText(
|
|
149
|
+
mode: TelegramVoiceReplyMode,
|
|
150
|
+
configured = true,
|
|
151
|
+
): string {
|
|
147
152
|
return [
|
|
148
|
-
VOICE_REPLY_MODE_SETTINGS_TITLE
|
|
153
|
+
`${VOICE_REPLY_MODE_SETTINGS_TITLE} <code>${getVoiceReplyModeLabel(
|
|
154
|
+
getVoiceReplyModeSetting(mode, configured),
|
|
155
|
+
)}</code>`,
|
|
149
156
|
"",
|
|
150
157
|
"Controls when pi-telegram converts assistant text replies into Telegram voice messages.",
|
|
151
158
|
"",
|
|
@@ -156,13 +163,15 @@ export function buildVoiceReplyModeSettingsText(): string {
|
|
|
156
163
|
].join("\n");
|
|
157
164
|
}
|
|
158
165
|
|
|
159
|
-
export function buildTimeInjectionModeSettingsText(
|
|
166
|
+
export function buildTimeInjectionModeSettingsText(
|
|
167
|
+
mode: TelegramTimeMode,
|
|
168
|
+
): string {
|
|
160
169
|
return [
|
|
161
|
-
TIME_INJECTION_MODE_SETTINGS_TITLE
|
|
170
|
+
`${TIME_INJECTION_MODE_SETTINGS_TITLE} <code>${mode}</code>`,
|
|
162
171
|
"",
|
|
163
172
|
"Controls whether Telegram-originated prompts include a compact wall-clock [time] line.",
|
|
164
173
|
"",
|
|
165
|
-
"<code>-</code> <code>
|
|
174
|
+
"<code>-</code> <code>hidden</code> (default): no time line is added to prompt context.",
|
|
166
175
|
"<code>-</code> <code>always</code>: add time to every Telegram turn.",
|
|
167
176
|
"<code>-</code> <code>interval</code>: add time at most once per chat interval (default: 1 hour).",
|
|
168
177
|
].join("\n");
|
|
@@ -257,7 +266,7 @@ export function buildProactivePushSettingsReplyMarkup(
|
|
|
257
266
|
export function buildTimeInjectionModeSettingsReplyMarkup(
|
|
258
267
|
mode: TelegramTimeMode,
|
|
259
268
|
): TelegramSettingsMenuReplyMarkup {
|
|
260
|
-
const modes: TelegramTimeMode[] = ["
|
|
269
|
+
const modes: TelegramTimeMode[] = ["hidden", "always", "interval"];
|
|
261
270
|
return {
|
|
262
271
|
inline_keyboard: [
|
|
263
272
|
[{ text: "⬆️ Back", callback_data: "settings:list" }],
|
|
@@ -314,30 +323,31 @@ export async function updateTelegramSettingsMenuMessage(
|
|
|
314
323
|
export async function updateProactivePushSettingsMessage(
|
|
315
324
|
deps: TelegramSettingsMenuCallbackDeps,
|
|
316
325
|
): Promise<void> {
|
|
326
|
+
const proactivePushEnabled = deps.isProactivePushEnabled();
|
|
317
327
|
await deps.updateSettingsMessage(
|
|
318
|
-
buildProactivePushSettingsText(),
|
|
319
|
-
buildProactivePushSettingsReplyMarkup(
|
|
328
|
+
buildProactivePushSettingsText(proactivePushEnabled),
|
|
329
|
+
buildProactivePushSettingsReplyMarkup(proactivePushEnabled),
|
|
320
330
|
);
|
|
321
331
|
}
|
|
322
332
|
|
|
323
333
|
export async function updateTimeInjectionModeSettingsMessage(
|
|
324
334
|
deps: TelegramSettingsMenuCallbackDeps,
|
|
325
335
|
): Promise<void> {
|
|
336
|
+
const mode = deps.getTimeInjectionMode();
|
|
326
337
|
await deps.updateSettingsMessage(
|
|
327
|
-
buildTimeInjectionModeSettingsText(),
|
|
328
|
-
buildTimeInjectionModeSettingsReplyMarkup(
|
|
338
|
+
buildTimeInjectionModeSettingsText(mode),
|
|
339
|
+
buildTimeInjectionModeSettingsReplyMarkup(mode),
|
|
329
340
|
);
|
|
330
341
|
}
|
|
331
342
|
|
|
332
343
|
export async function updateVoiceReplyModeSettingsMessage(
|
|
333
344
|
deps: TelegramSettingsMenuCallbackDeps,
|
|
334
345
|
): Promise<void> {
|
|
346
|
+
const mode = deps.getVoiceReplyMode();
|
|
347
|
+
const configured = deps.isVoiceReplyModeConfigured();
|
|
335
348
|
await deps.updateSettingsMessage(
|
|
336
|
-
buildVoiceReplyModeSettingsText(),
|
|
337
|
-
buildVoiceReplyModeSettingsReplyMarkup(
|
|
338
|
-
deps.getVoiceReplyMode(),
|
|
339
|
-
deps.isVoiceReplyModeConfigured(),
|
|
340
|
-
),
|
|
349
|
+
buildVoiceReplyModeSettingsText(mode, configured),
|
|
350
|
+
buildVoiceReplyModeSettingsReplyMarkup(mode, configured),
|
|
341
351
|
);
|
|
342
352
|
}
|
|
343
353
|
|
|
@@ -391,10 +401,18 @@ export async function handleTelegramSettingsMenuCallbackAction(
|
|
|
391
401
|
const mode = data.startsWith("settings:set:time-injection:")
|
|
392
402
|
? data.slice("settings:set:time-injection:".length)
|
|
393
403
|
: data.slice("settings:set:time:".length);
|
|
394
|
-
|
|
395
|
-
|
|
404
|
+
const normalizedMode = mode === "off" ? "hidden" : mode;
|
|
405
|
+
if (
|
|
406
|
+
normalizedMode === "hidden" ||
|
|
407
|
+
normalizedMode === "always" ||
|
|
408
|
+
normalizedMode === "interval"
|
|
409
|
+
) {
|
|
410
|
+
await deps.setTimeInjectionMode(normalizedMode);
|
|
396
411
|
await updateTimeInjectionModeSettingsMessage(deps);
|
|
397
|
-
await deps.answerCallbackQuery(
|
|
412
|
+
await deps.answerCallbackQuery(
|
|
413
|
+
callbackQueryId,
|
|
414
|
+
`Time injection: ${normalizedMode}`,
|
|
415
|
+
);
|
|
398
416
|
return true;
|
|
399
417
|
}
|
|
400
418
|
}
|
|
@@ -490,13 +508,15 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
490
508
|
if (
|
|
491
509
|
(hasTimeInjectionPrefix || query.data.startsWith("settings:set:time:")) &&
|
|
492
510
|
(timeMode === "off" ||
|
|
511
|
+
timeMode === "hidden" ||
|
|
493
512
|
timeMode === "always" ||
|
|
494
513
|
timeMode === "interval")
|
|
495
514
|
) {
|
|
496
|
-
|
|
515
|
+
const normalizedMode = timeMode === "off" ? "hidden" : timeMode;
|
|
516
|
+
await deps.setTimeInjectionMode(normalizedMode);
|
|
497
517
|
await deps.answerCallbackQuery(
|
|
498
518
|
query.id,
|
|
499
|
-
`Time injection: ${
|
|
519
|
+
`Time injection: ${normalizedMode}`,
|
|
500
520
|
);
|
|
501
521
|
return true;
|
|
502
522
|
}
|