Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a drop-in extension for can1357/oh-my-pi (omp >= 16.5.2). Changes vs upstream: - Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram - Bump version: 0.22.0 -> 0.22.0-evandro.1 - Update repository/homepage/bugs URLs to evandrodevbr - Add omp field in package.json (displayName + compatibleWith) - README: prepend 'Oh My Pi (omp) Compatibility' section with install commands, verified-components table, upstream lineage, and rationale - CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased - docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram - tests/public-api.test.ts: rename package import paths Gates re-run on the fork: - npm run typecheck -> tsc --noEmit (success) - npm test -> 1229 pass / 0 fail / 1 platform-only skip - npm run audit -> found 0 vulnerabilities - npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz Verified: - omp install --link ./pi-telegram -> Linked OK - omp plugin list -> @evandrodevbr/[email protected].0-evandro.1 Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
159 lines
4.7 KiB
TypeScript
159 lines
4.7 KiB
TypeScript
/**
|
|
* Pi prompt-template bridge helpers
|
|
* Zones: pi agent prompts, telegram controls, filesystem
|
|
* Discovers Pi prompt-template slash commands and expands them before Telegram queue dispatch
|
|
*/
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import type { PiSlashCommandInfo } from "./pi.ts";
|
|
|
|
export interface TelegramPromptTemplateCommand {
|
|
command: string;
|
|
description?: string;
|
|
path: string;
|
|
}
|
|
|
|
export type TelegramPromptTemplateReader = (path: string) => string;
|
|
|
|
const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
|
|
|
|
function stripPromptTemplateFrontmatter(content: string): string {
|
|
if (!content.startsWith("---")) return content;
|
|
const lines = content.split("\n");
|
|
if (lines[0]?.trim() !== "---") return content;
|
|
for (let index = 1; index < lines.length; index += 1) {
|
|
if (lines[index]?.trim() === "---")
|
|
return lines.slice(index + 1).join("\n");
|
|
}
|
|
return content;
|
|
}
|
|
|
|
export function parsePromptTemplateArgs(argsString: string): string[] {
|
|
const args: string[] = [];
|
|
let current = "";
|
|
let quote: string | undefined;
|
|
for (const char of argsString) {
|
|
if (quote) {
|
|
if (char === quote) {
|
|
quote = undefined;
|
|
} else {
|
|
current += char;
|
|
}
|
|
continue;
|
|
}
|
|
if (char === '"' || char === "'") {
|
|
quote = char;
|
|
continue;
|
|
}
|
|
if (char === " " || char === "\t") {
|
|
if (current) args.push(current);
|
|
current = "";
|
|
continue;
|
|
}
|
|
current += char;
|
|
}
|
|
if (current) args.push(current);
|
|
return args;
|
|
}
|
|
|
|
export function substitutePromptTemplateArgs(
|
|
content: string,
|
|
args: readonly string[],
|
|
): string {
|
|
let result = content.replace(/\$(\d+)/g, (_, num: string) => {
|
|
const index = Number.parseInt(num, 10) - 1;
|
|
return args[index] ?? "";
|
|
});
|
|
result = result.replace(
|
|
/\$\{@:(\d+)(?::(\d+))?\}/g,
|
|
(_, startValue: string, lengthValue: string | undefined) => {
|
|
const start = Math.max(Number.parseInt(startValue, 10) - 1, 0);
|
|
if (lengthValue) {
|
|
const length = Number.parseInt(lengthValue, 10);
|
|
return args.slice(start, start + length).join(" ");
|
|
}
|
|
return args.slice(start).join(" ");
|
|
},
|
|
);
|
|
const allArgs = args.join(" ");
|
|
return result.replace(/\$ARGUMENTS/g, allArgs).replace(/\$@/g, allArgs);
|
|
}
|
|
|
|
export function isTelegramPromptTemplateCommandName(name: string): boolean {
|
|
return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name);
|
|
}
|
|
|
|
export function mapPiPromptTemplateNameToTelegramCommandName(
|
|
name: string,
|
|
): string | undefined {
|
|
const command = name
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9_]+/g, "_")
|
|
.replace(/_+/g, "_")
|
|
.replace(/^_+|_+$/g, "")
|
|
.slice(0, 32)
|
|
.replace(/_+$/g, "");
|
|
return isTelegramPromptTemplateCommandName(command) ? command : undefined;
|
|
}
|
|
|
|
export interface TelegramPromptTemplateCommandGetterDeps {
|
|
getCommands: () => readonly PiSlashCommandInfo[];
|
|
reservedCommandNames?: readonly string[];
|
|
getReservedCommandNames?: () => readonly string[];
|
|
}
|
|
|
|
export function getTelegramPromptTemplateCommands(
|
|
commands: readonly PiSlashCommandInfo[],
|
|
reservedNames: ReadonlySet<string> = new Set(),
|
|
): TelegramPromptTemplateCommand[] {
|
|
const seen = new Set<string>();
|
|
const promptCommands: TelegramPromptTemplateCommand[] = [];
|
|
for (const command of commands) {
|
|
if (command.source !== "prompt") continue;
|
|
const telegramCommand = mapPiPromptTemplateNameToTelegramCommandName(
|
|
command.name,
|
|
);
|
|
if (!telegramCommand) continue;
|
|
if (reservedNames.has(telegramCommand)) continue;
|
|
if (seen.has(telegramCommand)) continue;
|
|
const sourcePath = command.sourceInfo?.path;
|
|
if (!sourcePath) continue;
|
|
seen.add(telegramCommand);
|
|
promptCommands.push({
|
|
command: telegramCommand,
|
|
description: command.description,
|
|
path: sourcePath,
|
|
});
|
|
}
|
|
return promptCommands.sort((a, b) => a.command.localeCompare(b.command));
|
|
}
|
|
|
|
export function createTelegramPromptTemplateCommandGetter(
|
|
deps: TelegramPromptTemplateCommandGetterDeps,
|
|
): () => TelegramPromptTemplateCommand[] {
|
|
return () => {
|
|
return getTelegramPromptTemplateCommands(
|
|
deps.getCommands(),
|
|
new Set([
|
|
...(deps.reservedCommandNames ?? []),
|
|
...(deps.getReservedCommandNames?.() ?? []),
|
|
]),
|
|
);
|
|
};
|
|
}
|
|
|
|
export function expandTelegramPromptTemplateCommand(
|
|
commandName: string,
|
|
args: string,
|
|
commands: readonly TelegramPromptTemplateCommand[],
|
|
readTemplate: TelegramPromptTemplateReader = (path) =>
|
|
readFileSync(path, "utf-8"),
|
|
): string | undefined {
|
|
const command = commands.find(
|
|
(candidate) => candidate.command === commandName,
|
|
);
|
|
if (!command) return undefined;
|
|
const content = stripPromptTemplateFrontmatter(readTemplate(command.path));
|
|
return substitutePromptTemplateArgs(content, parsePromptTemplateArgs(args));
|
|
}
|